diff --git a/contributing.md b/contributing.md index ef8546f0..d5db0b42 100644 --- a/contributing.md +++ b/contributing.md @@ -1,15 +1,25 @@ -#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem! +## Got a issue/feature request or submitting a pull request? -##Just make sure you think of the following things: +Make sure you think of the following things: - * Search through the existing (and closed) issues first. See if you can get your answer there. +## Issue + * Search through the existing (and closed) issues first, see if you can get your answer there. * Double check the result manually, because it could be an external issue. * Post logs! Without seeing what is going on, I can't reproduce the error. - * What is the movie + quality you are searching for. - * What are you settings for the specific problem. - * What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby). - * Give me a short step by step of how to reproduce. + * Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP. + * What is the movie + quality you are searching for? + * What are you're settings for the specific problem? + * What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby) + * Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs! + * Give a short step by step of how to reproduce the error. * What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example. - * I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;) + * I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;) + * If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!! -**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)** +## Pull Request + * Make sure you're pull request is made for develop branch (or relevant feature branch) + * Have you tested your PR? If not, why? + * Are there any limitations of your PR we should know of? + * Make sure to keep you're PR up-to-date with the branch you're trying to push into. + +**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)** diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 1b7f1636..b80ddcc1 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -34,6 +34,8 @@ class ClientScript(Plugin): 'scripts/library/question.js', 'scripts/library/scrollspy.js', 'scripts/library/spin.js', + 'scripts/library/Array.stableSort.js', + 'scripts/library/async.js', 'scripts/couchpotato.js', 'scripts/api.js', 'scripts/library/history.js', diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 2c97e1b4..773213be 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -31,13 +31,13 @@ class Scheduler(Plugin): pass def doShutdown(self): - super(Scheduler, self).doShutdown() self.stop() + return super(Scheduler, self).doShutdown() def stop(self): if self.started: log.debug('Stopping scheduler') - self.sched.shutdown() + self.sched.shutdown(wait = False) log.debug('Scheduler stopped') self.started = False diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index aecf0c4f..648c2c4c 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -183,9 +183,6 @@ class GitUpdater(BaseUpdater): def doUpdate(self): try: - log.debug('Stashing local changes') - self.repo.saveStash() - log.info('Updating to latest version') self.repo.pull() diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js index 0577c783..860ad514 100644 --- a/couchpotato/core/_base/updater/static/updater.js +++ b/couchpotato/core/_base/updater/static/updater.js @@ -24,7 +24,7 @@ var UpdaterBase = new Class({ self.doUpdate(); else { App.unBlockPage(); - App.fireEvent('message', 'No updates available'); + App.on('message', 'No updates available'); } } }) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 9e24d914..b6894edf 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -49,21 +49,26 @@ class Downloader(Provider): return [] - def _download(self, data = None, movie = None, manual = False, filedata = None): - if not movie: movie = {} + def _download(self, data = None, media = None, manual = False, filedata = None): + if not media: media = {} if not data: data = {} if self.isDisabled(manual, data): return - return self.download(data = data, movie = movie, filedata = filedata) + return self.download(data = data, media = media, filedata = filedata) - def _getAllDownloadStatus(self): + def _getAllDownloadStatus(self, download_ids): if self.isDisabled(manual = True, data = {}): return - return self.getAllDownloadStatus() + ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()] - def getAllDownloadStatus(self): + if ids: + return self.getAllDownloadStatus(ids) + else: + return + + def getAllDownloadStatus(self, ids): return def _removeFailed(self, release_download): diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py index 6b5279a1..91164d66 100644 --- a/couchpotato/core/downloaders/blackhole/__init__.py +++ b/couchpotato/core/downloaders/blackhole/__init__.py @@ -13,7 +13,7 @@ config = [{ 'list': 'download_providers', 'name': 'blackhole', 'label': 'Black hole', - 'description': 'Download the NZB/Torrent to a specific folder.', + 'description': 'Download the NZB/Torrent to a specific folder. Note: Seeding and copying/linking features do not work with Black hole.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index 854860cd..5216370f 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -12,8 +12,8 @@ class Blackhole(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -33,7 +33,7 @@ class Blackhole(Downloader): log.error('No nzb/torrent available: %s', data.get('url')) return False - file_name = self.createFileName(data, filedata, movie) + file_name = self.createFileName(data, filedata, media) full_path = os.path.join(directory, file_name) if self.conf('create_subdir'): @@ -51,10 +51,10 @@ class Blackhole(Downloader): with open(full_path, 'wb') as f: f.write(filedata) os.chmod(full_path, Env.getPermission('file')) - return True + return self.downloadReturnId('') else: log.info('File %s already exists.', full_path) - return True + return self.downloadReturnId('') except: log.error('Failed to download to blackhole %s', traceback.format_exc()) diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py index f65b74ae..57c19580 100644 --- a/couchpotato/core/downloaders/deluge/main.py +++ b/couchpotato/core/downloaders/deluge/main.py @@ -32,7 +32,10 @@ class Deluge(Downloader): return self.drpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) if not self.connect(): @@ -73,7 +76,7 @@ class Deluge(Downloader): if data.get('protocol') == 'torrent_magnet': remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) else: - filename = self.createFileName(data, filedata, movie) + filename = self.createFileName(data, filedata, media) remote_torrent = self.drpc.add_torrent_file(filename, filedata, options) if not remote_torrent: @@ -83,7 +86,7 @@ class Deluge(Downloader): log.info('Torrent sent to Deluge successfully.') return self.downloadReturnId(remote_torrent) - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking Deluge download status.') @@ -100,38 +103,39 @@ class Deluge(Downloader): for torrent_id in queue: torrent = queue[torrent_id] - log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused'])) - - # Deluge has no easy way to work out if a torrent is stalled or failing. - #status = 'failed' - status = 'busy' - if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']): - # We have torrent['seeding_time'] to work out what the seeding time is, but we do not - # have access to the downloader seed_time, as with deluge we have no way to pass it - # when the torrent is added. So Deluge will only look at the ratio. - # See above comment in download(). - status = 'seeding' - elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused': - status = 'completed' - - download_dir = sp(torrent['save_path']) - if torrent['move_on_completed']: - download_dir = torrent['move_completed_path'] - - torrent_files = [] - for file_item in torrent['files']: - torrent_files.append(os.path.join(download_dir), sp(file_item['path'])) - - release_downloads.append({ - 'id': torrent['hash'], - 'name': torrent['name'], - 'status': status, - 'original_status': torrent['state'], - 'seed_ratio': torrent['ratio'], - 'timeleft': str(timedelta(seconds = torrent['eta'])), - 'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])), - 'files': '|'.join(torrent_files), - }) + if torrent['hash'] in ids: + log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused'])) + + # Deluge has no easy way to work out if a torrent is stalled or failing. + #status = 'failed' + status = 'busy' + if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']): + # We have torrent['seeding_time'] to work out what the seeding time is, but we do not + # have access to the downloader seed_time, as with deluge we have no way to pass it + # when the torrent is added. So Deluge will only look at the ratio. + # See above comment in download(). + status = 'seeding' + elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused': + status = 'completed' + + download_dir = sp(torrent['save_path']) + if torrent['move_on_completed']: + download_dir = torrent['move_completed_path'] + + torrent_files = [] + for file_item in torrent['files']: + torrent_files.append(sp(os.path.join(download_dir, file_item['path']))) + + release_downloads.append({ + 'id': torrent['hash'], + 'name': torrent['name'], + 'status': status, + 'original_status': torrent['state'], + 'seed_ratio': torrent['ratio'], + 'timeleft': str(timedelta(seconds = torrent['eta'])), + 'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])), + 'files': '|'.join(torrent_files), + }) return release_downloads diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index f8506134..482da2ca 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -19,8 +19,8 @@ class NZBGet(Downloader): url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} if not filedata: @@ -30,7 +30,7 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.', data.get('name')) url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} - nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) + nzb_name = ss('%s.nzb' % self.createNzbName(data, media)) rpc = xmlrpclib.ServerProxy(url) try: @@ -67,7 +67,7 @@ class NZBGet(Downloader): log.error('NZBGet could not add %s to the queue.', nzb_name) return False - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking NZBGet download status.') @@ -102,51 +102,54 @@ class NZBGet(Downloader): release_downloads = ReleaseDownloadList(self) for nzb in groups: - log.debug('Found %s in NZBGet download queue', nzb['NZBFilename']) try: nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] except: nzb_id = nzb['NZBID'] - - timeleft = -1 - try: - if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']): - timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) - except: - pass - - release_downloads.append({ - 'id': nzb_id, - 'name': nzb['NZBFilename'], - 'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED', - # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item - 'timeleft': timeleft, - }) + if nzb_id in ids: + log.debug('Found %s in NZBGet download queue', nzb['NZBFilename']) + timeleft = -1 + try: + if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']): + timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) + except: + pass + + release_downloads.append({ + 'id': nzb_id, + 'name': nzb['NZBFilename'], + 'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED', + # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item + 'timeleft': timeleft, + }) for nzb in queue: # 'Parameters' is not passed in rpc.postqueue - log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) - release_downloads.append({ - 'id': nzb['NZBID'], - 'name': nzb['NZBFilename'], - 'original_status': nzb['Stage'], - 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, - }) + if nzb['NZBID'] in ids: + log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) + release_downloads.append({ + 'id': nzb['NZBID'], + 'name': nzb['NZBFilename'], + 'original_status': nzb['Stage'], + 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, + }) for nzb in history: - log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) try: nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] except: nzb_id = nzb['NZBID'] - release_downloads.append({ - 'id': nzb_id, - 'name': nzb['NZBFilename'], - 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', - 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], - 'timeleft': str(timedelta(seconds = 0)), - 'folder': sp(nzb['DestDir']) - }) + + if nzb_id in ids: + log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) + release_downloads.append({ + 'id': nzb_id, + 'name': nzb['NZBFilename'], + 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', + 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': sp(nzb['DestDir']) + }) return release_downloads diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index f4e233be..bcf8cae7 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -8,9 +8,11 @@ from uuid import uuid4 import hashlib import httplib import json +import os import socket import ssl import sys +import time import traceback import urllib2 @@ -23,44 +25,46 @@ class NZBVortex(Downloader): api_level = None session_id = None - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} # Send the nzb try: - nzb_filename = self.createFileName(data, filedata, movie) + nzb_filename = self.createFileName(data, filedata, media) self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True) + time.sleep(10) raw_statuses = self.call('nzb') - nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0] + nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0] return self.downloadReturnId(nzb_id) except: log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) return False - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): raw_statuses = self.call('nzb') release_downloads = ReleaseDownloadList(self) for nzb in raw_statuses.get('nzbs', []): + if nzb['id'] in ids: - # Check status - status = 'busy' - if nzb['state'] == 20: - status = 'completed' - elif nzb['state'] in [21, 22, 24]: - status = 'failed' - - release_downloads.append({ - 'id': nzb['id'], - 'name': nzb['uiTitle'], - 'status': status, - 'original_status': nzb['state'], - 'timeleft':-1, - 'folder': sp(nzb['destinationPath']), - }) + # Check status + status = 'busy' + if nzb['state'] == 20: + status = 'completed' + elif nzb['state'] in [21, 22, 24]: + status = 'failed' + + release_downloads.append({ + 'id': nzb['id'], + 'name': nzb['uiTitle'], + 'status': status, + 'original_status': nzb['state'], + 'timeleft':-1, + 'folder': sp(nzb['destinationPath']), + }) return release_downloads diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py index 643350e1..863cdb07 100644 --- a/couchpotato/core/downloaders/pneumatic/main.py +++ b/couchpotato/core/downloaders/pneumatic/main.py @@ -12,8 +12,8 @@ class Pneumatic(Downloader): protocol = ['nzb'] strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -25,7 +25,7 @@ class Pneumatic(Downloader): log.error('No nzb available!') return False - fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) + fullPath = os.path.join(directory, self.createFileName(data, filedata, media)) try: if not os.path.isfile(fullPath): @@ -33,7 +33,7 @@ class Pneumatic(Downloader): with open(fullPath, 'wb') as f: f.write(filedata) - nzb_name = self.createNzbName(data, movie) + nzb_name = self.createNzbName(data, media) strm_path = os.path.join(directory, nzb_name) strm_file = open(strm_path + '.strm', 'wb') @@ -41,11 +41,11 @@ class Pneumatic(Downloader): strm_file.write(strmContent) strm_file.close() - return True + return self.downloadReturnId('') else: log.info('File %s already exists.', fullPath) - return True + return self.downloadReturnId('') except: log.error('Failed to download .strm: %s', traceback.format_exc()) diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py index 026a56c6..684ea45e 100755 --- a/couchpotato/core/downloaders/rtorrent/__init__.py +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -58,14 +58,6 @@ config = [{ 'advanced': True, 'description': 'Also remove the leftover files.', }, - { - 'name': 'append_label', - 'label': 'Append Label', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'description': 'Append label to download location. Requires you to set the download location above.', - }, { 'name': 'paused', 'type': 'bool', diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index d7ae589f..2cbfd902 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -77,7 +77,10 @@ class rTorrent(Downloader): return True - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.debug('Sending "%s" to rTorrent.', (data.get('name'))) if not self.connect(): @@ -125,9 +128,7 @@ class rTorrent(Downloader): if self.conf('label'): torrent.set_custom(1, self.conf('label')) - if self.conf('directory') and self.conf('append_label'): - torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label'))) - elif self.conf('directory'): + if self.conf('directory'): torrent.set_directory(self.conf('directory')) # Set Ratio Group @@ -142,7 +143,7 @@ class rTorrent(Downloader): log.error('Failed to send torrent to rTorrent: %s', err) return False - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking rTorrent download status.') if not self.connect(): @@ -154,27 +155,28 @@ class rTorrent(Downloader): release_downloads = ReleaseDownloadList(self) for torrent in torrents: - torrent_files = [] - for file_item in torrent.get_files(): - torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) - - status = 'busy' - if torrent.complete: - if torrent.active: - status = 'seeding' - else: - status = 'completed' - - release_downloads.append({ - 'id': torrent.info_hash, - 'name': torrent.name, - 'status': status, - 'seed_ratio': torrent.ratio, - 'original_status': torrent.state, - 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, - 'folder': sp(torrent.directory), - 'files': '|'.join(torrent_files) - }) + if torrent.info_hash in ids: + torrent_files = [] + for file_item in torrent.get_files(): + torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) + + status = 'busy' + if torrent.complete: + if torrent.active: + status = 'seeding' + else: + status = 'completed' + + release_downloads.append({ + 'id': torrent.info_hash, + 'name': torrent.name, + 'status': status, + 'seed_ratio': torrent.ratio, + 'original_status': torrent.state, + 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, + 'folder': sp(torrent.directory), + 'files': '|'.join(torrent_files) + }) return release_downloads diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index aba21231..808ceab7 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -16,8 +16,8 @@ class Sabnzbd(Downloader): protocol = ['nzb'] - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} log.info('Sending "%s" to SABnzbd.', data.get('name')) @@ -25,7 +25,7 @@ class Sabnzbd(Downloader): req_params = { 'cat': self.conf('category'), 'mode': 'addurl', - 'nzbname': self.createNzbName(data, movie), + 'nzbname': self.createNzbName(data, media), 'priority': self.conf('priority'), } @@ -36,7 +36,7 @@ class Sabnzbd(Downloader): return False # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb - nzb_filename = self.createFileName(data, filedata, movie) + nzb_filename = self.createFileName(data, filedata, media) req_params['mode'] = 'addfile' else: req_params['name'] = data.get('url') @@ -64,7 +64,7 @@ class Sabnzbd(Downloader): log.error('Error getting data from SABNZBd: %s', sab_data) return False - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking SABnzbd download status.') @@ -91,35 +91,36 @@ class Sabnzbd(Downloader): # Get busy releases for nzb in queue.get('slots', []): - status = 'busy' - if 'ENCRYPTED / ' in nzb['filename']: - status = 'failed' - - release_downloads.append({ - 'id': nzb['nzo_id'], - 'name': nzb['filename'], - 'status': status, - 'original_status': nzb['status'], - 'timeleft': nzb['timeleft'] if not queue['paused'] else -1, - }) + if nzb['nzo_id'] in ids: + status = 'busy' + if 'ENCRYPTED / ' in nzb['filename']: + status = 'failed' + + release_downloads.append({ + 'id': nzb['nzo_id'], + 'name': nzb['filename'], + 'status': status, + 'original_status': nzb['status'], + 'timeleft': nzb['timeleft'] if not queue['paused'] else -1, + }) # Get old releases for nzb in history.get('slots', []): - - status = 'busy' - if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()): - status = 'failed' - elif nzb['status'] == 'Completed': - status = 'completed' - - release_downloads.append({ - 'id': nzb['nzo_id'], - 'name': nzb['name'], - 'status': status, - 'original_status': nzb['status'], - 'timeleft': str(timedelta(seconds = 0)), - 'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']), - }) + if nzb['nzo_id'] in ids: + status = 'busy' + if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()): + status = 'failed' + elif nzb['status'] == 'Completed': + status = 'completed' + + release_downloads.append({ + 'id': nzb['nzo_id'], + 'name': nzb['name'], + 'status': status, + 'original_status': nzb['status'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']), + }) return release_downloads diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology/main.py index d5082c77..5d192fcf 100644 --- a/couchpotato/core/downloaders/synology/main.py +++ b/couchpotato/core/downloaders/synology/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import isInt from couchpotato.core.logger import CPLog import json import requests +import traceback log = CPLog(__name__) @@ -12,8 +13,8 @@ class Synology(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] log = CPLog(__name__) - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} response = False @@ -34,14 +35,14 @@ class Synology(Downloader): elif data['protocol'] in ['nzb', 'torrent']: log.info('Adding %s' % data['protocol']) if not filedata: - log.error('No %s data found' % data['protocol']) + log.error('No %s data found', data['protocol']) else: filename = data['name'] + '.' + data['protocol'] response = srpc.create_task(filename = filename, filedata = filedata) - except Exception, err: - log.error('Exception while adding torrent: %s', err) + except: + log.error('Exception while adding torrent: %s', traceback.format_exc()) finally: - return response + return self.downloadReturnId('') if response else False def getEnabledProtocol(self): if self.conf('use_for') == 'both': diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 2eabb2e8..46b5f3b4 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -31,7 +31,9 @@ class Transmission(Downloader): return self.trpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol'))) @@ -81,7 +83,7 @@ class Transmission(Downloader): log.info('Torrent sent to Transmission successfully.') return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking Transmission download status.') @@ -100,31 +102,32 @@ class Transmission(Downloader): return False for torrent in queue['torrents']: - log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s', - (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished'])) - - torrent_files = [] - for file_item in torrent['files']: - torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name']))) - - status = 'busy' - if torrent.get('isStalled') and self.conf('stalled_as_failed'): - status = 'failed' - elif torrent['status'] == 0 and torrent['percentDone'] == 1: - status = 'completed' - elif torrent['status'] in [5, 6]: - status = 'seeding' - - release_downloads.append({ - 'id': torrent['hashString'], - 'name': torrent['name'], - 'status': status, - 'original_status': torrent['status'], - 'seed_ratio': torrent['uploadRatio'], - 'timeleft': str(timedelta(seconds = torrent['eta'])), - 'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])), - 'files': '|'.join(torrent_files) - }) + if torrent['hashString'] in ids: + log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s', + (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished'])) + + torrent_files = [] + for file_item in torrent['files']: + torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name']))) + + status = 'busy' + if torrent.get('isStalled') and self.conf('stalled_as_failed'): + status = 'failed' + elif torrent['status'] == 0 and torrent['percentDone'] == 1: + status = 'completed' + elif torrent['status'] in [5, 6]: + status = 'seeding' + + release_downloads.append({ + 'id': torrent['hashString'], + 'name': torrent['name'], + 'status': status, + 'original_status': torrent['status'], + 'seed_ratio': torrent['uploadRatio'], + 'timeleft': str(timedelta(seconds = torrent['eta'])), + 'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])), + 'files': '|'.join(torrent_files) + }) return release_downloads diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index 1db1b8a3..056cd15d 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -36,8 +36,8 @@ class uTorrent(Downloader): return self.utorrent_api - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol'))) @@ -77,7 +77,8 @@ class uTorrent(Downloader): else: info = bdecode(filedata)["info"] torrent_hash = sha1(benc(info)).hexdigest().upper() - torrent_filename = self.createFileName(data, filedata, movie) + + torrent_filename = self.createFileName(data, filedata, media) if data.get('seed_ratio'): torrent_params['seed_override'] = 1 @@ -104,7 +105,7 @@ class uTorrent(Downloader): return self.downloadReturnId(torrent_hash) - def getAllDownloadStatus(self): + def getAllDownloadStatus(self, ids): log.debug('Checking uTorrent download status.') @@ -129,47 +130,48 @@ class uTorrent(Downloader): # Get torrents for torrent in queue['torrents']: + if torrent[0] in ids: - #Get files of the torrent - torrent_files = [] - try: - torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) - torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] - except: - log.debug('Failed getting files from torrent: %s', torrent[2]) - - status_flags = { - "STARTED" : 1, - "CHECKING" : 2, - "CHECK-START" : 4, - "CHECKED" : 8, - "ERROR" : 16, - "PAUSED" : 32, - "QUEUED" : 64, - "LOADED" : 128 - } - - status = 'busy' - if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000: - status = 'seeding' - elif (torrent[1] & status_flags["ERROR"]): - status = 'failed' - elif torrent[4] == 1000: - status = 'completed' - - if not status == 'busy': - self.removeReadOnly(torrent_files) - - release_downloads.append({ - 'id': torrent[0], - 'name': torrent[2], - 'status': status, - 'seed_ratio': float(torrent[7]) / 1000, - 'original_status': torrent[1], - 'timeleft': str(timedelta(seconds = torrent[10])), - 'folder': sp(torrent[26]), - 'files': '|'.join(torrent_files) - }) + #Get files of the torrent + torrent_files = [] + try: + torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) + torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] + except: + log.debug('Failed getting files from torrent: %s', torrent[2]) + + status_flags = { + "STARTED" : 1, + "CHECKING" : 2, + "CHECK-START" : 4, + "CHECKED" : 8, + "ERROR" : 16, + "PAUSED" : 32, + "QUEUED" : 64, + "LOADED" : 128 + } + + status = 'busy' + if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000: + status = 'seeding' + elif (torrent[1] & status_flags["ERROR"]): + status = 'failed' + elif torrent[4] == 1000: + status = 'completed' + + if not status == 'busy': + self.removeReadOnly(torrent_files) + + release_downloads.append({ + 'id': torrent[0], + 'name': torrent[2], + 'status': status, + 'seed_ratio': float(torrent[7]) / 1000, + 'original_status': torrent[1], + 'timeleft': str(timedelta(seconds = torrent[10])), + 'folder': sp(torrent[26]), + 'files': '|'.join(torrent_files) + }) return release_downloads diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py index bfebcc1f..bcc698d7 100644 --- a/couchpotato/core/helpers/encoding.py +++ b/couchpotato/core/helpers/encoding.py @@ -49,8 +49,21 @@ def ss(original, *args): return u_original.encode('UTF-8') def sp(path, *args): + # Standardise encoding, normalise case, path and strip trailing '/' or '\' - return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep) + if not path or len(path) == 0: + return path + + # convert windows path (from remote box) to *nix path + if os.path.sep == '/' and '\\' in path: + path = '/' + path.replace(':', '').replace('\\', '/') + + path = os.path.normcase(os.path.normpath(ss(path, *args))) + + if path != os.path.sep: + path = path.rstrip(os.path.sep) + + return path def ek(original, *args): if isinstance(original, (str, unicode)): diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 7d35b997..1cf1f484 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -11,6 +11,9 @@ import sys log = CPLog(__name__) +def fnEscape(pattern): + return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]') + def link(src, dst): if os.name == 'nt': import ctypes diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index e6a249d5..1ba83863 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -38,7 +38,7 @@ class MediaBase(Plugin): def notifyFront(): db = get_session() media = db.query(Media).filter_by(id = media_id).first() - fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict)) + fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict)) db.expire_all() return notifyFront diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 87afb82a..86d5a4bc 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,10 +1,15 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.variable import splitString +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from couchpotato.core.settings.model import Media +from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ + Media +from sqlalchemy.orm import joinedload_all +from sqlalchemy.sql.expression import or_, asc, not_, desc +from string import ascii_lowercase log = CPLog(__name__) @@ -20,7 +25,49 @@ class MediaPlugin(MediaBase): } }) - addEvent('app.load', self.addSingleRefresh) + addApiView('media.list', self.listView, docs = { + 'desc': 'List media', + 'params': { + 'type': {'type': 'string', 'desc': 'Media type to filter on.'}, + 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, + 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, + 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, + 'search': {'desc': 'Search movie title'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'media': array, media found, +}"""} + }) + + addApiView('media.get', self.getView, docs = { + 'desc': 'Get media by id', + 'params': { + 'id': {'desc': 'The id of the media'}, + } + }) + + addApiView('media.delete', self.deleteView, docs = { + 'desc': 'Delete a media from the wanted list', + 'params': { + 'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'}, + 'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'}, + } + }) + + addApiView('media.available_chars', self.charView) + + addEvent('app.load', self.addSingleRefreshView) + addEvent('app.load', self.addSingleListView) + addEvent('app.load', self.addSingleCharView) + addEvent('app.load', self.addSingleDeleteView) + + addEvent('media.get', self.get) + addEvent('media.list', self.list) + addEvent('media.delete', self.delete) + addEvent('media.restatus', self.restatus) def refresh(self, id = '', **kwargs): db = get_session() @@ -34,7 +81,7 @@ class MediaPlugin(MediaBase): for title in media.library.titles: if title.default: default_title = title.title - fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True) + fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x}) fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x)) db.expire_all() @@ -43,7 +90,369 @@ class MediaPlugin(MediaBase): 'success': True, } - def addSingleRefresh(self): + def addSingleRefreshView(self): for media_type in fireEvent('media.types', merge = True): addApiView('%s.refresh' % media_type, self.refresh) + + def get(self, media_id): + + db = get_session() + + imdb_id = getImdb(str(media_id)) + + if imdb_id: + m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first() + else: + m = db.query(Media).filter_by(id = media_id).first() + + results = None + if m: + results = m.to_dict(self.default_dict) + + db.expire_all() + return results + + def getView(self, id = None, **kwargs): + + media = self.get(id) if id else None + + return { + 'success': media is not None, + 'media': media, + } + + def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): + + db = get_session() + + # Make a list from string + if status and not isinstance(status, (list, tuple)): + status = [status] + if release_status and not isinstance(release_status, (list, tuple)): + release_status = [release_status] + if types and not isinstance(types, (list, tuple)): + types = [types] + + # query movie ids + q = db.query(Media) \ + .with_entities(Media.id) \ + .group_by(Media.id) + + # Filter on movie status + if status and len(status) > 0: + statuses = fireEvent('status.get', status, single = len(status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Media.status_id.in_(statuses)) + + # Filter on release status + if release_status and len(release_status) > 0: + q = q.join(Media.releases) + + statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Release.status_id.in_(statuses)) + + # Filter on type + if types and len(types) > 0: + try: q = q.filter(Media.type.in_(types)) + except: pass + + # Only join when searching / ordering + if starts_with or search or order != 'release_order': + q = q.join(Media.library, Library.titles) \ + .filter(LibraryTitle.default == True) + + # Add search filters + filter_or = [] + if starts_with: + starts_with = toUnicode(starts_with.lower()) + if starts_with in ascii_lowercase: + filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) + else: + ignore = [] + for letter in ascii_lowercase: + ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) + filter_or.append(not_(or_(*ignore))) + + if search: + filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) + + if len(filter_or) > 0: + q = q.filter(or_(*filter_or)) + + total_count = q.count() + if total_count == 0: + return 0, [] + + if order == 'release_order': + q = q.order_by(desc(Release.last_edit)) + else: + q = q.order_by(asc(LibraryTitle.simple_title)) + + if limit_offset: + splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset + limit = splt[0] + offset = 0 if len(splt) is 1 else splt[1] + q = q.limit(limit).offset(offset) + + # Get all media_ids in sorted order + media_ids = [m.id for m in q.all()] + + # List release statuses + releases = db.query(Release) \ + .filter(Release.movie_id.in_(media_ids)) \ + .all() + + release_statuses = dict((m, set()) for m in media_ids) + releases_count = dict((m, 0) for m in media_ids) + for release in releases: + release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) + releases_count[release.movie_id] += 1 + + # Get main movie data + q2 = db.query(Media) \ + .options(joinedload_all('library.titles')) \ + .options(joinedload_all('library.files')) \ + .options(joinedload_all('status')) \ + .options(joinedload_all('files')) + + q2 = q2.filter(Media.id.in_(media_ids)) + + results = q2.all() + + # Create dict by movie id + movie_dict = {} + for movie in results: + movie_dict[movie.id] = movie + + # List movies based on media_ids order + movies = [] + for media_id in media_ids: + + releases = [] + for r in release_statuses.get(media_id): + x = splitString(r) + releases.append({'status_id': x[0], 'quality_id': x[1]}) + + # Merge releases with movie dict + movies.append(mergeDicts(movie_dict[media_id].to_dict({ + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + }), { + 'releases': releases, + 'releases_count': releases_count.get(media_id), + })) + + db.expire_all() + return total_count, movies + + def listView(self, **kwargs): + + types = splitString(kwargs.get('types')) + status = splitString(kwargs.get('status')) + release_status = splitString(kwargs.get('release_status')) + limit_offset = kwargs.get('limit_offset') + starts_with = kwargs.get('starts_with') + search = kwargs.get('search') + order = kwargs.get('order') + + total_movies, movies = self.list( + types = types, + status = status, + release_status = release_status, + limit_offset = limit_offset, + starts_with = starts_with, + search = search, + order = order + ) + + return { + 'success': True, + 'empty': len(movies) == 0, + 'total': total_movies, + 'movies': movies, + } + + def addSingleListView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempList(*args, **kwargs): + return self.listView(types = media_type, *args, **kwargs) + addApiView('%s.list' % media_type, tempList) + + def availableChars(self, types = None, status = None, release_status = None): + + types = types or [] + status = status or [] + release_status = release_status or [] + + db = get_session() + + # Make a list from string + if not isinstance(status, (list, tuple)): + status = [status] + if release_status and not isinstance(release_status, (list, tuple)): + release_status = [release_status] + if types and not isinstance(types, (list, tuple)): + types = [types] + + q = db.query(Media) + + # Filter on movie status + if status and len(status) > 0: + statuses = fireEvent('status.get', status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Media.status_id.in_(statuses)) + + # Filter on release status + if release_status and len(release_status) > 0: + + statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.join(Media.releases) \ + .filter(Release.status_id.in_(statuses)) + + # Filter on type + if types and len(types) > 0: + try: q = q.filter(Media.type.in_(types)) + except: pass + + q = q.join(Library, LibraryTitle) \ + .with_entities(LibraryTitle.simple_title) \ + .filter(LibraryTitle.default == True) + + titles = q.all() + + chars = set() + for title in titles: + try: + char = title[0][0] + char = char if char in ascii_lowercase else '#' + chars.add(str(char)) + except: + log.error('Failed getting title for %s', title.libraries_id) + + if len(chars) == 25: + break + + db.expire_all() + return ''.join(sorted(chars)) + + def charView(self, **kwargs): + + type = splitString(kwargs.get('type', 'movie')) + status = splitString(kwargs.get('status', None)) + release_status = splitString(kwargs.get('release_status', None)) + chars = self.availableChars(type, status, release_status) + + return { + 'success': True, + 'empty': len(chars) == 0, + 'chars': chars, + } + + def addSingleCharView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempChar(*args, **kwargs): + return self.charView(types = media_type, *args, **kwargs) + addApiView('%s.available_chars' % media_type, tempChar) + + def delete(self, media_id, delete_from = None): + + db = get_session() + + media = db.query(Media).filter_by(id = media_id).first() + if media: + deleted = False + if delete_from == 'all': + db.delete(media) + db.commit() + deleted = True + else: + done_status = fireEvent('status.get', 'done', single = True) + + total_releases = len(media.releases) + total_deleted = 0 + new_movie_status = None + for release in media.releases: + if delete_from in ['wanted', 'snatched', 'late']: + if release.status_id != done_status.get('id'): + db.delete(release) + total_deleted += 1 + new_movie_status = 'done' + elif delete_from == 'manage': + if release.status_id == done_status.get('id'): + db.delete(release) + total_deleted += 1 + new_movie_status = 'active' + db.commit() + + if total_releases == total_deleted: + db.delete(media) + db.commit() + deleted = True + elif new_movie_status: + new_status = fireEvent('status.get', new_movie_status, single = True) + media.profile_id = None + media.status_id = new_status.get('id') + db.commit() + else: + fireEvent('media.restatus', media.id, single = True) + + if deleted: + fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) + + db.expire_all() + return True + + def deleteView(self, id = '', **kwargs): + + ids = splitString(id) + for media_id in ids: + self.delete(media_id, delete_from = kwargs.get('delete_from', 'all')) + + return { + 'success': True, + } + + def addSingleDeleteView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempDelete(*args, **kwargs): + return self.deleteView(types = media_type, *args, **kwargs) + addApiView('%s.delete' % media_type, tempDelete) + + def restatus(self, media_id): + + active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) + + db = get_session() + + m = db.query(Media).filter_by(id = media_id).first() + if not m or len(m.library.titles) == 0: + log.debug('Can\'t restatus movie, doesn\'t seem to exist.') + return False + + log.debug('Changing status for %s', m.library.titles[0].title) + if not m.profile: + m.status_id = done_status.get('id') + else: + move_to_wanted = True + + for t in m.profile.types: + for release in m.releases: + if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): + move_to_wanted = False + + m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') + + db.commit() + + return True + diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index fdf09309..3c73eb27 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -147,7 +147,7 @@ class Searcher(SearcherBase): except: pass # Match longest name between [] - try: check_names.append(max(check_name.split('['), key = len)) + try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip()) except: pass for check_name in list(set(check_names)): diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 817b0a38..20663b59 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -2,15 +2,10 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \ - mergeDicts +from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase -from couchpotato.core.settings.model import Library, LibraryTitle, Media, \ - Release -from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql.expression import or_, asc, not_, desc -from string import ascii_lowercase +from couchpotato.core.settings.model import Media import time log = CPLog(__name__) @@ -26,33 +21,12 @@ class MovieBase(MovieTypeBase): super(MovieBase, self).__init__() self.initType() - addApiView('movie.list', self.listView, docs = { - 'desc': 'List movies in wanted list', - 'params': { - 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, - 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, - 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, - 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, - 'search': {'desc': 'Search movie title'}, - }, - 'return': {'type': 'object', 'example': """{ - 'success': True, - 'empty': bool, any movies returned or not, - 'movies': array, movies found, -}"""} - }) - addApiView('movie.get', self.getView, docs = { - 'desc': 'Get a movie by id', - 'params': { - 'id': {'desc': 'The id of the movie'}, - } - }) - addApiView('movie.available_chars', self.charView) addApiView('movie.add', self.addView, docs = { 'desc': 'Add new movie to the wanted list', 'params': { 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'}, 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, } }) @@ -64,255 +38,8 @@ class MovieBase(MovieTypeBase): 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, } }) - addApiView('movie.delete', self.deleteView, docs = { - 'desc': 'Delete a movie from the wanted list', - 'params': { - 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, - 'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'}, - } - }) addEvent('movie.add', self.add) - addEvent('movie.delete', self.delete) - addEvent('movie.get', self.get) - addEvent('movie.list', self.list) - addEvent('movie.restatus', self.restatus) - - def getView(self, id = None, **kwargs): - - movie = self.get(id) if id else None - - return { - 'success': movie is not None, - 'movie': movie, - } - - def get(self, movie_id): - - db = get_session() - - imdb_id = getImdb(str(movie_id)) - - if imdb_id: - m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first() - else: - m = db.query(Media).filter_by(id = movie_id).first() - - results = None - if m: - results = m.to_dict(self.default_dict) - - db.expire_all() - return results - - def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): - - db = get_session() - - # Make a list from string - if status and not isinstance(status, (list, tuple)): - status = [status] - if release_status and not isinstance(release_status, (list, tuple)): - release_status = [release_status] - - # query movie ids - q = db.query(Media) \ - .with_entities(Media.id) \ - .group_by(Media.id) - - # Filter on movie status - if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) - - # Filter on release status - if release_status and len(release_status) > 0: - q = q.join(Media.releases) - - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Release.status_id.in_(statuses)) - - # Only join when searching / ordering - if starts_with or search or order != 'release_order': - q = q.join(Media.library, Library.titles) \ - .filter(LibraryTitle.default == True) - - # Add search filters - filter_or = [] - if starts_with: - starts_with = toUnicode(starts_with.lower()) - if starts_with in ascii_lowercase: - filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) - else: - ignore = [] - for letter in ascii_lowercase: - ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) - filter_or.append(not_(or_(*ignore))) - - if search: - filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) - - if len(filter_or) > 0: - q = q.filter(or_(*filter_or)) - - total_count = q.count() - if total_count == 0: - return 0, [] - - if order == 'release_order': - q = q.order_by(desc(Release.last_edit)) - else: - q = q.order_by(asc(LibraryTitle.simple_title)) - - if limit_offset: - splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset - limit = splt[0] - offset = 0 if len(splt) is 1 else splt[1] - q = q.limit(limit).offset(offset) - - # Get all movie_ids in sorted order - movie_ids = [m.id for m in q.all()] - - # List release statuses - releases = db.query(Release) \ - .filter(Release.movie_id.in_(movie_ids)) \ - .all() - - release_statuses = dict((m, set()) for m in movie_ids) - releases_count = dict((m, 0) for m in movie_ids) - for release in releases: - release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) - releases_count[release.movie_id] += 1 - - # Get main movie data - q2 = db.query(Media) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) - - q2 = q2.filter(Media.id.in_(movie_ids)) - - results = q2.all() - - # Create dict by movie id - movie_dict = {} - for movie in results: - movie_dict[movie.id] = movie - - # List movies based on movie_ids order - movies = [] - for movie_id in movie_ids: - - releases = [] - for r in release_statuses.get(movie_id): - x = splitString(r) - releases.append({'status_id': x[0], 'quality_id': x[1]}) - - # Merge releases with movie dict - movies.append(mergeDicts(movie_dict[movie_id].to_dict({ - 'library': {'titles': {}, 'files':{}}, - 'files': {}, - }), { - 'releases': releases, - 'releases_count': releases_count.get(movie_id), - })) - - db.expire_all() - return total_count, movies - - def availableChars(self, status = None, release_status = None): - - status = status or [] - release_status = release_status or [] - - db = get_session() - - # Make a list from string - if not isinstance(status, (list, tuple)): - status = [status] - if release_status and not isinstance(release_status, (list, tuple)): - release_status = [release_status] - - q = db.query(Media) - - # Filter on movie status - if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) - - # Filter on release status - if release_status and len(release_status) > 0: - - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.join(Media.releases) \ - .filter(Release.status_id.in_(statuses)) - - q = q.join(Library, LibraryTitle) \ - .with_entities(LibraryTitle.simple_title) \ - .filter(LibraryTitle.default == True) - - titles = q.all() - - chars = set() - for title in titles: - try: - char = title[0][0] - char = char if char in ascii_lowercase else '#' - chars.add(str(char)) - except: - log.error('Failed getting title for %s', title.libraries_id) - - if len(chars) == 25: - break - - db.expire_all() - return ''.join(sorted(chars)) - - def listView(self, **kwargs): - - status = splitString(kwargs.get('status')) - release_status = splitString(kwargs.get('release_status')) - limit_offset = kwargs.get('limit_offset') - starts_with = kwargs.get('starts_with') - search = kwargs.get('search') - order = kwargs.get('order') - - total_movies, movies = self.list( - status = status, - release_status = release_status, - limit_offset = limit_offset, - starts_with = starts_with, - search = search, - order = order - ) - - return { - 'success': True, - 'empty': len(movies) == 0, - 'total': total_movies, - 'movies': movies, - } - - def charView(self, **kwargs): - - status = splitString(kwargs.get('status', None)) - release_status = splitString(kwargs.get('release_status', None)) - chars = self.availableChars(status, release_status) - - return { - 'success': True, - 'empty': len(chars) == 0, - 'chars': chars, - } def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): if not params: params = {} @@ -421,9 +148,9 @@ class MovieBase(MovieTypeBase): available_status = fireEvent('status.get', 'available', single = True) ids = splitString(id) - for movie_id in ids: + for media_id in ids: - m = db.query(Media).filter_by(id = movie_id).first() + m = db.query(Media).filter_by(id = media_id).first() if not m: continue @@ -446,98 +173,12 @@ class MovieBase(MovieTypeBase): db.commit() - fireEvent('movie.restatus', m.id) + fireEvent('media.restatus', m.id) movie_dict = m.to_dict(self.default_dict) - fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) + fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) db.expire_all() return { 'success': True, } - - def deleteView(self, id = '', **kwargs): - - ids = splitString(id) - for movie_id in ids: - self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all')) - - return { - 'success': True, - } - - def delete(self, movie_id, delete_from = None): - - db = get_session() - - movie = db.query(Media).filter_by(id = movie_id).first() - if movie: - deleted = False - if delete_from == 'all': - db.delete(movie) - db.commit() - deleted = True - else: - done_status = fireEvent('status.get', 'done', single = True) - - total_releases = len(movie.releases) - total_deleted = 0 - new_movie_status = None - for release in movie.releases: - if delete_from in ['wanted', 'snatched', 'late']: - if release.status_id != done_status.get('id'): - db.delete(release) - total_deleted += 1 - new_movie_status = 'done' - elif delete_from == 'manage': - if release.status_id == done_status.get('id'): - db.delete(release) - total_deleted += 1 - new_movie_status = 'active' - db.commit() - - if total_releases == total_deleted: - db.delete(movie) - db.commit() - deleted = True - elif new_movie_status: - new_status = fireEvent('status.get', new_movie_status, single = True) - movie.profile_id = None - movie.status_id = new_status.get('id') - db.commit() - else: - fireEvent('movie.restatus', movie.id, single = True) - - if deleted: - fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) - - db.expire_all() - return True - - def restatus(self, movie_id): - - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) - - db = get_session() - - m = db.query(Media).filter_by(id = movie_id).first() - if not m or len(m.library.titles) == 0: - log.debug('Can\'t restatus movie, doesn\'t seem to exist.') - return False - - log.debug('Changing status for %s', m.library.titles[0].title) - if not m.profile: - m.status_id = done_status.get('id') - else: - move_to_wanted = True - - for t in m.profile.types: - for release in m.releases: - if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): - move_to_wanted = False - - m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') - - db.commit() - - return True diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index aaa8be12..85dee2e5 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -52,8 +52,8 @@ var MovieList = new Class({ self.getMovies(); - App.addEvent('movie.added', self.movieAdded.bind(self)) - App.addEvent('movie.deleted', self.movieDeleted.bind(self)) + App.on('movie.added', self.movieAdded.bind(self)) + App.on('movie.deleted', self.movieDeleted.bind(self)) }, movieDeleted: function(notification){ @@ -65,6 +65,7 @@ var MovieList = new Class({ movie.destroy(); delete self.movies_added[notification.data.id]; self.setCounter(self.counter_count-1); + self.total_movies--; } }) } @@ -75,6 +76,7 @@ var MovieList = new Class({ movieAdded: function(notification){ var self = this; + self.fireEvent('movieAdded', notification); if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ window.scroll(0,0); self.createMovie(notification.data, 'top'); @@ -279,7 +281,7 @@ var MovieList = new Class({ // Get available chars and highlight if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) - Api.request('movie.available_chars', { + Api.request('media.available_chars', { 'data': Object.merge({ 'status': self.options.status }, self.filter), @@ -370,7 +372,7 @@ var MovieList = new Class({ 'click': function(e){ (e).preventDefault(); this.set('text', 'Deleting..') - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': ids.join(','), 'delete_from': self.options.identifier @@ -390,6 +392,7 @@ var MovieList = new Class({ self.movies.erase(movie); movie.destroy(); self.setCounter(self.counter_count-1); + self.total_movies--; }); self.calculateSelected(); @@ -547,8 +550,9 @@ var MovieList = new Class({ } - Api.request(self.options.api_call || 'movie.list', { + Api.request(self.options.api_call || 'media.list', { 'data': Object.merge({ + 'type': 'movie', 'status': self.options.status, 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null }, self.filter), diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index f6e0f542..66c84c68 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -126,7 +126,9 @@ MA.Release = new Class({ else self.showHelper(); - App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){ + App.on('movie.searcher.ended', function(notification){ + if(self.movie.data.id != notification.data.id) return; + self.releases = null; if(self.options_container){ self.options_container.destroy(); @@ -250,12 +252,14 @@ MA.Release = new Class({ else if(!self.next_release && status.identifier == 'available'){ self.next_release = release; } - + var update_handle = function(notification) { - var q = self.movie.quality.getElement('.q_id' + release.quality_id), + if(notification.data.id != release.id) return; + + var q = self.movie.quality.getElement('.q_id' + release.quality_id), status = Status.get(release.status_id), - new_status = Status.get(notification.data); - + new_status = Status.get(notification.data.status_id); + release.status_id = new_status.id release.el.set('class', 'item ' + new_status.identifier); @@ -272,7 +276,7 @@ MA.Release = new Class({ } } - App.addEvent('release.update_status.' + release.id, update_handle); + App.on('release.update_status', update_handle); }); @@ -285,7 +289,7 @@ MA.Release = new Class({ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); - + var nr = self.next_release, lr = self.last_release; @@ -427,7 +431,7 @@ MA.Release = new Class({ markMovieDone: function(){ var self = this; - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': self.movie.get('id'), 'delete_from': 'wanted' @@ -446,7 +450,7 @@ MA.Release = new Class({ }, - tryNextRelease: function(movie_id){ + tryNextRelease: function(){ var self = this; Api.request('movie.searcher.try_next', { @@ -817,7 +821,7 @@ MA.Delete = new Class({ self.callChain(); }, function(){ - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': self.movie.get('id'), 'delete_from': self.movie.list.options.identifier diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index c013bd80..a88a2077 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -1036,7 +1036,7 @@ text-overflow: ellipsis; overflow: hidden; width: 85%; - direction: rtl; + direction: ltr; vertical-align: middle; } diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index a865325b..9c0ae015 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -23,23 +23,49 @@ var Movie = new Class({ addEvents: function(){ var self = this; - App.addEvent('movie.update.'+self.data.id, function(notification){ + self.global_events = {} + + // Do refresh with new data + self.global_events['movie.update'] = function(notification){ + if(self.data.id != notification.data.id) return; + self.busy(false); self.removeView(); self.update.delay(2000, self, notification); - }); + } + App.on('movie.update', self.global_events['movie.update']); + // Add spinner on load / search ['movie.busy', 'movie.searcher.started'].each(function(listener){ - App.addEvent(listener+'.'+self.data.id, function(notification){ - if(notification.data) + self.global_events[listener] = function(notification){ + if(notification.data && self.data.id == notification.data.id) self.busy(true) - }); + } + App.on(listener, self.global_events[listener]); }) - App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){ - if(notification.data) + // Remove spinner + self.global_events['movie.searcher.ended'] = function(notification){ + if(notification.data && self.data.id == notification.data.id) self.busy(false) - }); + } + App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); + + // Reload when releases have updated + self.global_events['release.update_status'] = function(notification){ + var data = notification.data + if(data && self.data.id == data.movie_id){ + + if(!self.data.releases) + self.data.releases = []; + + self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id}); + self.updateReleases(); + } + } + + App.on('release.update_status', self.global_events['release.update_status']); + }, destroy: function(){ @@ -52,10 +78,9 @@ var Movie = new Class({ self.list.checkIfEmpty(); // Remove events - App.removeEvents('movie.update.'+self.data.id); - ['movie.busy', 'movie.searcher.started'].each(function(listener){ - App.removeEvents(listener+'.'+self.data.id); - }) + Object.each(self.global_events, function(handle, listener){ + App.off(listener, handle); + }); }, busy: function(set_busy, timeout){ @@ -179,21 +204,7 @@ var Movie = new Class({ }); // Add releases - if(self.data.releases) - self.data.releases.each(function(release){ - - var q = self.quality.getElement('.q_id'+ release.quality_id), - status = Status.get(release.status_id); - - if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done')) - var q = self.addQuality(release.quality_id) - - if (status && q && !q.hasClass(status.identifier)){ - q.addClass(status.identifier); - q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) - } - - }); + self.updateReleases(); Object.each(self.options.actions, function(action, key){ self.action[key.toLowerCase()] = action = new self.options.actions[key](self) @@ -203,6 +214,26 @@ var Movie = new Class({ }, + updateReleases: function(){ + var self = this; + if(!self.data.releases || self.data.releases.length == 0) return; + + self.data.releases.each(function(release){ + + var q = self.quality.getElement('.q_id'+ release.quality_id), + status = Status.get(release.status_id); + + if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done')) + var q = self.addQuality(release.quality_id) + + if (status && q && !q.hasClass(status.identifier)){ + q.addClass(status.identifier); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) + } + + }); + }, + addQuality: function(quality_id){ var self = this; diff --git a/couchpotato/core/media/movie/library/movie/main.py b/couchpotato/core/media/movie/library/movie/main.py index 718e7390..b0d05202 100644 --- a/couchpotato/core/media/movie/library/movie/main.py +++ b/couchpotato/core/media/movie/library/movie/main.py @@ -151,7 +151,7 @@ class MovieLibraryPlugin(LibraryBase): else: dates = library.info.get('release_date') - if dates and dates.get('expires', 0) < time.time() or not dates: + if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: dates = fireEvent('movie.release_date', identifier = identifier, merge = True) library.info.update({'release_date': dates }) db.commit() diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 93441c59..d90f9598 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -145,10 +145,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): default_title = getTitle(movie['library']) if not default_title: log.error('No proper info found for movie, removing it from library to cause it from having more issues.') - fireEvent('movie.delete', movie['id'], single = True) + fireEvent('media.delete', movie['id'], single = True) return - fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) + fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title) ret = False @@ -192,7 +192,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): else: log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) - fireEvent('movie.restatus', movie['id']) + fireEvent('media.restatus', movie['id']) break # Break if CP wants to shut down @@ -202,7 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if len(too_early_to_search) > 0: log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True) + fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']}) return ret @@ -318,14 +318,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase): 'success': trynext } - def tryNextRelease(self, movie_id, manual = False): + def tryNextRelease(self, media_id, manual = False): snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True) try: db = get_session() rels = db.query(Release) \ - .filter_by(movie_id = movie_id) \ + .filter_by(movie_id = media_id) \ .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \ .all() @@ -333,7 +333,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): rel.status_id = ignored_status.get('id') db.commit() - movie_dict = fireEvent('movie.get', movie_id, single = True) + movie_dict = fireEvent('media.get', media_id = media_id, single = True) log.info('Trying next release for: %s', getTitle(movie_dict['library'])) fireEvent('movie.searcher.single', movie_dict, manual = manual) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 4c0d0992..63d2075e 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -17,7 +17,7 @@ class Notification(Provider): listen_to = [ 'renamer.after', 'movie.snatched', 'updater.available', 'updater.updated', - 'core.message', + 'core.message.important', ] dont_listen_to = [] diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 04acf284..cd63c2cb 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -21,6 +21,12 @@ class CoreNotifier(Notification): m_lock = None + listen_to = [ + 'renamer.after', 'movie.snatched', + 'updater.available', 'updater.updated', + 'core.message', 'core.message.important', + ] + def __init__(self): super(CoreNotifier, self).__init__() @@ -121,7 +127,10 @@ class CoreNotifier(Notification): for message in messages: if message.get('time') > last_check: - fireEvent('core.message', message = message.get('message'), data = message) + message['sticky'] = True # Always sticky core messages + + message_type = 'core.message.important' if message.get('important') else 'core.message' + fireEvent(message_type, message = message.get('message'), data = message) if last_check < message.get('time'): last_check = message.get('time') diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index e485976e..a0c3b15c 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -10,8 +10,8 @@ var NotificationBase = new Class({ // Listener App.addEvent('unload', self.stopPoll.bind(self)); App.addEvent('reload', self.startInterval.bind(self, [true])); - App.addEvent('notification', self.notify.bind(self)); - App.addEvent('message', self.showMessage.bind(self)); + App.on('notification', self.notify.bind(self)); + App.on('message', self.showMessage.bind(self)); // Add test buttons to settings page App.addEvent('load', self.addTestButtons.bind(self)); @@ -50,9 +50,9 @@ var NotificationBase = new Class({ , 'top'); self.notifications.include(result); - if(result.data.important !== undefined && !result.read){ + if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){ var sticky = true - App.fireEvent('message', [result.message, sticky, result]) + App.trigger('message', [result.message, sticky, result]) } else if(!result.read){ self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) @@ -147,7 +147,7 @@ var NotificationBase = new Class({ // Process data if(json){ Array.each(json.result, function(result){ - App.fireEvent(result.type, result); + App.trigger(result.type, result); if(result.message && result.read === undefined) self.showMessage(result.message); }) diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py index c67ac97d..41a4323b 100644 --- a/couchpotato/core/notifications/email/main.py +++ b/couchpotato/core/notifications/email/main.py @@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from couchpotato.environment import Env from email.mime.text import MIMEText +from email.utils import formatdate, make_msgid import smtplib import traceback @@ -30,6 +31,8 @@ class Email(Notification): message['Subject'] = self.default_title message['From'] = from_address message['To'] = to_address + message['Date'] = formatdate(localtime = 1) + message['Message-ID'] = make_msgid() try: # Open the SMTP connection, via SSL if requested diff --git a/couchpotato/core/notifications/pushbullet/__init__.py b/couchpotato/core/notifications/pushbullet/__init__.py new file mode 100644 index 00000000..e61a44e3 --- /dev/null +++ b/couchpotato/core/notifications/pushbullet/__init__.py @@ -0,0 +1,39 @@ +from .main import Pushbullet + +def start(): + return Pushbullet() + +config = [{ + 'name': 'pushbullet', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'pushbullet', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'label': 'User API Key' + }, + { + 'name': 'devices', + 'default': '', + 'advanced': True, + 'description': 'IDs of devices to send notifications to, empty = all devices' + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/pushbullet/main.py b/couchpotato/core/notifications/pushbullet/main.py new file mode 100644 index 00000000..2e6db29d --- /dev/null +++ b/couchpotato/core/notifications/pushbullet/main.py @@ -0,0 +1,86 @@ +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +import base64 +import json + +log = CPLog(__name__) + + +class Pushbullet(Notification): + + url = 'https://api.pushbullet.com/api/%s' + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + devices = self.getDevices() + if devices is None: + return False + + # Get all the device IDs linked to this user + if not len(devices): + response = self.request('devices') + if not response: + return False + + devices += [device.get('id') for device in response['devices']] + + successful = 0 + for device in devices: + response = self.request( + 'pushes', + cache = False, + device_id = device, + type = 'note', + title = self.default_title, + body = toUnicode(message) + ) + + if response: + successful += 1 + else: + log.error('Unable to push notification to Pushbullet device with ID %s' % device) + + return successful == len(devices) + + def getDevices(self): + devices = [d.strip() for d in self.conf('devices').split(',')] + + # Remove empty items + devices = [d for d in devices if len(d)] + + # Break on any ids that aren't integers + valid_devices = [] + + for device_id in devices: + d = tryInt(device_id, None) + + if not d: + log.error('Device ID "%s" is not valid', device_id) + return None + + valid_devices.append(d) + + return valid_devices + + def request(self, method, cache = True, **kwargs): + try: + base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1] + + headers = { + "Authorization": "Basic %s" % base64string + } + + if cache: + return self.getJsonData(self.url % method, headers = headers, params = kwargs) + else: + data = self.urlopen(self.url % method, headers = headers, params = kwargs) + return json.loads(data) + + except Exception, ex: + log.error('Pushbullet request failed') + log.debug(ex) + + return None diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index dafa0f63..04662e27 100644 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ b/couchpotato/core/notifications/xbmc/__init__.py @@ -46,6 +46,14 @@ config = [{ 'advanced': True, 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', }, + { + 'name': 'force_full_scan', + 'label': 'Always do a full scan', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.', + }, { 'name': 'on_snatch', 'default': 0, diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index dc185c41..9dac4979 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -36,7 +36,7 @@ class XBMC(Notification): if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): param = {} - if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]): + if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])): param = {'directory': data['destination_dir']} calls.append(('VideoLibrary.Scan', param)) diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index 440232b2..a81719c4 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -41,7 +41,7 @@ config = [{ 'label': 'Required Genres', 'default': '', 'placeholder': 'Example: Action, Crime & Drama', - 'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"' + 'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"') }, { 'name': 'ignored_genres', diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 92547cb0..2edcd3be 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -43,7 +43,7 @@ class Automation(Plugin): if self.shuttingDown(): break - movie_dict = fireEvent('movie.get', movie_id, single = True) + movie_dict = fireEvent('media.get', movie_id, single = True) fireEvent('movie.searcher.single', movie_dict) - return True \ No newline at end of file + return True diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 649e359d..2bc8fb9e 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -289,19 +289,19 @@ class Plugin(object): Env.get('cache').set(cache_key_md5, value, timeout) return value - def createNzbName(self, data, movie): - tag = self.cpTag(movie) + def createNzbName(self, data, media): + tag = self.cpTag(media) return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag) - def createFileName(self, data, filedata, movie): - name = sp(os.path.join(self.createNzbName(data, movie))) + def createFileName(self, data, filedata, media): + name = sp(os.path.join(self.createNzbName(data, media))) if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata: return '%s.%s' % (name, 'rar') return '%s.%s' % (name, data.get('protocol')) - def cpTag(self, movie): + def cpTag(self, media): if Env.setting('enabled', 'renamer'): - return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else '' + return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else '' return '' diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index e8ccaf7e..a764f317 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -1,6 +1,6 @@ from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync -from couchpotato.core.helpers.encoding import ss +from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -79,6 +79,7 @@ class Manage(Plugin): try: directories = self.directories() + directories.sort() added_identifiers = [] # Add some progress @@ -111,22 +112,20 @@ class Manage(Plugin): if self.conf('cleanup') and full and not self.shuttingDown(): # Get movies with done status - total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True) + total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) for done_movie in done_movies: if done_movie['library']['identifier'] not in added_identifiers: - fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all') + fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') else: releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True) for release in releases: - if len(release.get('files', [])) == 0: - fireEvent('release.delete', release['id']) - else: + if len(release.get('files', [])) > 0: for release_file in release.get('files', []): # Remove release not available anymore - if not os.path.isfile(ss(release_file['path'])): + if not os.path.isfile(sp(release_file['path'])): fireEvent('release.clean', release['id']) break @@ -201,7 +200,7 @@ class Manage(Plugin): self.in_progress[folder]['to_go'] -= 1 total = self.in_progress[folder]['total'] - movie_dict = fireEvent('movie.get', identifier, single = True) + movie_dict = fireEvent('media.get', identifier, single = True) fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library'])) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 7992965f..3aaa77ea 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -2,7 +2,7 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.variable import mergeDicts, md5, getExt +from couchpotato.core.helpers.variable import mergeDicts, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Quality, Profile, ProfileType @@ -19,14 +19,14 @@ class QualityPlugin(Plugin): {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, - {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, + {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]}, - {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, - {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]}, - {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, - {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, - {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, - {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']} + {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, + {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, + {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, + {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]}, + {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]}, + {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]} ] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] @@ -50,6 +50,8 @@ class QualityPlugin(Plugin): addEvent('app.initialize', self.fill, priority = 10) + addEvent('app.test', self.doTest) + def preReleases(self): return self.pre_releases @@ -165,9 +167,10 @@ class QualityPlugin(Plugin): if not extra: extra = {} # Create hash for cache - cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files])) + cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) cached = self.getCache(cache_key) - if cached and len(extra) == 0: return cached + if cached and len(extra) == 0: + return cached qualities = self.all() @@ -228,11 +231,6 @@ class QualityPlugin(Plugin): if len(set(words) & set(alt)) == len(alt): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) score += points.get(tag_type) - elif len(set(words) & set(alt)) > 0: - partial = list(set(words) & set(alt))[0] - if len(partial) > 2: - log.debug('Found %s via partial %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) - score += points.get(tag_type) / 3 if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) @@ -285,3 +283,33 @@ class QualityPlugin(Plugin): if add_score != 0: for allow in quality.get('allow', []): score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 + + def doTest(self): + + tests = { + 'Movie Name (1999)-DVD-Rip.avi': 'dvdrip', + 'Movie Name 1999 720p Bluray.mkv': '720p', + 'Movie Name 1999 BR-Rip 720p.avi': 'brrip', + 'Movie Name 1999 720p Web Rip.avi': 'scr', + 'Movie Name 1999 Web DL.avi': 'brrip', + 'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr', + 'Movie.Name.1999.DVDRip-Group': 'dvdrip', + 'Movie.Name.1999.DVD-Rip-Group': 'dvdrip', + 'Movie.Name.1999.DVD-R-Group': 'dvdr', + } + + correct = 0 + for name in tests: + success = self.guess([name]).get('identifier') == tests[name] + if not success: + log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier'))) + + correct += success + + if correct == len(tests): + log.info('Quality test successful') + return True + else: + log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests))) + + diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 009a60e9..11779803 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -100,14 +100,14 @@ class Release(Plugin): done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True) # Add movie - movie = db.query(Media).filter_by(library_id = group['library'].get('id')).first() - if not movie: - movie = Media( + media = db.query(Media).filter_by(library_id = group['library'].get('id')).first() + if not media: + media = Media( library_id = group['library'].get('id'), profile_id = 0, status_id = done_status.get('id') ) - db.add(movie) + db.add(media) db.commit() # Add Release @@ -120,7 +120,7 @@ class Release(Plugin): if not rel: rel = Relea( identifier = identifier, - movie = movie, + movie = media, quality_id = group['meta_data']['quality'].get('id'), status_id = done_status.get('id') ) @@ -142,7 +142,7 @@ class Release(Plugin): except: log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc())) - fireEvent('movie.restatus', movie.id) + fireEvent('media.restatus', media.id) return True @@ -269,7 +269,7 @@ class Release(Plugin): if filedata == 'try_next': return filedata - download_result = fireEvent('download', data = data, movie = media, manual = manual, filedata = filedata, single = True) + download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True) log.debug('Downloader result: %s', download_result) if download_result: @@ -288,7 +288,7 @@ class Release(Plugin): value = toUnicode(download_result.get(key)) ) rls.info.append(rls_info) - db.commit() + db.commit() log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) @@ -446,6 +446,6 @@ class Release(Plugin): db.commit() #Update all movie info as there is no release update function - fireEvent('notify.frontend', type = 'release.update_status.%s' % rel.id, data = status.get('id')) + fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict()) return True diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index c8f6b37f..8b602cbd 100755 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -93,7 +93,7 @@ config = [{ 'default': 1, 'type': 'int', 'unit': 'min(s)', - 'description': 'Detect movie status every X minutes. Will start the renamer if movie is completed or handle failed download if these options are enabled', + 'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is completed or handle failed download if these options are enabled'), }, { 'advanced': True, @@ -122,13 +122,13 @@ config = [{ 'advanced': True, 'name': 'separator', 'label': 'File-Separator', - 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), }, { 'advanced': True, 'name': 'foldersep', 'label': 'Folder-Separator', - 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), }, { 'name': 'file_action', @@ -136,7 +136,7 @@ config = [{ 'default': 'link', 'type': 'dropdown', 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], - 'description': 'Link or Copy after downloading completed (and allow for seeding), or Move after seeding completed. Link first tries hard link, then sym link and falls back to Copy.', + 'description': ('Link, Copy or Move after download completed.', 'Link first tries hard link, then sym link and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'), 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index ba2cf245..b4f668ff 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -3,7 +3,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss, sp from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ - getImdb, link, symlink, tryInt, splitString + getImdb, link, symlink, tryInt, splitString, fnEscape from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, Release, \ @@ -30,10 +30,10 @@ class Renamer(Plugin): 'desc': 'For the renamer to check for new files to rename in a folder', 'params': { 'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'}, - 'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'}, - 'movie_folder': {'desc': 'Optional: The folder of a specific release to scan. Don\'t use in combination with base_folder.'}, + 'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'}, + 'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'}, 'downloader' : {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'}, - 'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in movie_folder. \'downloader\' is required with this option.'}, + 'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in media_folder. \'downloader\' is required with this option.'}, 'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''}, }, }) @@ -64,26 +64,31 @@ class Renamer(Plugin): def scanView(self, **kwargs): async = tryInt(kwargs.get('async', 0)) - movie_folder = sp(kwargs.get('movie_folder')) - base_folder = sp(kwargs.get('base_folder')) + media_folder = sp(kwargs.get('media_folder')) + + # Backwards compatibility, to be removed after a few versions :) + if not media_folder: + media_folder = sp(kwargs.get('movie_folder')) + downloader = kwargs.get('downloader') download_id = kwargs.get('download_id') + files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')]) status = kwargs.get('status', 'completed') - release_download = None - if not base_folder and movie_folder: - release_download = {'folder': movie_folder} - release_download.update({'id': download_id, 'downloader': downloader, 'status': status} if download_id else {}) + release_download = {'folder': media_folder} if media_folder else None + if release_download: + release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {}) fire_handle = fireEvent if not async else fireEventAsync - fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download) + fire_handle('renamer.scan', release_download) return { 'success': True } - def scan(self, base_folder = None, release_download = None): + def scan(self, release_download = None): + if not release_download: release_download = {} if self.isDisabled(): return @@ -92,11 +97,11 @@ class Renamer(Plugin): log.info('Renamer is already running, if you see this often, check the logs above for errors.') return - from_folder = sp(self.conf('from') if not base_folder else base_folder) + from_folder = sp(self.conf('from')) to_folder = sp(self.conf('to')) - # Get movie folder to process - movie_folder = sp(release_download and release_download.get('folder')) + # Get media folder to process + media_folder = release_download.get('folder') # Get all folders that should not be processed no_process = [to_folder] @@ -104,7 +109,7 @@ class Renamer(Plugin): no_process.extend([item['destination'] for item in cat_list]) try: if Env.setting('library', section = 'manage').strip(): - no_process.extend(splitString(Env.setting('library', section = 'manage'), '::')) + no_process.extend([sp(manage_folder) for manage_folder in splitString(Env.setting('library', section = 'manage'), '::')]) except: pass @@ -114,40 +119,40 @@ class Renamer(Plugin): return else: for item in no_process: - if from_folder in item: - log.error('To protect your data, the movie libraries can\'t be inside of or the same as the "from" folder.') + if '%s%s' % (from_folder, os.path.sep) in item: + log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.') return - # Check to see if the no_process folders are inside the provided movie_folder - if movie_folder and not os.path.isdir(movie_folder): - log.debug('The provided movie folder %s does not exist. Trying to find it in the \'from\' folder.', movie_folder) + # Check to see if the no_process folders are inside the provided media_folder + if media_folder and not os.path.isdir(media_folder): + log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder) # Update to the from folder - if len(release_download.get('files')) == 1: - new_movie_folder = from_folder + if len(splitString(release_download.get('files'), '|')) == 1: + new_media_folder = from_folder else: - new_movie_folder = sp(os.path.join(from_folder, os.path.basename(movie_folder))) + new_media_folder = os.path.join(from_folder, os.path.basename(media_folder)) - if not os.path.isdir(new_movie_folder): - log.error('The provided movie folder %s does not exist and could also not be found in the \'from\' folder.', movie_folder) + if not os.path.isdir(new_media_folder): + log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder) return # Update the files - new_files = [os.path.join(new_movie_folder, os.path.relpath(filename, movie_folder)) for filename in splitString(release_download.get('files'), '|')] + new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in splitString(release_download.get('files'), '|')] if new_files and not os.path.isfile(new_files[0]): - log.error('The provided movie folder %s does not exist and its files could also not be found in the \'from\' folder.', movie_folder) + log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder) return # Update release_download info to the from folder - log.debug('Release %s found in the \'from\' folder.', movie_folder) - release_download['folder'] = new_movie_folder + log.debug('Release %s found in the \'from\' folder.', media_folder) + release_download['folder'] = new_media_folder release_download['files'] = '|'.join(new_files) - movie_folder = new_movie_folder + media_folder = new_media_folder - if movie_folder: + if media_folder: for item in no_process: - if movie_folder in item: - log.error('To protect your data, the movie libraries can\'t be inside of or the same as the provided movie folder.') + if '%s%s' % (media_folder, os.path.sep) in item: + log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.') return # Make sure a checkSnatched marked all downloads/seeds as such @@ -156,26 +161,26 @@ class Renamer(Plugin): self.renaming_started = True - # make sure the movie folder name is included in the search + # make sure the media folder name is included in the search folder = None files = [] - if movie_folder: - log.info('Scanning movie folder %s...', movie_folder) - folder = os.path.dirname(movie_folder) + if media_folder: + log.info('Scanning media folder %s...', media_folder) + folder = os.path.dirname(media_folder) if release_download.get('files', ''): files = splitString(release_download['files'], '|') # If there is only one file in the torrent, the downloader did not create a subfolder if len(files) == 1: - folder = movie_folder + folder = media_folder else: # Get all files from the specified folder try: - for root, folders, names in os.walk(movie_folder): - files.extend([os.path.join(root, name) for name in names]) + for root, folders, names in os.walk(media_folder): + files.extend([sp(os.path.join(root, name)) for name in names]) except: - log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc())) + log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc())) db = get_session() @@ -185,7 +190,7 @@ class Renamer(Plugin): # Unpack any archives extr_files = None if self.conf('unrar'): - folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files, + folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files, cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download)) groups = fireEvent('scanner.scan', folder = folder if folder else from_folder, @@ -496,8 +501,8 @@ class Renamer(Plugin): if os.path.isfile(src): os.remove(src) - parent_dir = sp(os.path.dirname(src)) - if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not from_folder in parent_dir: + parent_dir = os.path.dirname(src) + if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not '%s%s' % (parent_dir, os.path.sep) in [destination, media_folder] and not '%s%s' % (from_folder, os.path.sep) in parent_dir: delete_folders.append(parent_dir) except: @@ -529,7 +534,7 @@ class Renamer(Plugin): self.tagRelease(group = group, tag = 'failed_rename') # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent - if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(release_download): + if self.movieInFromFolder(media_folder) and self.downloadIsTorrent(release_download): self.tagRelease(group = group, tag = 'renamed_already') # Remove matching releases @@ -541,12 +546,12 @@ class Renamer(Plugin): log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download): - if movie_folder: + if media_folder: # Delete the movie folder - group_folder = movie_folder + group_folder = media_folder else: # Delete the first empty subfolder in the tree relative to the 'from' folder - group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder)).split(os.path.sep)[0]) + group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder).split(os.path.sep)[0])) try: log.info('Deleting folder: %s', group_folder) @@ -606,7 +611,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif isinstance(release_download, dict): # Tag download_files if they are known if release_download['files']: - tag_files = release_download['files'].split('|') + tag_files = splitString(release_download['files'], '|') # Tag all files in release folder else: @@ -614,6 +619,11 @@ Remove it if you want it to be renamed (again, or at least let it try again) tag_files.extend([os.path.join(root, name) for name in names]) for filename in tag_files: + + # Dont tag .ignore files + if os.path.splitext(filename)[1] == '.ignore': + continue + tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag) if not os.path.isfile(tag_filename): self.createFile(tag_filename, text) @@ -630,21 +640,21 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Untag download_files if they are known if release_download['files']: - tag_files = release_download['files'].split('|') + tag_files = splitString(release_download['files'], '|') # Untag all files in release folder else: for root, folders, names in os.walk(release_download['folder']): - tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore']) + tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) # Find all .ignore files in folder ignore_files = [] for root, dirnames, filenames in os.walk(folder): - ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag)) + ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and delete if found for tag_file in tag_files: - ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (re.escape(os.path.splitext(tag_file)[0]), tag if tag else '*')) + ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))) for filename in ignore_file: try: os.remove(filename) @@ -664,20 +674,20 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find tag on download_files if they are known if release_download['files']: - tag_files = release_download['files'].split('|') + tag_files = splitString(release_download['files'], '|') # Find tag on all files in release folder else: for root, folders, names in os.walk(release_download['folder']): - tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore']) + tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) # Find all .ignore files in folder for root, dirnames, filenames in os.walk(folder): - ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag)) + ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and return True found for tag_file in tag_files: - ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')) + ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))) if ignore_file: return True @@ -772,7 +782,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) return string def deleteEmptyFolder(self, folder, show_error = True): - folder = ss(folder) + folder = sp(folder) loge = log.error if show_error else log.debug for root, dirs, files in os.walk(folder): @@ -806,126 +816,159 @@ Remove it if you want it to be renamed (again, or at least let it try again) Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')]) ).all() + if not rels: + #No releases found that need status checking + self.checking_snatched = False + return True + + # Collect all download information with the download IDs from the releases + download_ids = [] + try: + for rel in rels: + rel_dict = rel.to_dict({'info': {}}) + if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'): + download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']}) + except: + log.error('Error getting download IDs from database') + self.checking_snatched = False + return False + + release_downloads = fireEvent('download.status', download_ids, merge = True) + if not release_downloads: + log.debug('Download status functionality is not implemented for any active downloaders.') + fireEvent('renamer.scan') + + self.checking_snatched = False + return True + scan_releases = [] scan_required = False - if rels: - log.debug('Checking status snatched releases...') + log.debug('Checking status snatched releases...') - release_downloads = fireEvent('download.status', merge = True) - if not release_downloads: - log.debug('Download status functionality is not implemented for active downloaders.') - scan_required = True - else: - try: - for rel in rels: - rel_dict = rel.to_dict({'info': {}}) - movie_dict = fireEvent('movie.get', rel.movie_id, single = True) + try: + for rel in rels: + rel_dict = rel.to_dict({'info': {}}) + movie_dict = fireEvent('media.get', media_id = rel.movie_id, single = True) - if not isinstance(rel_dict['info'], (dict)): - log.error('Faulty release found without any info, ignoring.') + if not isinstance(rel_dict['info'], (dict)): + log.error('Faulty release found without any info, ignoring.') + fireEvent('release.update_status', rel.id, status = ignored_status, single = True) + continue + + # Check if download ID is available + if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'): + log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name'])) + scan_required = True + + # Continue with next release + continue + + # Find release in downloaders + nzbname = self.createNzbName(rel_dict['info'], movie_dict) + + for release_download in release_downloads: + found_release = False + if rel_dict['info'].get('download_id'): + if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']: + log.debug('Found release by id: %s', release_download['id']) + found_release = True + break + else: + if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: + log.debug('Found release by release name or imdb ID: %s', release_download['name']) + found_release = True + break + + if not found_release: + log.info('%s not found in downloaders', nzbname) + + #Check status if already missing and for how long, if > 1 week, set to ignored else to missing + if rel.status_id == missing_status.get('id'): + if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: fireEvent('release.update_status', rel.id, status = ignored_status, single = True) - continue + else: + # Set the release to missing + fireEvent('release.update_status', rel.id, status = missing_status, single = True) - # check status - nzbname = self.createNzbName(rel_dict['info'], movie_dict) + # Continue with next release + continue - found = False - for release_download in release_downloads: - found_release = False - if rel_dict['info'].get('download_id'): - if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']: - log.debug('Found release by id: %s', release_download['id']) - found_release = True + # Log that we found the release + timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft'] + log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft)) + + # Check status of release + if release_download['status'] == 'busy': + # Set the release to snatched if it was missing before + fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + + # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading + if self.movieInFromFolder(release_download['folder']): + self.tagRelease(release_download = release_download, tag = 'downloading') + + elif release_download['status'] == 'seeding': + #If linking setting is enabled, process release + if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download): + log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) + + # Remove the downloading tag + self.untagRelease(release_download = release_download, tag = 'downloading') + + # Scan and set the torrent to paused if required + release_download.update({'pause': True, 'scan': True, 'process_complete': False}) + scan_releases.append(release_download) + else: + #let it seed + log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio'])) + + # Set the release to seeding + fireEvent('release.update_status', rel.id, status = seeding_status, single = True) + + elif release_download['status'] == 'failed': + # Set the release to failed + fireEvent('release.update_status', rel.id, status = failed_status, single = True) + + fireEvent('download.remove_failed', release_download, single = True) + + if self.conf('next_on_failed'): + fireEvent('movie.searcher.try_next_release', media_id = rel.movie_id) + + elif release_download['status'] == 'completed': + log.info('Download of %s completed!', release_download['name']) + + #Make sure the downloader sent over a path to look in + if self.statusInfoComplete(release_download): + + # If the release has been seeding, process now the seeding is done + if rel.status_id == seeding_status.get('id'): + if self.conf('file_action') != 'move': + # Set the release to done as the movie has already been renamed + fireEvent('release.update_status', rel.id, status = downloaded_status, single = True) + + # Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': False, 'process_complete': True}) + scan_releases.append(release_download) else: - if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: - found_release = True + # Scan and Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_releases.append(release_download) - if found_release: - timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft'] - log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft)) + else: + # Set the release to snatched if it was missing before + fireEvent('release.update_status', rel.id, status = snatched_status, single = True) - if release_download['status'] == 'busy': - # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + # Remove the downloading tag + self.untagRelease(release_download = release_download, tag = 'downloading') - # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading - if self.movieInFromFolder(release_download['folder']): - self.tagRelease(release_download = release_download, tag = 'downloading') + # Scan and Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_releases.append(release_download) + else: + scan_required = True - elif release_download['status'] == 'seeding': - #If linking setting is enabled, process release - if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download): - log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) - - # Remove the downloading tag - self.untagRelease(release_download = release_download, tag = 'downloading') - - # Scan and set the torrent to paused if required - release_download.update({'pause': True, 'scan': True, 'process_complete': False}) - scan_releases.append(release_download) - else: - #let it seed - log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio'])) - - # Set the release to seeding - fireEvent('release.update_status', rel.id, status = seeding_status, single = True) - - elif release_download['status'] == 'failed': - # Set the release to failed - fireEvent('release.update_status', rel.id, status = failed_status, single = True) - - fireEvent('download.remove_failed', release_download, single = True) - - if self.conf('next_on_failed'): - fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id) - elif release_download['status'] == 'completed': - log.info('Download of %s completed!', release_download['name']) - if self.statusInfoComplete(release_download): - - # If the release has been seeding, process now the seeding is done - if rel.status_id == seeding_status.get('id'): - if self.conf('file_action') != 'move': - # Set the release to done as the movie has already been renamed - fireEvent('release.update_status', rel.id, status = downloaded_status, single = True) - - # Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': False, 'process_complete': True}) - scan_releases.append(release_download) - else: - # Scan and Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': True, 'process_complete': True}) - scan_releases.append(release_download) - - else: - # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) - - # Remove the downloading tag - self.untagRelease(release_download = release_download, tag = 'downloading') - - # Scan and Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': True, 'process_complete': True}) - scan_releases.append(release_download) - else: - scan_required = True - - found = True - break - - if not found: - log.info('%s not found in downloaders', nzbname) - - #Check status if already missing and for how long, if > 1 week, set to ignored else to missing - if rel.status_id == missing_status.get('id'): - if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: - fireEvent('release.update_status', rel.id, status = ignored_status, single = True) - else: - # Set the release to missing - fireEvent('release.update_status', rel.id, status = missing_status, single = True) - - except: - log.error('Failed checking for release in downloader: %s', traceback.format_exc()) + except: + log.error('Failed checking for release in downloader: %s', traceback.format_exc()) # The following can either be done here, or inside the scanner if we pass it scan_items in one go for release_download in scan_releases: @@ -948,7 +991,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) fireEvent('renamer.scan') self.checking_snatched = False - return True def extendReleaseDownload(self, release_download): @@ -995,10 +1037,10 @@ Remove it if you want it to be renamed (again, or at least let it try again) def statusInfoComplete(self, release_download): return release_download['id'] and release_download['downloader'] and release_download['folder'] - def movieInFromFolder(self, movie_folder): - return (movie_folder and sp(self.conf('from')) in movie_folder) or not movie_folder + def movieInFromFolder(self, media_folder): + return media_folder and '%s%s' % (sp(self.conf('from')), os.path.sep) in sp(media_folder) or not media_folder - def extractFiles(self, folder = None, movie_folder = None, files = None, cleanup = False): + def extractFiles(self, folder = None, media_folder = None, files = None, cleanup = False): if not files: files = [] # RegEx for finding rar files @@ -1013,12 +1055,12 @@ Remove it if you want it to be renamed (again, or at least let it try again) folder = from_folder check_file_date = True - if movie_folder: + if media_folder: check_file_date = False if not files: for root, folders, names in os.walk(folder): - files.extend([os.path.join(root, name) for name in names]) + files.extend([sp(os.path.join(root, name)) for name in names]) # Find all archive files archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)] @@ -1064,13 +1106,13 @@ Remove it if you want it to be renamed (again, or at least let it try again) log.info('Archive %s found. Extracting...', os.path.basename(archive['file'])) try: rar_handle = RarFile(archive['file']) - extr_path = sp(os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))) + extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder)) self.makeDir(extr_path) for packedinfo in rar_handle.infolist(): - if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))): + if not packedinfo.isdir and not os.path.isfile(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))): log.debug('Extracting %s...', packedinfo.filename) rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False) - extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename))) + extr_files.append(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))) del rar_handle except Exception, e: log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc())) @@ -1087,9 +1129,9 @@ Remove it if you want it to be renamed (again, or at least let it try again) files.remove(filename) # Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided) - if extr_files and sp(folder) != from_folder: + if extr_files and folder != from_folder: for leftoverfile in list(files): - move_to = sp(os.path.join(from_folder, os.path.relpath(leftoverfile, folder))) + move_to = os.path.join(from_folder, os.path.relpath(leftoverfile, folder)) try: self.makeDir(os.path.dirname(move_to)) @@ -1109,18 +1151,18 @@ Remove it if you want it to be renamed (again, or at least let it try again) if cleanup: # Remove all left over folders - log.debug('Removing old movie folder %s...', movie_folder) - self.deleteEmptyFolder(movie_folder) + log.debug('Removing old movie folder %s...', media_folder) + self.deleteEmptyFolder(media_folder) - movie_folder = sp(os.path.join(from_folder, os.path.relpath(movie_folder, folder))) + media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder)) folder = from_folder if extr_files: files.extend(extr_files) - # Cleanup files and folder if movie_folder was not provided - if not movie_folder: + # Cleanup files and folder if media_folder was not provided + if not media_folder: files = [] folder = None - return folder, movie_folder, files, extr_files + return folder, media_folder, files, extr_files diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index e77e62ad..24eb7ac7 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -124,7 +124,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in os.walk(folder): - files.extend([os.path.join(root, filename) for filename in walk_files]) + files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) # Break if CP wants to shut down if self.shuttingDown(): @@ -134,7 +134,7 @@ class Scanner(Plugin): log.error('Failed getting files from %s: %s', (folder, traceback.format_exc())) else: check_file_date = False - files = [ss(x) for x in files] + files = [sp(x) for x in files] for file_path in files: @@ -454,7 +454,7 @@ class Scanner(Plugin): data['resolution_width'] = meta.get('resolution_width', 720) data['resolution_height'] = meta.get('resolution_height', 480) data['audio_channels'] = meta.get('audio_channels', 2.0) - data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480) + data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2) except: log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) pass diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index 5f9da1a1..54b6ca31 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -1,11 +1,11 @@ -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getTitle, splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \ - halfMultipartScore + halfMultipartScore, sceneScore from couchpotato.environment import Env log = CPLog(__name__) @@ -62,4 +62,7 @@ class Score(Plugin): if extra_score: score += extra_score(nzb) + # Scene / Nuke scoring + score += sceneScore(nzb['name']) + return score diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index 6aa0b465..895f5fc0 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -1,8 +1,13 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog from couchpotato.environment import Env import re +import traceback + +log = CPLog(__name__) + name_scores = [ # Tags @@ -160,3 +165,38 @@ def halfMultipartScore(nzb_name): return -30 return 0 + + +def sceneScore(nzb_name): + + check_names = [nzb_name] + + # Match names between " + try: check_names.append(re.search(r'([\'"])[^\1]*\1', nzb_name).group(0)) + except: pass + + # Match longest name between [] + try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', nzb_name), key = len).strip()) + except: pass + + for name in check_names: + + # Strip twice, remove possible file extensions + name = name.lower().strip(' "\'\.-_\[\]') + name = re.sub('\.([a-z0-9]{0,4})$', '', name) + name = name.strip(' "\'\.-_\[\]') + + # Make sure year and groupname is in there + year = re.findall('(?P19[0-9]{2}|20[0-9]{2})', name) + group = re.findall('\-([a-z0-9]+)$', name) + + if len(year) > 0 and len(group) > 0: + try: + validate = fireEvent('release.validate', name, single = True) + if validate and tryInt(validate.get('score')) != 0: + log.debug('Release "%s" scored %s, reason: %s', (nzb_name, validate['score'], validate['reasons'])) + return tryInt(validate.get('score')) + except: + log.error('Failed scoring scene: %s', traceback.format_exc()) + + return 0 diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index bbd40853..fcff4cdf 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/couchpotato/core/plugins/subtitle/__init__.py @@ -20,7 +20,7 @@ config = [{ }, { 'name': 'languages', - 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at on Wikipedia', + 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'), }, # { # 'name': 'automatic', diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index e447dacd..7504d6a9 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -1,6 +1,6 @@ from couchpotato import get_session from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.encoding import toUnicode, sp from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -58,9 +58,9 @@ class Subtitle(Plugin): for d_sub in downloaded: log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files)) - group['files']['subtitle'].append(d_sub.path) - group['before_rename'].append(d_sub.path) - group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2] + group['files']['subtitle'].append(sp(d_sub.path)) + group['before_rename'].append(sp(d_sub.path)) + group['subtitle_language'][sp(d_sub.path)] = [d_sub.language.alpha2] return True diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index da27d853..41b1e62f 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -264,14 +264,14 @@ class ResultList(list): result_ids = None provider = None - movie = None + media = None quality = None - def __init__(self, provider, movie, quality, **kwargs): + def __init__(self, provider, media, quality, **kwargs): self.result_ids = [] self.provider = provider - self.movie = movie + self.media = media self.quality = quality self.kwargs = kwargs @@ -285,13 +285,13 @@ class ResultList(list): new_result = self.fillResult(result) - is_correct = fireEvent('searcher.correct_release', new_result, self.movie, self.quality, + is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality, imdb_results = self.kwargs.get('imdb_results', False), single = True) if is_correct and new_result['id'] not in self.result_ids: is_correct_weight = float(is_correct) - new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True) + new_result['score'] += fireEvent('score.calculate', new_result, self.media, single = True) old_score = new_result['score'] new_result['score'] = int(old_score * is_correct_weight) diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index 0bb2e6a4..daf2a306 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -93,11 +93,11 @@ class MovieResultModifier(Plugin): for movie in l.movies: if movie.status_id == active_status['id']: - temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True) + temp['in_wanted'] = fireEvent('media.get', movie.id, single = True) for release in movie.releases: if release.status_id == done_status['id']: - temp['in_library'] = fireEvent('movie.get', movie.id, single = True) + temp['in_library'] = fireEvent('media.get', movie.id, single = True) except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) diff --git a/couchpotato/core/providers/info/couchpotatoapi/main.py b/couchpotato/core/providers/info/couchpotatoapi/main.py index 89eddc3c..4dd942e0 100644 --- a/couchpotato/core/providers/info/couchpotatoapi/main.py +++ b/couchpotato/core/providers/info/couchpotatoapi/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.providers.info.base import MovieProvider from couchpotato.environment import Env +import base64 import time log = CPLog(__name__) @@ -11,6 +12,7 @@ log = CPLog(__name__) class CouchPotatoApi(MovieProvider): urls = { + 'validate': 'https://api.couchpota.to/validate/%s/', 'search': 'https://api.couchpota.to/search/%s/', 'info': 'https://api.couchpota.to/info/%s/', 'is_movie': 'https://api.couchpota.to/ismovie/%s/', @@ -30,6 +32,8 @@ class CouchPotatoApi(MovieProvider): addEvent('movie.suggest', self.getSuggestions) addEvent('movie.is_movie', self.isMovie) + addEvent('release.validate', self.validate) + addEvent('cp.source_url', self.getSourceUrl) addEvent('cp.messages', self.getMessages) @@ -51,6 +55,14 @@ class CouchPotatoApi(MovieProvider): def search(self, q, limit = 5): return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders()) + def validate(self, name = None): + + if not name: + return + + name_enc = base64.b64encode(name) + return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders()) + def isMovie(self, identifier = None): if not identifier: diff --git a/couchpotato/core/providers/info/omdbapi/main.py b/couchpotato/core/providers/info/omdbapi/main.py index 47374f47..605dadf1 100755 --- a/couchpotato/core/providers/info/omdbapi/main.py +++ b/couchpotato/core/providers/info/omdbapi/main.py @@ -84,6 +84,10 @@ class OMDBAPI(MovieProvider): year = tryInt(movie.get('Year', '')) + actors = {} + for actor in splitString(movie.get('Actors', '')): + actors[actor] = '' #omdb does not return actor roles + movie_data = { 'type': 'movie', 'via_imdb': True, @@ -105,7 +109,7 @@ class OMDBAPI(MovieProvider): 'genres': splitString(movie.get('Genre', '')), 'directors': splitString(movie.get('Director', '')), 'writers': splitString(movie.get('Writer', '')), - 'actors': splitString(movie.get('Actors', '')), + 'actor_roles': actors, } movie_data = dict((k, v) for k, v in movie_data.iteritems() if v) except: diff --git a/couchpotato/core/providers/info/themoviedb/main.py b/couchpotato/core/providers/info/themoviedb/main.py index a7901351..cd957927 100644 --- a/couchpotato/core/providers/info/themoviedb/main.py +++ b/couchpotato/core/providers/info/themoviedb/main.py @@ -92,6 +92,13 @@ class TheMovieDb(MovieProvider): poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') + images = { + 'poster': [poster] if poster else [], + #'backdrop': [backdrop] if backdrop else [], + 'poster_original': [poster_original] if poster_original else [], + 'backdrop_original': [backdrop_original] if backdrop_original else [], + } + # Genres try: genres = [genre.name for genre in movie.genres] @@ -103,18 +110,22 @@ class TheMovieDb(MovieProvider): if not movie.releasedate or year == '1900' or year.lower() == 'none': year = None + # Gather actors data + actors = {} + for cast_item in movie.cast: + try: + actors[toUnicode(cast_item.name)] = toUnicode(cast_item.character) + images['actor %s' % toUnicode(cast_item.name)] = self.getImage(cast_item, type = 'profile', size = 'original') + except: + log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc())) + movie_data = { 'type': 'movie', 'via_tmdb': True, 'tmdb_id': movie.id, 'titles': [toUnicode(movie.title)], 'original_title': movie.originaltitle, - 'images': { - 'poster': [poster] if poster else [], - #'backdrop': [backdrop] if backdrop else [], - 'poster_original': [poster_original] if poster_original else [], - 'backdrop_original': [backdrop_original] if backdrop_original else [], - }, + 'images': images, 'imdb': movie.imdb, 'runtime': movie.runtime, 'released': str(movie.releasedate), @@ -122,6 +133,7 @@ class TheMovieDb(MovieProvider): 'plot': movie.overview, 'genres': genres, 'collection': getattr(movie.collection, 'name', None), + 'actor_roles': actors } movie_data = dict((k, v) for k, v in movie_data.iteritems() if v) @@ -145,7 +157,7 @@ class TheMovieDb(MovieProvider): try: image_url = getattr(movie, type).geturl(size = 'original') except: - log.debug('Failed getting %s.%s for "%s"', (type, size, movie.title)) + log.debug('Failed getting %s.%s for "%s"', (type, size, movie)) return image_url diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py index 7073363d..4c547c35 100644 --- a/couchpotato/core/providers/metadata/xbmc/main.py +++ b/couchpotato/core/providers/metadata/xbmc/main.py @@ -65,7 +65,7 @@ class XBMC(MetaDataBase): name = type try: - if data['library'].get(type): + if movie_info.get(type): el = SubElement(nfoxml, name) el.text = toUnicode(movie_info.get(type, '')) except: @@ -89,10 +89,18 @@ class XBMC(MetaDataBase): genres.text = toUnicode(genre) # Actors - for actor in movie_info.get('actors', []): - actors = SubElement(nfoxml, 'actor') - name = SubElement(actors, 'name') - name.text = toUnicode(actor) + for actor_name in movie_info.get('actor_roles', {}): + role_name = movie_info['actor_roles'][actor_name] + + actor = SubElement(nfoxml, 'actor') + name = SubElement(actor, 'name') + name.text = toUnicode(actor_name) + if role_name: + role = SubElement(actor, 'role') + role.text = toUnicode(role_name) + if movie_info['images'].get('actor %s' % actor_name, ''): + thumb = SubElement(actor, 'thumb') + thumb.text = toUnicode(movie_info['images'].get('actor %s' % actor_name)) # Directors for director_name in movie_info.get('directors', []): @@ -112,6 +120,51 @@ class XBMC(MetaDataBase): sorttitle = SubElement(nfoxml, 'sorttitle') sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year')) + # Images + for image_url in movie_info['images']['poster_original']: + image = SubElement(nfoxml, 'thumb') + image.text = toUnicode(image_url) + fanart = SubElement(nfoxml, 'fanart') + for image_url in movie_info['images']['backdrop_original']: + image = SubElement(fanart, 'thumb') + image.text = toUnicode(image_url) + + # Add trailer if found + trailer_found = False + if data.get('renamed_files'): + for filename in data.get('renamed_files'): + if 'trailer' in filename: + trailer = SubElement(nfoxml, 'trailer') + trailer.text = toUnicode(filename) + trailer_found = True + if not trailer_found and data['files'].get('trailer'): + trailer = SubElement(nfoxml, 'trailer') + trailer.text = toUnicode(data['files']['trailer'][0]) + + # Add file metadata + fileinfo = SubElement(nfoxml, 'fileinfo') + streamdetails = SubElement(fileinfo, 'streamdetails') + + # Video data + if data['meta_data'].get('video'): + video = SubElement(streamdetails, 'video') + codec = SubElement(video, 'codec') + codec.text = toUnicode(data['meta_data']['video']) + aspect = SubElement(video, 'aspect') + aspect.text = str(data['meta_data']['aspect']) + width = SubElement(video, 'width') + width.text = str(data['meta_data']['resolution_width']) + height = SubElement(video, 'height') + height.text = str(data['meta_data']['resolution_height']) + + # Audio data + if data['meta_data'].get('audio'): + audio = SubElement(streamdetails, 'audio') + codec = SubElement(audio, 'codec') + codec.text = toUnicode(data['meta_data'].get('audio')) + channels = SubElement(audio, 'channels') + channels.text = toUnicode(data['meta_data'].get('audio_channels')) + # Clean up the xml and return it nfoxml = xml.dom.minidom.parseString(tostring(nfoxml)) xml_string = nfoxml.toprettyxml(indent = ' ') diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index bd1b6c32..c1eed852 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -10,6 +10,7 @@ from urllib2 import HTTPError from urlparse import urlparse import time import traceback +import urllib2 log = CPLog(__name__) @@ -159,7 +160,15 @@ class Newznab(NZBProvider, RSS): return 'try_next' try: - data = self.urlopen(url, show_error = False) + # Get final redirected url + log.debug('Checking %s for redirects.', url) + req = urllib2.Request(url) + res = urllib2.urlopen(req) + finalurl = res.geturl() + if finalurl != url: + log.debug('Redirect url used: %s', finalurl) + + data = self.urlopen(finalurl, show_error = False) self.limits_reached[host] = False return data except HTTPError, e: diff --git a/couchpotato/core/providers/torrent/bithdtv/__init__.py b/couchpotato/core/providers/torrent/bithdtv/__init__.py index 2c7af031..8c6f97a0 100644 --- a/couchpotato/core/providers/torrent/bithdtv/__init__.py +++ b/couchpotato/core/providers/torrent/bithdtv/__init__.py @@ -4,7 +4,7 @@ def start(): return BiTHDTV() config = [{ - 'name': 'BiT-HDTV', + 'name': 'bithdtv', 'groups': [ { 'tab': 'searcher', diff --git a/couchpotato/core/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/providers/torrent/torrentpotato/__init__.py new file mode 100644 index 00000000..5054f98c --- /dev/null +++ b/couchpotato/core/providers/torrent/torrentpotato/__init__.py @@ -0,0 +1,66 @@ +from .main import TorrentPotato + +def start(): + return TorrentPotato() + +config = [{ + 'name': 'torrentpotato', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentPotato', + 'order': 10, + 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'use', + 'default': '' + }, + { + 'name': 'host', + 'default': '', + 'description': 'The url path of your TorrentPotato provider.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': '0', + 'description': 'Starting score for each release found via this provider.', + }, + { + 'name': 'name', + 'label': 'Username', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'default': '1', + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'default': '40', + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'pass_key', + 'default': ',', + 'label': 'Pass Key', + 'description': 'Can be found on your profile page', + 'type': 'combined', + 'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/providers/torrent/torrentpotato/main.py new file mode 100644 index 00000000..a76c0c8f --- /dev/null +++ b/couchpotato/core/providers/torrent/torrentpotato/main.py @@ -0,0 +1,129 @@ +from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.base import ResultList +from couchpotato.core.providers.torrent.base import TorrentProvider +from urlparse import urlparse +import re +import traceback + +log = CPLog(__name__) + + +class TorrentPotato(TorrentProvider): + + urls = {} + limits_reached = {} + + http_time_between_calls = 1 # Seconds + + def search(self, movie, quality): + hosts = self.getHosts() + + results = ResultList(self, movie, quality, imdb_results = True) + + for host in hosts: + if self.isDisabled(host): + continue + + self._searchOnHost(host, movie, quality, results) + + return results + + def _searchOnHost(self, host, movie, quality, results): + + arguments = tryUrlencode({ + 'user': host['name'], + 'passkey': host['pass_key'], + 'imdbid': movie['library']['identifier'] + }) + url = '%s?%s' % (host['host'], arguments) + + torrents = self.getJsonData(url, cache_timeout = 1800) + + if torrents: + try: + if torrents.get('error'): + log.error('%s: %s', (torrents.get('error'), host['host'])) + elif torrents.get('results'): + for torrent in torrents.get('results', []): + results.append({ + 'id': torrent.get('torrent_id'), + 'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet', + 'provider_extra': urlparse(host['host']).hostname or host['host'], + 'name': toUnicode(torrent.get('release_name')), + 'url': torrent.get('download_url'), + 'detail_url': torrent.get('details_url'), + 'size': torrent.get('size'), + 'score': host['extra_score'], + 'seeders': torrent.get('seeders'), + 'leechers': torrent.get('leechers'), + 'seed_ratio': host['seed_ratio'], + 'seed_time': host['seed_time'], + }) + + except: + log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc())) + + def getHosts(self): + + uses = splitString(str(self.conf('use')), clean = False) + hosts = splitString(self.conf('host'), clean = False) + names = splitString(self.conf('name'), clean = False) + seed_times = splitString(self.conf('seed_time'), clean = False) + seed_ratios = splitString(self.conf('seed_ratio'), clean = False) + pass_keys = splitString(self.conf('pass_key'), clean = False) + extra_score = splitString(self.conf('extra_score'), clean = False) + + list = [] + for nr in range(len(hosts)): + + try: key = pass_keys[nr] + except: key = '' + + try: host = hosts[nr] + except: host = '' + + try: name = names[nr] + except: name = '' + + try: ratio = seed_ratios[nr] + except: ratio = '' + + try: seed_time = seed_times[nr] + except: seed_time = '' + + list.append({ + 'use': uses[nr], + 'host': host, + 'name': name, + 'seed_ratio': tryFloat(ratio), + 'seed_time': tryInt(seed_time), + 'pass_key': key, + 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 + }) + + return list + + def belongsTo(self, url, provider = None, host = None): + + hosts = self.getHosts() + + for host in hosts: + result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider) + if result: + return result + + def isDisabled(self, host = None): + return not self.isEnabled(host) + + def isEnabled(self, host = None): + + # Return true if at least one is enabled and no host is given + if host is None: + for host in self.getHosts(): + if self.isEnabled(host): + return True + return False + + return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use']) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 0f04d838..b393ef94 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -78,6 +78,7 @@ class Env(object): return s.get(attr, default = default, section = section, type = type) # Set setting + s.addSection(section) s.set(section, attr, value) s.save() diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 571023ea..452f0055 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -8,6 +8,7 @@ from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers from tornado.httpserver import HTTPServer from tornado.web import Application, StaticFileHandler, RedirectHandler +from uuid import uuid4 import locale import logging import os.path @@ -144,7 +145,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En Env.set('dev', development) # Disable logging for some modules - for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler']: + for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler', 'tornado']: logging.getLogger(logger_name).setLevel(logging.ERROR) for logger_name in ['gntp', 'migrate']: @@ -215,6 +216,10 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En Env.set('web_base', web_base) api_key = Env.setting('api_key') + if not api_key: + api_key = uuid4().hex + Env.setting('api_key', value = api_key) + api_base = r'%sapi/%s/' % (web_base, api_key) Env.set('api_base', api_base) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 59fac34b..eae865f4 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -11,6 +11,12 @@ pages: [], block: [], + initialize: function(){ + var self = this; + + self.global_events = {}; + }, + setup: function(options) { var self = this; self.setOptions(options); @@ -30,7 +36,7 @@ History.addEvent('change', self.openPage.bind(self)); self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self)); self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self)); - + // Check if device is touchenabled self.touch_device = 'ontouchstart' in window || navigator.msMaxTouchPoints; if(self.touch_device) @@ -55,7 +61,7 @@ History.push(url); } }, - + isMac: function(){ return Browser.Platform.mac }, @@ -111,7 +117,7 @@ } }) ]; - + setting_links.each(function(a){ self.block.more.addLink(a) }); @@ -336,6 +342,66 @@ }) ) ); + }, + + /* + * Global events + */ + on: function(name, handle){ + var self = this; + + if(!self.global_events[name]) + self.global_events[name] = []; + + self.global_events[name].push(handle); + + }, + + trigger: function(name, args, on_complete){ + var self = this; + + if(!self.global_events[name]){ return; } + + if(!on_complete && typeOf(args) == 'function'){ + on_complete = args; + args = {}; + } + + // Create parallel callback + var callbacks = []; + self.global_events[name].each(function(handle, nr){ + + callbacks.push(function(callback){ + var results = handle(args || {}); + callback(null, results || null); + }); + + }); + + // Fire events + async.parallel(callbacks, function(err, results){ + if(err) p(err); + + if(on_complete) + on_complete(results); + }); + + }, + + off: function(name, handle){ + var self = this; + + if(!self.global_events[name]) return; + + // Remove single + if(handle){ + self.global_events[name] = self.global_events[name].erase(handle); + } + // Reset full event + else { + self.global_events[name] = []; + } + } }); @@ -503,7 +569,7 @@ function randomString(length, extra) { case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; } }); - return this.sort(comparer); + return this.stableSort(comparer); } }); diff --git a/couchpotato/static/scripts/library/Array.stableSort.js b/couchpotato/static/scripts/library/Array.stableSort.js new file mode 100644 index 00000000..062c7566 --- /dev/null +++ b/couchpotato/static/scripts/library/Array.stableSort.js @@ -0,0 +1,56 @@ +/* +--- + +script: Array.stableSort.js + +description: Add a stable sort algorithm for all browsers + +license: MIT-style license. + +authors: + - Yorick Sijsling + +requires: + core/1.3: '*' + +provides: + - [Array.stableSort, Array.mergeSort] + +... +*/ + +(function() { + + var defaultSortFunction = function(a, b) { + return a > b ? 1 : (a < b ? -1 : 0); + } + + Array.implement({ + + stableSort: function(compare) { + // I would love some real feature recognition. Problem is that an unstable algorithm sometimes/often gives the same result as an unstable algorithm. + return (Browser.chrome || Browser.firefox2 || Browser.opera9) ? this.mergeSort(compare) : this.sort(compare); + }, + + mergeSort: function(compare, token) { + compare = compare || defaultSortFunction; + if (this.length > 1) { + // Split and sort both parts + var right = this.splice(Math.floor(this.length / 2)).mergeSort(compare); + var left = this.splice(0).mergeSort(compare); // 'this' is now empty. + + // Merge parts together + while (left.length > 0 || right.length > 0) { + this.push( + right.length === 0 ? left.shift() + : left.length === 0 ? right.shift() + : compare(left[0], right[0]) > 0 ? right.shift() + : left.shift()); + } + } + return this; + } + + }); +})(); + diff --git a/couchpotato/static/scripts/library/async.js b/couchpotato/static/scripts/library/async.js new file mode 100644 index 00000000..cb6320d6 --- /dev/null +++ b/couchpotato/static/scripts/library/async.js @@ -0,0 +1,955 @@ +/*global setImmediate: false, setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = setImmediate; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + } + })); + }); + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _each(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor !== Array) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (test()) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (!test()) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain) cargo.drain(); + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.compose = function (/* functions... */) { + var fns = Array.prototype.reverse.call(arguments); + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // Node.js + else if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // included directly via +""" + soup = BeautifulSoup(doc, "xml") + # lxml would have stripped this while parsing, but we can add + # it later. + soup.script.string = 'console.log("< < hey > > ");' + encoded = soup.encode() + self.assertTrue(b"< < hey > >" in encoded) + + def test_can_parse_unicode_document(self): + markup = u'Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!' + soup = self.soup(markup) + self.assertEqual(u'Sacr\xe9 bleu!', soup.root.string) + + def test_popping_namespaced_tag(self): + markup = 'b2012-07-02T20:33:42Zcd' + soup = self.soup(markup) + self.assertEqual( + unicode(soup.rss), markup) def test_docstring_includes_correct_encoding(self): soup = self.soup("") @@ -472,6 +529,20 @@ class XMLTreeBuilderSmokeTest(object): self.assertEqual("http://example.com/", root['xmlns:a']) self.assertEqual("http://example.net/", root['xmlns:b']) + def test_closing_namespaced_tag(self): + markup = '20010504' + soup = self.soup(markup) + self.assertEqual(unicode(soup.p), markup) + + def test_namespaced_attributes(self): + markup = '' + soup = self.soup(markup) + self.assertEqual(unicode(soup.foo), markup) + + def test_namespaced_attributes_xml_namespace(self): + markup = 'bar' + soup = self.soup(markup) + self.assertEqual(unicode(soup.foo), markup) class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): """Smoke test for a tree builder that supports HTML5.""" @@ -501,6 +572,12 @@ class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): self.assertEqual(namespace, soup.math.namespace) self.assertEqual(namespace, soup.msqrt.namespace) + def test_xml_declaration_becomes_comment(self): + markup = '' + soup = self.soup(markup) + self.assertTrue(isinstance(soup.contents[0], Comment)) + self.assertEqual(soup.contents[0], '?xml version="1.0" encoding="utf-8"?') + self.assertEqual("html", soup.contents[0].next_element.name) def skipIf(condition, reason): def nothing(test, *args, **kwargs): diff --git a/libs/gntp/__init__.py b/libs/gntp/__init__.py index eabbfa47..e69de29b 100755 --- a/libs/gntp/__init__.py +++ b/libs/gntp/__init__.py @@ -1,509 +0,0 @@ -import re -import hashlib -import time -import StringIO - -__version__ = '0.8' - -#GNTP/ [:][ :.] -GNTP_INFO_LINE = re.compile( - 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' + - ' (?P[A-Z0-9]+(:(?P[A-F0-9]+))?) ?' + - '((?P[A-Z0-9]+):(?P[A-F0-9]+).(?P[A-F0-9]+))?\r\n', - re.IGNORECASE -) - -GNTP_INFO_LINE_SHORT = re.compile( - 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)', - re.IGNORECASE -) - -GNTP_HEADER = re.compile('([\w-]+):(.+)') - -GNTP_EOL = '\r\n' - - -class BaseError(Exception): - def gntp_error(self): - error = GNTPError(self.errorcode, self.errordesc) - return error.encode() - - -class ParseError(BaseError): - errorcode = 500 - errordesc = 'Error parsing the message' - - -class AuthError(BaseError): - errorcode = 400 - errordesc = 'Error with authorization' - - -class UnsupportedError(BaseError): - errorcode = 500 - errordesc = 'Currently unsupported by gntp.py' - - -class _GNTPBuffer(StringIO.StringIO): - """GNTP Buffer class""" - def writefmt(self, message = "", *args): - """Shortcut function for writing GNTP Headers""" - self.write((message % args).encode('utf8', 'replace')) - self.write(GNTP_EOL) - - -class _GNTPBase(object): - """Base initilization - - :param string messagetype: GNTP Message type - :param string version: GNTP Protocol version - :param string encription: Encryption protocol - """ - def __init__(self, messagetype = None, version = '1.0', encryption = None): - self.info = { - 'version': version, - 'messagetype': messagetype, - 'encryptionAlgorithmID': encryption - } - self.headers = {} - self.resources = {} - - def __str__(self): - return self.encode() - - def _parse_info(self, data): - """Parse the first line of a GNTP message to get security and other info values - - :param string data: GNTP Message - :return dict: Parsed GNTP Info line - """ - - match = GNTP_INFO_LINE.match(data) - - if not match: - raise ParseError('ERROR_PARSING_INFO_LINE') - - info = match.groupdict() - if info['encryptionAlgorithmID'] == 'NONE': - info['encryptionAlgorithmID'] = None - - return info - - def set_password(self, password, encryptAlgo = 'MD5'): - """Set a password for a GNTP Message - - :param string password: Null to clear password - :param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512 - """ - hash = { - 'MD5': hashlib.md5, - 'SHA1': hashlib.sha1, - 'SHA256': hashlib.sha256, - 'SHA512': hashlib.sha512, - } - - self.password = password - self.encryptAlgo = encryptAlgo.upper() - if not password: - self.info['encryptionAlgorithmID'] = None - self.info['keyHashAlgorithm'] = None - return - if not self.encryptAlgo in hash.keys(): - raise UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo) - - hashfunction = hash.get(self.encryptAlgo) - - password = password.encode('utf8') - seed = time.ctime() - salt = hashfunction(seed).hexdigest() - saltHash = hashfunction(seed).digest() - keyBasis = password + saltHash - key = hashfunction(keyBasis).digest() - keyHash = hashfunction(key).hexdigest() - - self.info['keyHashAlgorithmID'] = self.encryptAlgo - self.info['keyHash'] = keyHash.upper() - self.info['salt'] = salt.upper() - - def _decode_hex(self, value): - """Helper function to decode hex string to `proper` hex string - - :param string value: Human readable hex string - :return string: Hex string - """ - result = '' - for i in range(0, len(value), 2): - tmp = int(value[i:i + 2], 16) - result += chr(tmp) - return result - - def _decode_binary(self, rawIdentifier, identifier): - rawIdentifier += '\r\n\r\n' - dataLength = int(identifier['Length']) - pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier) - pointerEnd = pointerStart + dataLength - data = self.raw[pointerStart:pointerEnd] - if not len(data) == dataLength: - raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data))) - return data - - def _validate_password(self, password): - """Validate GNTP Message against stored password""" - self.password = password - if password == None: - raise AuthError('Missing password') - keyHash = self.info.get('keyHash', None) - if keyHash is None and self.password is None: - return True - if keyHash is None: - raise AuthError('Invalid keyHash') - if self.password is None: - raise AuthError('Missing password') - - password = self.password.encode('utf8') - saltHash = self._decode_hex(self.info['salt']) - - keyBasis = password + saltHash - key = hashlib.md5(keyBasis).digest() - keyHash = hashlib.md5(key).hexdigest() - - if not keyHash.upper() == self.info['keyHash'].upper(): - raise AuthError('Invalid Hash') - return True - - def validate(self): - """Verify required headers""" - for header in self._requiredHeaders: - if not self.headers.get(header, False): - raise ParseError('Missing Notification Header: ' + header) - - def _format_info(self): - """Generate info line for GNTP Message - - :return string: - """ - info = u'GNTP/%s %s' % ( - self.info.get('version'), - self.info.get('messagetype'), - ) - if self.info.get('encryptionAlgorithmID', None): - info += ' %s:%s' % ( - self.info.get('encryptionAlgorithmID'), - self.info.get('ivValue'), - ) - else: - info += ' NONE' - - if self.info.get('keyHashAlgorithmID', None): - info += ' %s:%s.%s' % ( - self.info.get('keyHashAlgorithmID'), - self.info.get('keyHash'), - self.info.get('salt') - ) - - return info - - def _parse_dict(self, data): - """Helper function to parse blocks of GNTP headers into a dictionary - - :param string data: - :return dict: - """ - dict = {} - for line in data.split('\r\n'): - match = GNTP_HEADER.match(line) - if not match: - continue - - key = unicode(match.group(1).strip(), 'utf8', 'replace') - val = unicode(match.group(2).strip(), 'utf8', 'replace') - dict[key] = val - return dict - - def add_header(self, key, value): - if isinstance(value, unicode): - self.headers[key] = value - else: - self.headers[key] = unicode('%s' % value, 'utf8', 'replace') - - def add_resource(self, data): - """Add binary resource - - :param string data: Binary Data - """ - identifier = hashlib.md5(data).hexdigest() - self.resources[identifier] = data - return 'x-growl-resource://%s' % identifier - - def decode(self, data, password = None): - """Decode GNTP Message - - :param string data: - """ - self.password = password - self.raw = data - parts = self.raw.split('\r\n\r\n') - self.info = self._parse_info(data) - self.headers = self._parse_dict(parts[0]) - - def encode(self): - """Encode a generic GNTP Message - - :return string: GNTP Message ready to be sent - """ - - buffer = _GNTPBuffer() - - buffer.writefmt(self._format_info()) - - #Headers - for k, v in self.headers.iteritems(): - buffer.writefmt('%s: %s', k, v) - buffer.writefmt() - - #Resources - for resource, data in self.resources.iteritems(): - buffer.writefmt('Identifier: %s', resource) - buffer.writefmt('Length: %d', len(data)) - buffer.writefmt() - buffer.write(data) - buffer.writefmt() - buffer.writefmt() - - return buffer.getvalue() - - -class GNTPRegister(_GNTPBase): - """Represents a GNTP Registration Command - - :param string data: (Optional) See decode() - :param string password: (Optional) Password to use while encoding/decoding messages - """ - _requiredHeaders = [ - 'Application-Name', - 'Notifications-Count' - ] - _requiredNotificationHeaders = ['Notification-Name'] - - def __init__(self, data = None, password = None): - _GNTPBase.__init__(self, 'REGISTER') - self.notifications = [] - - if data: - self.decode(data, password) - else: - self.set_password(password) - self.add_header('Application-Name', 'pygntp') - self.add_header('Notifications-Count', 0) - - def validate(self): - '''Validate required headers and validate notification headers''' - for header in self._requiredHeaders: - if not self.headers.get(header, False): - raise ParseError('Missing Registration Header: ' + header) - for notice in self.notifications: - for header in self._requiredNotificationHeaders: - if not notice.get(header, False): - raise ParseError('Missing Notification Header: ' + header) - - def decode(self, data, password): - """Decode existing GNTP Registration message - - :param string data: Message to decode - """ - self.raw = data - parts = self.raw.split('\r\n\r\n') - self.info = self._parse_info(data) - self._validate_password(password) - self.headers = self._parse_dict(parts[0]) - - for i, part in enumerate(parts): - if i == 0: - continue # Skip Header - if part.strip() == '': - continue - notice = self._parse_dict(part) - if notice.get('Notification-Name', False): - self.notifications.append(notice) - elif notice.get('Identifier', False): - notice['Data'] = self._decode_binary(part, notice) - #open('register.png','wblol').write(notice['Data']) - self.resources[notice.get('Identifier')] = notice - - def add_notification(self, name, enabled = True): - """Add new Notification to Registration message - - :param string name: Notification Name - :param boolean enabled: Enable this notification by default - """ - notice = {} - notice['Notification-Name'] = u'%s' % name - notice['Notification-Enabled'] = u'%s' % enabled - - self.notifications.append(notice) - self.add_header('Notifications-Count', len(self.notifications)) - - def encode(self): - """Encode a GNTP Registration Message - - :return string: Encoded GNTP Registration message - """ - - buffer = _GNTPBuffer() - - buffer.writefmt(self._format_info()) - - #Headers - for k, v in self.headers.iteritems(): - buffer.writefmt('%s: %s', k, v) - buffer.writefmt() - - #Notifications - if len(self.notifications) > 0: - for notice in self.notifications: - for k, v in notice.iteritems(): - buffer.writefmt('%s: %s', k, v) - buffer.writefmt() - - #Resources - for resource, data in self.resources.iteritems(): - buffer.writefmt('Identifier: %s', resource) - buffer.writefmt('Length: %d', len(data)) - buffer.writefmt() - buffer.write(data) - buffer.writefmt() - buffer.writefmt() - - return buffer.getvalue() - - -class GNTPNotice(_GNTPBase): - """Represents a GNTP Notification Command - - :param string data: (Optional) See decode() - :param string app: (Optional) Set Application-Name - :param string name: (Optional) Set Notification-Name - :param string title: (Optional) Set Notification Title - :param string password: (Optional) Password to use while encoding/decoding messages - """ - _requiredHeaders = [ - 'Application-Name', - 'Notification-Name', - 'Notification-Title' - ] - - def __init__(self, data = None, app = None, name = None, title = None, password = None): - _GNTPBase.__init__(self, 'NOTIFY') - - if data: - self.decode(data, password) - else: - self.set_password(password) - if app: - self.add_header('Application-Name', app) - if name: - self.add_header('Notification-Name', name) - if title: - self.add_header('Notification-Title', title) - - def decode(self, data, password): - """Decode existing GNTP Notification message - - :param string data: Message to decode. - """ - self.raw = data - parts = self.raw.split('\r\n\r\n') - self.info = self._parse_info(data) - self._validate_password(password) - self.headers = self._parse_dict(parts[0]) - - for i, part in enumerate(parts): - if i == 0: - continue # Skip Header - if part.strip() == '': - continue - notice = self._parse_dict(part) - if notice.get('Identifier', False): - notice['Data'] = self._decode_binary(part, notice) - #open('notice.png','wblol').write(notice['Data']) - self.resources[notice.get('Identifier')] = notice - - -class GNTPSubscribe(_GNTPBase): - """Represents a GNTP Subscribe Command - - :param string data: (Optional) See decode() - :param string password: (Optional) Password to use while encoding/decoding messages - """ - _requiredHeaders = [ - 'Subscriber-ID', - 'Subscriber-Name', - ] - - def __init__(self, data = None, password = None): - _GNTPBase.__init__(self, 'SUBSCRIBE') - if data: - self.decode(data, password) - else: - self.set_password(password) - - -class GNTPOK(_GNTPBase): - """Represents a GNTP OK Response - - :param string data: (Optional) See _GNTPResponse.decode() - :param string action: (Optional) Set type of action the OK Response is for - """ - _requiredHeaders = ['Response-Action'] - - def __init__(self, data = None, action = None): - _GNTPBase.__init__(self, '-OK') - if data: - self.decode(data) - if action: - self.add_header('Response-Action', action) - - -class GNTPError(_GNTPBase): - """Represents a GNTP Error response - - :param string data: (Optional) See _GNTPResponse.decode() - :param string errorcode: (Optional) Error code - :param string errordesc: (Optional) Error Description - """ - _requiredHeaders = ['Error-Code', 'Error-Description'] - - def __init__(self, data = None, errorcode = None, errordesc = None): - _GNTPBase.__init__(self, '-ERROR') - if data: - self.decode(data) - if errorcode: - self.add_header('Error-Code', errorcode) - self.add_header('Error-Description', errordesc) - - def error(self): - return (self.headers.get('Error-Code', None), - self.headers.get('Error-Description', None)) - - -def parse_gntp(data, password = None): - """Attempt to parse a message as a GNTP message - - :param string data: Message to be parsed - :param string password: Optional password to be used to verify the message - """ - match = GNTP_INFO_LINE_SHORT.match(data) - if not match: - raise ParseError('INVALID_GNTP_INFO') - info = match.groupdict() - if info['messagetype'] == 'REGISTER': - return GNTPRegister(data, password = password) - elif info['messagetype'] == 'NOTIFY': - return GNTPNotice(data, password = password) - elif info['messagetype'] == 'SUBSCRIBE': - return GNTPSubscribe(data, password = password) - elif info['messagetype'] == '-OK': - return GNTPOK(data) - elif info['messagetype'] == '-ERROR': - return GNTPError(data) - raise ParseError('INVALID_GNTP_MESSAGE') diff --git a/libs/gntp/cli.py b/libs/gntp/cli.py new file mode 100644 index 00000000..bc083062 --- /dev/null +++ b/libs/gntp/cli.py @@ -0,0 +1,141 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +import logging +import os +import sys +from optparse import OptionParser, OptionGroup + +from gntp.notifier import GrowlNotifier +from gntp.shim import RawConfigParser +from gntp.version import __version__ + +DEFAULT_CONFIG = os.path.expanduser('~/.gntp') + +config = RawConfigParser({ + 'hostname': 'localhost', + 'password': None, + 'port': 23053, +}) +config.read([DEFAULT_CONFIG]) +if not config.has_section('gntp'): + config.add_section('gntp') + + +class ClientParser(OptionParser): + def __init__(self): + OptionParser.__init__(self, version="%%prog %s" % __version__) + + group = OptionGroup(self, "Network Options") + group.add_option("-H", "--host", + dest="host", default=config.get('gntp', 'hostname'), + help="Specify a hostname to which to send a remote notification. [%default]") + group.add_option("--port", + dest="port", default=config.getint('gntp', 'port'), type="int", + help="port to listen on [%default]") + group.add_option("-P", "--password", + dest='password', default=config.get('gntp', 'password'), + help="Network password") + self.add_option_group(group) + + group = OptionGroup(self, "Notification Options") + group.add_option("-n", "--name", + dest="app", default='Python GNTP Test Client', + help="Set the name of the application [%default]") + group.add_option("-s", "--sticky", + dest='sticky', default=False, action="store_true", + help="Make the notification sticky [%default]") + group.add_option("--image", + dest="icon", default=None, + help="Icon for notification (URL or /path/to/file)") + group.add_option("-m", "--message", + dest="message", default=None, + help="Sets the message instead of using stdin") + group.add_option("-p", "--priority", + dest="priority", default=0, type="int", + help="-2 to 2 [%default]") + group.add_option("-d", "--identifier", + dest="identifier", + help="Identifier for coalescing") + group.add_option("-t", "--title", + dest="title", default=None, + help="Set the title of the notification [%default]") + group.add_option("-N", "--notification", + dest="name", default='Notification', + help="Set the notification name [%default]") + group.add_option("--callback", + dest="callback", + help="URL callback") + self.add_option_group(group) + + # Extra Options + self.add_option('-v', '--verbose', + dest='verbose', default=0, action='count', + help="Verbosity levels") + + def parse_args(self, args=None, values=None): + values, args = OptionParser.parse_args(self, args, values) + + if values.message is None: + print('Enter a message followed by Ctrl-D') + try: + message = sys.stdin.read() + except KeyboardInterrupt: + exit() + else: + message = values.message + + if values.title is None: + values.title = ' '.join(args) + + # If we still have an empty title, use the + # first bit of the message as the title + if values.title == '': + values.title = message[:20] + + values.verbose = logging.WARNING - values.verbose * 10 + + return values, message + + +def main(): + (options, message) = ClientParser().parse_args() + logging.basicConfig(level=options.verbose) + if not os.path.exists(DEFAULT_CONFIG): + logging.info('No config read found at %s', DEFAULT_CONFIG) + + growl = GrowlNotifier( + applicationName=options.app, + notifications=[options.name], + defaultNotifications=[options.name], + hostname=options.host, + password=options.password, + port=options.port, + ) + result = growl.register() + if result is not True: + exit(result) + + # This would likely be better placed within the growl notifier + # class but until I make _checkIcon smarter this is "easier" + if options.icon is not None and not options.icon.startswith('http'): + logging.info('Loading image %s', options.icon) + f = open(options.icon) + options.icon = f.read() + f.close() + + result = growl.notify( + noteType=options.name, + title=options.title, + description=message, + icon=options.icon, + sticky=options.sticky, + priority=options.priority, + callback=options.callback, + identifier=options.identifier, + ) + if result is not True: + exit(result) + +if __name__ == "__main__": + main() diff --git a/libs/gntp/config.py b/libs/gntp/config.py new file mode 100644 index 00000000..7536bd14 --- /dev/null +++ b/libs/gntp/config.py @@ -0,0 +1,77 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +""" +The gntp.config module is provided as an extended GrowlNotifier object that takes +advantage of the ConfigParser module to allow us to setup some default values +(such as hostname, password, and port) in a more global way to be shared among +programs using gntp +""" +import logging +import os + +import gntp.notifier +import gntp.shim + +__all__ = [ + 'mini', + 'GrowlNotifier' +] + +logger = logging.getLogger(__name__) + + +class GrowlNotifier(gntp.notifier.GrowlNotifier): + """ + ConfigParser enhanced GrowlNotifier object + + For right now, we are only interested in letting users overide certain + values from ~/.gntp + + :: + + [gntp] + hostname = ? + password = ? + port = ? + """ + def __init__(self, *args, **kwargs): + config = gntp.shim.RawConfigParser({ + 'hostname': kwargs.get('hostname', 'localhost'), + 'password': kwargs.get('password'), + 'port': kwargs.get('port', 23053), + }) + + config.read([os.path.expanduser('~/.gntp')]) + + # If the file does not exist, then there will be no gntp section defined + # and the config.get() lines below will get confused. Since we are not + # saving the config, it should be safe to just add it here so the + # code below doesn't complain + if not config.has_section('gntp'): + logger.info('Error reading ~/.gntp config file') + config.add_section('gntp') + + kwargs['password'] = config.get('gntp', 'password') + kwargs['hostname'] = config.get('gntp', 'hostname') + kwargs['port'] = config.getint('gntp', 'port') + + super(GrowlNotifier, self).__init__(*args, **kwargs) + + +def mini(description, **kwargs): + """Single notification function + + Simple notification function in one line. Has only one required parameter + and attempts to use reasonable defaults for everything else + :param string description: Notification message + """ + kwargs['notifierFactory'] = GrowlNotifier + gntp.notifier.mini(description, **kwargs) + + +if __name__ == '__main__': + # If we're running this module directly we're likely running it as a test + # so extra debugging is useful + logging.basicConfig(level=logging.INFO) + mini('Testing mini notification') diff --git a/libs/gntp/core.py b/libs/gntp/core.py new file mode 100644 index 00000000..ee544d3d --- /dev/null +++ b/libs/gntp/core.py @@ -0,0 +1,511 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +import hashlib +import re +import time + +import gntp.shim +import gntp.errors as errors + +__all__ = [ + 'GNTPRegister', + 'GNTPNotice', + 'GNTPSubscribe', + 'GNTPOK', + 'GNTPError', + 'parse_gntp', +] + +#GNTP/ [:][ :.] +GNTP_INFO_LINE = re.compile( + 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' + + ' (?P[A-Z0-9]+(:(?P[A-F0-9]+))?) ?' + + '((?P[A-Z0-9]+):(?P[A-F0-9]+).(?P[A-F0-9]+))?\r\n', + re.IGNORECASE +) + +GNTP_INFO_LINE_SHORT = re.compile( + 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)', + re.IGNORECASE +) + +GNTP_HEADER = re.compile('([\w-]+):(.+)') + +GNTP_EOL = gntp.shim.b('\r\n') +GNTP_SEP = gntp.shim.b(': ') + + +class _GNTPBuffer(gntp.shim.StringIO): + """GNTP Buffer class""" + def writeln(self, value=None): + if value: + self.write(gntp.shim.b(value)) + self.write(GNTP_EOL) + + def writeheader(self, key, value): + if not isinstance(value, str): + value = str(value) + self.write(gntp.shim.b(key)) + self.write(GNTP_SEP) + self.write(gntp.shim.b(value)) + self.write(GNTP_EOL) + + +class _GNTPBase(object): + """Base initilization + + :param string messagetype: GNTP Message type + :param string version: GNTP Protocol version + :param string encription: Encryption protocol + """ + def __init__(self, messagetype=None, version='1.0', encryption=None): + self.info = { + 'version': version, + 'messagetype': messagetype, + 'encryptionAlgorithmID': encryption + } + self.hash_algo = { + 'MD5': hashlib.md5, + 'SHA1': hashlib.sha1, + 'SHA256': hashlib.sha256, + 'SHA512': hashlib.sha512, + } + self.headers = {} + self.resources = {} + + def __str__(self): + return self.encode() + + def _parse_info(self, data): + """Parse the first line of a GNTP message to get security and other info values + + :param string data: GNTP Message + :return dict: Parsed GNTP Info line + """ + + match = GNTP_INFO_LINE.match(data) + + if not match: + raise errors.ParseError('ERROR_PARSING_INFO_LINE') + + info = match.groupdict() + if info['encryptionAlgorithmID'] == 'NONE': + info['encryptionAlgorithmID'] = None + + return info + + def set_password(self, password, encryptAlgo='MD5'): + """Set a password for a GNTP Message + + :param string password: Null to clear password + :param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512 + """ + if not password: + self.info['encryptionAlgorithmID'] = None + self.info['keyHashAlgorithm'] = None + return + + self.password = gntp.shim.b(password) + self.encryptAlgo = encryptAlgo.upper() + + if not self.encryptAlgo in self.hash_algo: + raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo) + + hashfunction = self.hash_algo.get(self.encryptAlgo) + + password = password.encode('utf8') + seed = time.ctime().encode('utf8') + salt = hashfunction(seed).hexdigest() + saltHash = hashfunction(seed).digest() + keyBasis = password + saltHash + key = hashfunction(keyBasis).digest() + keyHash = hashfunction(key).hexdigest() + + self.info['keyHashAlgorithmID'] = self.encryptAlgo + self.info['keyHash'] = keyHash.upper() + self.info['salt'] = salt.upper() + + def _decode_hex(self, value): + """Helper function to decode hex string to `proper` hex string + + :param string value: Human readable hex string + :return string: Hex string + """ + result = '' + for i in range(0, len(value), 2): + tmp = int(value[i:i + 2], 16) + result += chr(tmp) + return result + + def _decode_binary(self, rawIdentifier, identifier): + rawIdentifier += '\r\n\r\n' + dataLength = int(identifier['Length']) + pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier) + pointerEnd = pointerStart + dataLength + data = self.raw[pointerStart:pointerEnd] + if not len(data) == dataLength: + raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data))) + return data + + def _validate_password(self, password): + """Validate GNTP Message against stored password""" + self.password = password + if password is None: + raise errors.AuthError('Missing password') + keyHash = self.info.get('keyHash', None) + if keyHash is None and self.password is None: + return True + if keyHash is None: + raise errors.AuthError('Invalid keyHash') + if self.password is None: + raise errors.AuthError('Missing password') + + keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5') + + password = self.password.encode('utf8') + saltHash = self._decode_hex(self.info['salt']) + + keyBasis = password + saltHash + self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest() + keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest() + + if not keyHash.upper() == self.info['keyHash'].upper(): + raise errors.AuthError('Invalid Hash') + return True + + def validate(self): + """Verify required headers""" + for header in self._requiredHeaders: + if not self.headers.get(header, False): + raise errors.ParseError('Missing Notification Header: ' + header) + + def _format_info(self): + """Generate info line for GNTP Message + + :return string: + """ + info = 'GNTP/%s %s' % ( + self.info.get('version'), + self.info.get('messagetype'), + ) + if self.info.get('encryptionAlgorithmID', None): + info += ' %s:%s' % ( + self.info.get('encryptionAlgorithmID'), + self.info.get('ivValue'), + ) + else: + info += ' NONE' + + if self.info.get('keyHashAlgorithmID', None): + info += ' %s:%s.%s' % ( + self.info.get('keyHashAlgorithmID'), + self.info.get('keyHash'), + self.info.get('salt') + ) + + return info + + def _parse_dict(self, data): + """Helper function to parse blocks of GNTP headers into a dictionary + + :param string data: + :return dict: Dictionary of parsed GNTP Headers + """ + d = {} + for line in data.split('\r\n'): + match = GNTP_HEADER.match(line) + if not match: + continue + + key = match.group(1).strip() + val = match.group(2).strip() + d[key] = val + return d + + def add_header(self, key, value): + self.headers[key] = value + + def add_resource(self, data): + """Add binary resource + + :param string data: Binary Data + """ + data = gntp.shim.b(data) + identifier = hashlib.md5(data).hexdigest() + self.resources[identifier] = data + return 'x-growl-resource://%s' % identifier + + def decode(self, data, password=None): + """Decode GNTP Message + + :param string data: + """ + self.password = password + self.raw = gntp.shim.u(data) + parts = self.raw.split('\r\n\r\n') + self.info = self._parse_info(self.raw) + self.headers = self._parse_dict(parts[0]) + + def encode(self): + """Encode a generic GNTP Message + + :return string: GNTP Message ready to be sent. Returned as a byte string + """ + + buff = _GNTPBuffer() + + buff.writeln(self._format_info()) + + #Headers + for k, v in self.headers.items(): + buff.writeheader(k, v) + buff.writeln() + + #Resources + for resource, data in self.resources.items(): + buff.writeheader('Identifier', resource) + buff.writeheader('Length', len(data)) + buff.writeln() + buff.write(data) + buff.writeln() + buff.writeln() + + return buff.getvalue() + + +class GNTPRegister(_GNTPBase): + """Represents a GNTP Registration Command + + :param string data: (Optional) See decode() + :param string password: (Optional) Password to use while encoding/decoding messages + """ + _requiredHeaders = [ + 'Application-Name', + 'Notifications-Count' + ] + _requiredNotificationHeaders = ['Notification-Name'] + + def __init__(self, data=None, password=None): + _GNTPBase.__init__(self, 'REGISTER') + self.notifications = [] + + if data: + self.decode(data, password) + else: + self.set_password(password) + self.add_header('Application-Name', 'pygntp') + self.add_header('Notifications-Count', 0) + + def validate(self): + '''Validate required headers and validate notification headers''' + for header in self._requiredHeaders: + if not self.headers.get(header, False): + raise errors.ParseError('Missing Registration Header: ' + header) + for notice in self.notifications: + for header in self._requiredNotificationHeaders: + if not notice.get(header, False): + raise errors.ParseError('Missing Notification Header: ' + header) + + def decode(self, data, password): + """Decode existing GNTP Registration message + + :param string data: Message to decode + """ + self.raw = gntp.shim.u(data) + parts = self.raw.split('\r\n\r\n') + self.info = self._parse_info(self.raw) + self._validate_password(password) + self.headers = self._parse_dict(parts[0]) + + for i, part in enumerate(parts): + if i == 0: + continue # Skip Header + if part.strip() == '': + continue + notice = self._parse_dict(part) + if notice.get('Notification-Name', False): + self.notifications.append(notice) + elif notice.get('Identifier', False): + notice['Data'] = self._decode_binary(part, notice) + #open('register.png','wblol').write(notice['Data']) + self.resources[notice.get('Identifier')] = notice + + def add_notification(self, name, enabled=True): + """Add new Notification to Registration message + + :param string name: Notification Name + :param boolean enabled: Enable this notification by default + """ + notice = {} + notice['Notification-Name'] = name + notice['Notification-Enabled'] = enabled + + self.notifications.append(notice) + self.add_header('Notifications-Count', len(self.notifications)) + + def encode(self): + """Encode a GNTP Registration Message + + :return string: Encoded GNTP Registration message. Returned as a byte string + """ + + buff = _GNTPBuffer() + + buff.writeln(self._format_info()) + + #Headers + for k, v in self.headers.items(): + buff.writeheader(k, v) + buff.writeln() + + #Notifications + if len(self.notifications) > 0: + for notice in self.notifications: + for k, v in notice.items(): + buff.writeheader(k, v) + buff.writeln() + + #Resources + for resource, data in self.resources.items(): + buff.writeheader('Identifier', resource) + buff.writeheader('Length', len(data)) + buff.writeln() + buff.write(data) + buff.writeln() + buff.writeln() + + return buff.getvalue() + + +class GNTPNotice(_GNTPBase): + """Represents a GNTP Notification Command + + :param string data: (Optional) See decode() + :param string app: (Optional) Set Application-Name + :param string name: (Optional) Set Notification-Name + :param string title: (Optional) Set Notification Title + :param string password: (Optional) Password to use while encoding/decoding messages + """ + _requiredHeaders = [ + 'Application-Name', + 'Notification-Name', + 'Notification-Title' + ] + + def __init__(self, data=None, app=None, name=None, title=None, password=None): + _GNTPBase.__init__(self, 'NOTIFY') + + if data: + self.decode(data, password) + else: + self.set_password(password) + if app: + self.add_header('Application-Name', app) + if name: + self.add_header('Notification-Name', name) + if title: + self.add_header('Notification-Title', title) + + def decode(self, data, password): + """Decode existing GNTP Notification message + + :param string data: Message to decode. + """ + self.raw = gntp.shim.u(data) + parts = self.raw.split('\r\n\r\n') + self.info = self._parse_info(self.raw) + self._validate_password(password) + self.headers = self._parse_dict(parts[0]) + + for i, part in enumerate(parts): + if i == 0: + continue # Skip Header + if part.strip() == '': + continue + notice = self._parse_dict(part) + if notice.get('Identifier', False): + notice['Data'] = self._decode_binary(part, notice) + #open('notice.png','wblol').write(notice['Data']) + self.resources[notice.get('Identifier')] = notice + + +class GNTPSubscribe(_GNTPBase): + """Represents a GNTP Subscribe Command + + :param string data: (Optional) See decode() + :param string password: (Optional) Password to use while encoding/decoding messages + """ + _requiredHeaders = [ + 'Subscriber-ID', + 'Subscriber-Name', + ] + + def __init__(self, data=None, password=None): + _GNTPBase.__init__(self, 'SUBSCRIBE') + if data: + self.decode(data, password) + else: + self.set_password(password) + + +class GNTPOK(_GNTPBase): + """Represents a GNTP OK Response + + :param string data: (Optional) See _GNTPResponse.decode() + :param string action: (Optional) Set type of action the OK Response is for + """ + _requiredHeaders = ['Response-Action'] + + def __init__(self, data=None, action=None): + _GNTPBase.__init__(self, '-OK') + if data: + self.decode(data) + if action: + self.add_header('Response-Action', action) + + +class GNTPError(_GNTPBase): + """Represents a GNTP Error response + + :param string data: (Optional) See _GNTPResponse.decode() + :param string errorcode: (Optional) Error code + :param string errordesc: (Optional) Error Description + """ + _requiredHeaders = ['Error-Code', 'Error-Description'] + + def __init__(self, data=None, errorcode=None, errordesc=None): + _GNTPBase.__init__(self, '-ERROR') + if data: + self.decode(data) + if errorcode: + self.add_header('Error-Code', errorcode) + self.add_header('Error-Description', errordesc) + + def error(self): + return (self.headers.get('Error-Code', None), + self.headers.get('Error-Description', None)) + + +def parse_gntp(data, password=None): + """Attempt to parse a message as a GNTP message + + :param string data: Message to be parsed + :param string password: Optional password to be used to verify the message + """ + data = gntp.shim.u(data) + match = GNTP_INFO_LINE_SHORT.match(data) + if not match: + raise errors.ParseError('INVALID_GNTP_INFO') + info = match.groupdict() + if info['messagetype'] == 'REGISTER': + return GNTPRegister(data, password=password) + elif info['messagetype'] == 'NOTIFY': + return GNTPNotice(data, password=password) + elif info['messagetype'] == 'SUBSCRIBE': + return GNTPSubscribe(data, password=password) + elif info['messagetype'] == '-OK': + return GNTPOK(data) + elif info['messagetype'] == '-ERROR': + return GNTPError(data) + raise errors.ParseError('INVALID_GNTP_MESSAGE') diff --git a/libs/gntp/errors.py b/libs/gntp/errors.py new file mode 100644 index 00000000..c006fd68 --- /dev/null +++ b/libs/gntp/errors.py @@ -0,0 +1,25 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +class BaseError(Exception): + pass + + +class ParseError(BaseError): + errorcode = 500 + errordesc = 'Error parsing the message' + + +class AuthError(BaseError): + errorcode = 400 + errordesc = 'Error with authorization' + + +class UnsupportedError(BaseError): + errorcode = 500 + errordesc = 'Currently unsupported by gntp.py' + + +class NetworkError(BaseError): + errorcode = 500 + errordesc = "Error connecting to growl server" diff --git a/libs/gntp/notifier.py b/libs/gntp/notifier.py index 539dae2a..1719ecdf 100755 --- a/libs/gntp/notifier.py +++ b/libs/gntp/notifier.py @@ -1,3 +1,6 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + """ The gntp.notifier module is provided as a simple way to send notifications using GNTP @@ -9,10 +12,15 @@ using GNTP `Original Python bindings `_ """ -import gntp -import socket import logging import platform +import socket +import sys + +from gntp.version import __version__ +import gntp.core +import gntp.errors as errors +import gntp.shim __all__ = [ 'mini', @@ -37,9 +45,9 @@ class GrowlNotifier(object): passwordHash = 'MD5' socketTimeout = 3 - def __init__(self, applicationName = 'Python GNTP', notifications = [], - defaultNotifications = None, applicationIcon = None, hostname = 'localhost', - password = None, port = 23053): + def __init__(self, applicationName='Python GNTP', notifications=[], + defaultNotifications=None, applicationIcon=None, hostname='localhost', + password=None, port=23053): self.applicationName = applicationName self.notifications = list(notifications) @@ -61,7 +69,7 @@ class GrowlNotifier(object): then we return False ''' logger.info('Checking icon') - return data.startswith('http') + return gntp.shim.u(data).startswith('http') def register(self): """Send GNTP Registration @@ -71,7 +79,7 @@ class GrowlNotifier(object): sent a registration message at least once """ logger.info('Sending registration to %s:%s', self.hostname, self.port) - register = gntp.GNTPRegister() + register = gntp.core.GNTPRegister() register.add_header('Application-Name', self.applicationName) for notification in self.notifications: enabled = notification in self.defaultNotifications @@ -80,16 +88,16 @@ class GrowlNotifier(object): if self._checkIcon(self.applicationIcon): register.add_header('Application-Icon', self.applicationIcon) else: - id = register.add_resource(self.applicationIcon) - register.add_header('Application-Icon', id) + resource = register.add_resource(self.applicationIcon) + register.add_header('Application-Icon', resource) if self.password: register.set_password(self.password, self.passwordHash) self.add_origin_info(register) self.register_hook(register) return self._send('register', register) - def notify(self, noteType, title, description, icon = None, sticky = False, - priority = None, callback = None, identifier = None): + def notify(self, noteType, title, description, icon=None, sticky=False, + priority=None, callback=None, identifier=None, custom={}): """Send a GNTP notifications .. warning:: @@ -102,6 +110,8 @@ class GrowlNotifier(object): :param boolean sticky: Sticky notification :param integer priority: Message priority level from -2 to 2 :param string callback: URL callback + :param dict custom: Custom attributes. Key names should be prefixed with X- + according to the spec but this is not enforced by this class .. warning:: For now, only URL callbacks are supported. In the future, the @@ -109,7 +119,7 @@ class GrowlNotifier(object): """ logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port) assert noteType in self.notifications - notice = gntp.GNTPNotice() + notice = gntp.core.GNTPNotice() notice.add_header('Application-Name', self.applicationName) notice.add_header('Notification-Name', noteType) notice.add_header('Notification-Title', title) @@ -123,8 +133,8 @@ class GrowlNotifier(object): if self._checkIcon(icon): notice.add_header('Notification-Icon', icon) else: - id = notice.add_resource(icon) - notice.add_header('Notification-Icon', id) + resource = notice.add_resource(icon) + notice.add_header('Notification-Icon', resource) if description: notice.add_header('Notification-Text', description) @@ -133,6 +143,9 @@ class GrowlNotifier(object): if identifier: notice.add_header('Notification-Coalescing-ID', identifier) + for key in custom: + notice.add_header(key, custom[key]) + self.add_origin_info(notice) self.notify_hook(notice) @@ -140,7 +153,7 @@ class GrowlNotifier(object): def subscribe(self, id, name, port): """Send a Subscribe request to a remote machine""" - sub = gntp.GNTPSubscribe() + sub = gntp.core.GNTPSubscribe() sub.add_header('Subscriber-ID', id) sub.add_header('Subscriber-Name', name) sub.add_header('Subscriber-Port', port) @@ -156,7 +169,7 @@ class GrowlNotifier(object): """Add optional Origin headers to message""" packet.add_header('Origin-Machine-Name', platform.node()) packet.add_header('Origin-Software-Name', 'gntp.py') - packet.add_header('Origin-Software-Version', gntp.__version__) + packet.add_header('Origin-Software-Version', __version__) packet.add_header('Origin-Platform-Name', platform.system()) packet.add_header('Origin-Platform-Version', platform.platform()) @@ -179,27 +192,33 @@ class GrowlNotifier(object): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(self.socketTimeout) - s.connect((self.hostname, self.port)) - s.send(data) - recv_data = s.recv(1024) - while not recv_data.endswith("\r\n\r\n"): - recv_data += s.recv(1024) - response = gntp.parse_gntp(recv_data) + try: + s.connect((self.hostname, self.port)) + s.send(data) + recv_data = s.recv(1024) + while not recv_data.endswith(gntp.shim.b("\r\n\r\n")): + recv_data += s.recv(1024) + except socket.error: + # Python2.5 and Python3 compatibile exception + exc = sys.exc_info()[1] + raise errors.NetworkError(exc) + + response = gntp.core.parse_gntp(recv_data) s.close() logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response) - if type(response) == gntp.GNTPOK: + if type(response) == gntp.core.GNTPOK: return True logger.error('Invalid response: %s', response.error()) return response.error() -def mini(description, applicationName = 'PythonMini', noteType = "Message", - title = "Mini Message", applicationIcon = None, hostname = 'localhost', - password = None, port = 23053, sticky = False, priority = None, - callback = None, notificationIcon = None, identifier = None, - notifierFactory = GrowlNotifier): +def mini(description, applicationName='PythonMini', noteType="Message", + title="Mini Message", applicationIcon=None, hostname='localhost', + password=None, port=23053, sticky=False, priority=None, + callback=None, notificationIcon=None, identifier=None, + notifierFactory=GrowlNotifier): """Single notification function Simple notification function in one line. Has only one required parameter @@ -210,32 +229,37 @@ def mini(description, applicationName = 'PythonMini', noteType = "Message", For now, only URL callbacks are supported. In the future, the callback argument will also support a function """ - growl = notifierFactory( - applicationName = applicationName, - notifications = [noteType], - defaultNotifications = [noteType], - applicationIcon = applicationIcon, - hostname = hostname, - password = password, - port = port, - ) - result = growl.register() - if result is not True: - return result + try: + growl = notifierFactory( + applicationName=applicationName, + notifications=[noteType], + defaultNotifications=[noteType], + applicationIcon=applicationIcon, + hostname=hostname, + password=password, + port=port, + ) + result = growl.register() + if result is not True: + return result - return growl.notify( - noteType = noteType, - title = title, - description = description, - icon = notificationIcon, - sticky = sticky, - priority = priority, - callback = callback, - identifier = identifier, - ) + return growl.notify( + noteType=noteType, + title=title, + description=description, + icon=notificationIcon, + sticky=sticky, + priority=priority, + callback=callback, + identifier=identifier, + ) + except Exception: + # We want the "mini" function to be simple and swallow Exceptions + # in order to be less invasive + logger.exception("Growl error") if __name__ == '__main__': # If we're running this module directly we're likely running it as a test # so extra debugging is useful - logging.basicConfig(level = logging.INFO) + logging.basicConfig(level=logging.INFO) mini('Testing mini notification') diff --git a/libs/gntp/shim.py b/libs/gntp/shim.py new file mode 100644 index 00000000..3a387828 --- /dev/null +++ b/libs/gntp/shim.py @@ -0,0 +1,45 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +""" +Python2.5 and Python3.3 compatibility shim + +Heavily inspirted by the "six" library. +https://pypi.python.org/pypi/six +""" + +import sys + +PY3 = sys.version_info[0] == 3 + +if PY3: + def b(s): + if isinstance(s, bytes): + return s + return s.encode('utf8', 'replace') + + def u(s): + if isinstance(s, bytes): + return s.decode('utf8', 'replace') + return s + + from io import BytesIO as StringIO + from configparser import RawConfigParser +else: + def b(s): + if isinstance(s, unicode): + return s.encode('utf8', 'replace') + return s + + def u(s): + if isinstance(s, unicode): + return s + if isinstance(s, int): + s = str(s) + return unicode(s, "utf8", "replace") + + from StringIO import StringIO + from ConfigParser import RawConfigParser + +b.__doc__ = "Ensure we have a byte string" +u.__doc__ = "Ensure we have a unicode string" diff --git a/libs/gntp/version.py b/libs/gntp/version.py new file mode 100644 index 00000000..2166aaca --- /dev/null +++ b/libs/gntp/version.py @@ -0,0 +1,4 @@ +# Copyright: 2013 Paul Traylor +# These sources are released under the terms of the MIT license: see LICENSE + +__version__ = '1.0.2' diff --git a/libs/guessit/__init__.py b/libs/guessit/__init__.py index ce140248..e6cfa276 100755 --- a/libs/guessit/__init__.py +++ b/libs/guessit/__init__.py @@ -20,7 +20,7 @@ from __future__ import unicode_literals -__version__ = '0.7-dev' +__version__ = '0.6.2' __all__ = ['Guess', 'Language', 'guess_file_info', 'guess_video_info', 'guess_movie_info', 'guess_episode_info'] @@ -76,6 +76,7 @@ from guessit.language import Language from guessit.matcher import IterativeMatcher from guessit.textutils import clean_string import logging +import json log = logging.getLogger(__name__) @@ -105,17 +106,74 @@ def _guess_filename(filename, filetype): mtree = IterativeMatcher(filename, filetype=filetype) + m = mtree.matched() + + second_pass_opts = [] + second_pass_transfo_opts = {} + # if there are multiple possible years found, we assume the first one is # part of the title, reparse the tree taking this into account years = set(n.value for n in find_nodes(mtree.match_tree, 'year')) if len(years) >= 2: - mtree = IterativeMatcher(filename, filetype=filetype, - opts=['skip_first_year']) + second_pass_opts.append('skip_first_year') + to_skip_language_nodes = [] + + title_nodes = set(n for n in find_nodes(mtree.match_tree, ['title', 'series'])) + title_spans = {} + for title_node in title_nodes: + title_spans[title_node.span[0]] = title_node + title_spans[title_node.span[1]] = title_node + + for lang_key in ('language', 'subtitleLanguage'): + langs = {} + lang_nodes = set(n for n in find_nodes(mtree.match_tree, lang_key)) + + for lang_node in lang_nodes: + lang = lang_node.guess.get(lang_key, None) + if len(lang_node.value) > 3 and (lang_node.span[0] in title_spans.keys() or lang_node.span[1] in title_spans.keys()): + # Language is next or before title, and is not a language code. Add to skip for 2nd pass. + + # if filetype is subtitle and the language appears last, just before + # the extension, then it is likely a subtitle language + parts = clean_string(lang_node.root.value).split() + if m['type'] in ['moviesubtitle', 'episodesubtitle'] and (parts.index(lang_node.value) == len(parts) - 2): + continue + + to_skip_language_nodes.append(lang_node) + elif not lang in langs: + langs[lang] = lang_node + else: + # The same language was found. Keep the more confident one, and add others to skip for 2nd pass. + existing_lang_node = langs[lang] + to_skip = None + if existing_lang_node.guess.confidence('language') >= lang_node.guess.confidence('language'): + # lang_node is to remove + to_skip = lang_node + else: + # existing_lang_node is to remove + langs[lang] = lang_node + to_skip = existing_lang_node + to_skip_language_nodes.append(to_skip) + + + if to_skip_language_nodes: + second_pass_transfo_opts['guess_language'] = ( + ((), { 'skip': [ { 'node_idx': node.parent.node_idx, + 'span': node.span } + for node in to_skip_language_nodes ] })) + + if second_pass_opts or second_pass_transfo_opts: + # 2nd pass is needed + log.info("Running 2nd pass with options: %s" % second_pass_opts) + log.info("Transfo options: %s" % second_pass_transfo_opts) + mtree = IterativeMatcher(filename, filetype=filetype, + opts=second_pass_opts, + transfo_opts=second_pass_transfo_opts) m = mtree.matched() - if 'language' not in m and 'subtitleLanguage' not in m: + if 'language' not in m and 'subtitleLanguage' not in m or 'title' not in m: return m # if we found some language, make sure we didn't cut a title or sth... @@ -123,51 +181,10 @@ def _guess_filename(filename, filetype): opts=['nolanguage', 'nocountry']) m2 = mtree2.matched() - - if m.get('title') is None: - return m - if m.get('title') != m2.get('title'): title = next(find_nodes(mtree.match_tree, 'title')) title2 = next(find_nodes(mtree2.match_tree, 'title')) - langs = list(find_nodes(mtree.match_tree, ['language', 'subtitleLanguage'])) - if not langs: - return warning('A weird error happened with language detection') - - # find the language that is likely more relevant - for lng in langs: - if lng.value in title2.value: - # if the language was detected as part of a potential title, - # look at this one in particular - lang = lng - break - else: - # pick the first one if we don't have a better choice - lang = langs[0] - - - # language code are rarely part of a title, and those - # should be handled by the Language exceptions anyway - if len(lang.value) <= 3: - return m - - - # if filetype is subtitle and the language appears last, just before - # the extension, then it is likely a subtitle language - parts = clean_string(title.root.value).split() - if (m['type'] in ['moviesubtitle', 'episodesubtitle'] and - parts.index(lang.value) == len(parts) - 2): - return m - - # if the language was in the middle of the other potential title, - # keep the other title (eg: The Italian Job), except if it is at the - # very beginning, in which case we consider it an error - if m2['title'].startswith(lang.value): - return m - elif lang.value in title2.value: - return m2 - # if a node is in an explicit group, then the correct title is probably # the other one if title.root.node_at(title.node_idx[:2]).is_explicit(): @@ -175,9 +192,6 @@ def _guess_filename(filename, filetype): elif title2.root.node_at(title2.node_idx[:2]).is_explicit(): return m - return warning('Not sure of the title because of the language position') - - return m diff --git a/libs/guessit/__main__.py b/libs/guessit/__main__.py index 957ec9da..ccfa3af6 100755 --- a/libs/guessit/__main__.py +++ b/libs/guessit/__main__.py @@ -24,16 +24,19 @@ from guessit import u from guessit import slogging, guess_file_info from optparse import OptionParser import logging +import sys +import os +import locale -def detect_filename(filename, filetype, info=['filename']): +def detect_filename(filename, filetype, info=['filename'], advanced = False): filename = u(filename) print('For:', filename) - print('GuessIt found:', guess_file_info(filename, filetype, info).nice_string()) + print('GuessIt found:', guess_file_info(filename, filetype, info).nice_string(advanced)) -def run_demo(episodes=True, movies=True): +def run_demo(episodes=True, movies=True, advanced=False): # NOTE: tests should not be added here but rather in the tests/ folder # this is just intended as a quick example if episodes: @@ -50,7 +53,7 @@ def run_demo(episodes=True, movies=True): for f in testeps: print('-'*80) - detect_filename(f, filetype='episode') + detect_filename(f, filetype='episode', advanced=advanced) if movies: @@ -77,12 +80,17 @@ def run_demo(episodes=True, movies=True): for f in testmovies: print('-'*80) - detect_filename(f, filetype = 'movie') + detect_filename(f, filetype = 'movie', advanced = advanced) def main(): slogging.setupLogging() + # see http://bugs.python.org/issue2128 + if sys.version_info.major < 3 and os.name == 'nt': + for i, a in enumerate(sys.argv): + sys.argv[i] = a.decode(locale.getpreferredencoding()) + parser = OptionParser(usage = 'usage: %prog [options] file1 [file2...]') parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help = 'display debug output') @@ -92,6 +100,8 @@ def main(): 'them, comma-separated') parser.add_option('-t', '--type', dest = 'filetype', default = 'autodetect', help = 'the suggested file type: movie, episode or autodetect') + parser.add_option('-a', '--advanced', dest = 'advanced', action='store_true', default = False, + help = 'display advanced information for filename guesses, as json output') parser.add_option('-d', '--demo', action='store_true', dest='demo', default=False, help = 'run a few builtin tests instead of analyzing a file') @@ -100,13 +110,14 @@ def main(): logging.getLogger('guessit').setLevel(logging.DEBUG) if options.demo: - run_demo(episodes=True, movies=True) + run_demo(episodes=True, movies=True, advanced=options.advanced) else: if args: for filename in args: detect_filename(filename, filetype = options.filetype, - info = options.info.split(',')) + info = options.info.split(','), + advanced = options.advanced) else: parser.print_help() diff --git a/libs/guessit/fileutils.py b/libs/guessit/fileutils.py index dc077e64..9531f82a 100755 --- a/libs/guessit/fileutils.py +++ b/libs/guessit/fileutils.py @@ -44,13 +44,14 @@ def split_path(path): result = [] while True: head, tail = os.path.split(path) + headlen = len(head) # on Unix systems, the root folder is '/' - if head == '/' and tail == '': + if head and head == '/'*headlen and tail == '': return ['/'] + result # on Windows, the root folder is a drive letter (eg: 'C:\') or for shares \\ - if ((len(head) == 3 and head[1:] == ':\\') or (len(head) == 2 and head == '\\\\')) and tail == '': + if ((headlen == 3 and head[1:] == ':\\') or (headlen == 2 and head == '\\\\')) and tail == '': return [head] + result if head == '' and tail == '': @@ -61,6 +62,7 @@ def split_path(path): path = head continue + # otherwise, add the last path fragment and keep splitting result = [tail] + result path = head diff --git a/libs/guessit/guess.py b/libs/guessit/guess.py index 33d36517..73babceb 100755 --- a/libs/guessit/guess.py +++ b/libs/guessit/guess.py @@ -41,15 +41,21 @@ class Guess(UnicodeMixin, dict): confidence = kwargs.pop('confidence') except KeyError: confidence = 0 + + try: + raw = kwargs.pop('raw') + except KeyError: + raw = None dict.__init__(self, *args, **kwargs) self._confidence = {} + self._raw = {} for prop in self: self._confidence[prop] = confidence - - - def to_dict(self): + self._raw[prop] = raw + + def to_dict(self, advanced=False): data = dict(self) for prop, value in data.items(): if isinstance(value, datetime.date): @@ -58,46 +64,65 @@ class Guess(UnicodeMixin, dict): data[prop] = u(value) elif isinstance(value, list): data[prop] = [u(x) for x in value] + if advanced: + data[prop] = {"value": data[prop], "raw": self.raw(prop), "confidence": self.confidence(prop)} return data - def nice_string(self): - data = self.to_dict() - - parts = json.dumps(data, indent=4).split('\n') - for i, p in enumerate(parts): - if p[:5] != ' "': - continue - - prop = p.split('"')[1] - parts[i] = (' [%.2f] "' % self.confidence(prop)) + p[5:] - - return '\n'.join(parts) + def nice_string(self, advanced=False): + if advanced: + data = self.to_dict(advanced) + return json.dumps(data, indent=4) + else: + data = self.to_dict() + + parts = json.dumps(data, indent=4).split('\n') + for i, p in enumerate(parts): + if p[:5] != ' "': + continue + + prop = p.split('"')[1] + parts[i] = (' [%.2f] "' % self.confidence(prop)) + p[5:] + + return '\n'.join(parts) def __unicode__(self): return u(self.to_dict()) def confidence(self, prop): return self._confidence.get(prop, -1) + + def raw(self, prop): + return self._raw.get(prop, None) - def set(self, prop, value, confidence=None): + def set(self, prop, value, confidence=None, raw=None): self[prop] = value if confidence is not None: self._confidence[prop] = confidence + if raw is not None: + self._raw[prop] = raw def set_confidence(self, prop, value): self._confidence[prop] = value + + def set_raw(self, prop, value): + self._raw[prop] = value - def update(self, other, confidence=None): + def update(self, other, confidence=None, raw=None): dict.update(self, other) if isinstance(other, Guess): for prop in other: self._confidence[prop] = other.confidence(prop) + self._raw[prop] = other.raw(prop) if confidence is not None: for prop in other: self._confidence[prop] = confidence + if raw is not None: + for prop in other: + self._raw[prop] = raw + def update_highest_confidence(self, other): """Update this guess with the values from the given one. In case there is property present in both, only the one with the highest one @@ -110,6 +135,7 @@ class Guess(UnicodeMixin, dict): continue self[prop] = other[prop] self._confidence[prop] = other.confidence(prop) + self._raw[prop] = other.raw(prop) def choose_int(g1, g2): @@ -181,7 +207,7 @@ def choose_string(g1, g2): elif v1l in v2l: return (v1, combined_prob) - # in case of conflict, return the one with highest priority + # in case of conflict, return the one with highest confidence else: if c1 > c2: return (v1, c1 - c2) @@ -288,7 +314,8 @@ def merge_all(guesses, append=None): result.set(prop, result.get(prop, []) + [g[prop]], # TODO: what to do with confidence here? maybe an # arithmetic mean... - confidence=g.confidence(prop)) + confidence=g.confidence(prop), + raw=g.raw(prop)) del g[prop] diff --git a/libs/guessit/language.py b/libs/guessit/language.py index 2714c6e0..4d22cf05 100755 --- a/libs/guessit/language.py +++ b/libs/guessit/language.py @@ -296,7 +296,7 @@ UNDETERMINED = Language('und') ALL_LANGUAGES = frozenset(Language(lng) for lng in lng_all_names) - frozenset([UNDETERMINED]) ALL_LANGUAGES_NAMES = lng_all_names -def search_language(string, lang_filter=None): +def search_language(string, lang_filter=None, skip=None): """Looks for language patterns, and if found return the language object, its group span and an associated confidence. @@ -345,6 +345,16 @@ def search_language(string, lang_filter=None): if pos != -1: end = pos + len(lang) + + # skip if span in in skip list + while skip and (pos - 1, end - 1) in skip: + pos = slow.find(lang, end) + if pos == -1: + continue + end = pos + len(lang) + if pos == -1: + continue + # make sure our word is always surrounded by separators if slow[pos - 1] not in sep or slow[end] not in sep: continue diff --git a/libs/guessit/matcher.py b/libs/guessit/matcher.py index 43378192..1984c01c 100755 --- a/libs/guessit/matcher.py +++ b/libs/guessit/matcher.py @@ -21,14 +21,14 @@ from __future__ import unicode_literals from guessit import PY3, u, base_text_type from guessit.matchtree import MatchTree -from guessit.textutils import normalize_unicode +from guessit.textutils import normalize_unicode, clean_string import logging log = logging.getLogger(__name__) class IterativeMatcher(object): - def __init__(self, filename, filetype='autodetect', opts=None): + def __init__(self, filename, filetype='autodetect', opts=None, transfo_opts=None): """An iterative matcher tries to match different patterns that appear in the filename. @@ -38,7 +38,8 @@ class IterativeMatcher(object): a movie. The recognized 'filetype' values are: - [ autodetect, subtitle, movie, moviesubtitle, episode, episodesubtitle ] + [ autodetect, subtitle, info, movie, moviesubtitle, movieinfo, episode, + episodesubtitle, episodeinfo ] The IterativeMatcher works mainly in 2 steps: @@ -61,15 +62,20 @@ class IterativeMatcher(object): it corresponds to a video codec, denoted by the letter'v' in the 4th line. (for more info, see guess.matchtree.to_string) + Second, it tries to merge all this information into a single object + containing all the found properties, and does some (basic) conflict + resolution when they arise. - Second, it tries to merge all this information into a single object - containing all the found properties, and does some (basic) conflict - resolution when they arise. + + When you create the Matcher, you can pass it: + - a list 'opts' of option names, that act as global flags + - a dict 'transfo_opts' of { transfo_name: (transfo_args, transfo_kwargs) } + with which to call the transfo.process() function. """ - valid_filetypes = ('autodetect', 'subtitle', 'video', - 'movie', 'moviesubtitle', - 'episode', 'episodesubtitle') + valid_filetypes = ('autodetect', 'subtitle', 'info', 'video', + 'movie', 'moviesubtitle', 'movieinfo', + 'episode', 'episodesubtitle', 'episodeinfo') if filetype not in valid_filetypes: raise ValueError("filetype needs to be one of %s" % valid_filetypes) if not PY3 and not isinstance(filename, unicode): @@ -80,10 +86,22 @@ class IterativeMatcher(object): if opts is None: opts = [] - elif isinstance(opts, base_text_type): - opts = opts.split() + if not isinstance(opts, list): + raise ValueError('opts must be a list of option names! Received: type=%s val=%s', + type(opts), opts) + + if transfo_opts is None: + transfo_opts = {} + if not isinstance(transfo_opts, dict): + raise ValueError('transfo_opts must be a dict of { transfo_name: (args, kwargs) }. '+ + 'Received: type=%s val=%s', type(transfo_opts), transfo_opts) self.match_tree = MatchTree(filename) + + # sanity check: make sure we don't process a (mostly) empty string + if clean_string(filename) == '': + return + mtree = self.match_tree mtree.guess.set('type', filetype, confidence=1.0) @@ -91,7 +109,11 @@ class IterativeMatcher(object): transfo = __import__('guessit.transfo.' + transfo_name, globals=globals(), locals=locals(), fromlist=['process'], level=0) - transfo.process(mtree, *args, **kwargs) + default_args, default_kwargs = transfo_opts.get(transfo_name, ((), {})) + all_args = args or default_args + all_kwargs = dict(default_kwargs) + all_kwargs.update(kwargs) # keep all kwargs merged together + transfo.process(mtree, *all_args, **all_kwargs) # 1- first split our path into dirs + basename + ext apply_transfo('split_path_components') @@ -111,7 +133,7 @@ class IterativeMatcher(object): # - language before episodes_rexps # - properties before language (eg: he-aac vs hebrew) # - release_group before properties (eg: XviD-?? vs xvid) - if mtree.guess['type'] in ('episode', 'episodesubtitle'): + if mtree.guess['type'] in ('episode', 'episodesubtitle', 'episodeinfo'): strategy = [ 'guess_date', 'guess_website', 'guess_release_group', 'guess_properties', 'guess_language', 'guess_video_rexps', @@ -124,6 +146,7 @@ class IterativeMatcher(object): if 'nolanguage' in opts: strategy.remove('guess_language') + for name in strategy: apply_transfo(name) @@ -143,7 +166,7 @@ class IterativeMatcher(object): # 5- try to identify the remaining unknown groups by looking at their # position relative to other known elements - if mtree.guess['type'] in ('episode', 'episodesubtitle'): + if mtree.guess['type'] in ('episode', 'episodesubtitle', 'episodeinfo'): apply_transfo('guess_episode_info_from_position') else: apply_transfo('guess_movie_title_from_position') diff --git a/libs/guessit/patterns.py b/libs/guessit/patterns.py index ed3982b9..f803a11c 100755 --- a/libs/guessit/patterns.py +++ b/libs/guessit/patterns.py @@ -25,6 +25,8 @@ import re subtitle_exts = [ 'srt', 'idx', 'sub', 'ssa' ] +info_exts = [ 'nfo' ] + video_exts = ['3g2', '3gp', '3gp2', 'asf', 'avi', 'divx', 'flv', 'm4v', 'mk2', 'mka', 'mkv', 'mov', 'mp4', 'mp4a', 'mpeg', 'mpg', 'ogg', 'ogm', 'ogv', 'qt', 'ra', 'ram', 'rm', 'ts', 'wav', 'webm', 'wma', 'wmv'] @@ -32,7 +34,7 @@ video_exts = ['3g2', '3gp', '3gp2', 'asf', 'avi', 'divx', 'flv', 'm4v', 'mk2', group_delimiters = [ '()', '[]', '{}' ] # separator character regexp -sep = r'[][)(}{+ /\._-]' # regexp art, hehe :D +sep = r'[][,)(}{+ /\._-]' # regexp art, hehe :D # character used to represent a deleted char (when matching groups) deleted = '_' @@ -49,7 +51,7 @@ episode_rexps = [ # ... Season 2 ... #(r'[Ss](?P[0-9]{1,3})[^0-9]?(?P(?:-?[xX-][0-9]{1,3})+)[^0-9]', 1.0, (0, -1)), # ... 2x13 ... - (r'[^0-9](?P[0-9]{1,2})[^0-9]?(?P(?:-?[xX][0-9]{1,3})+)[^0-9]', 1.0, (1, -1)), + (r'[^0-9](?P[0-9]{1,2})[^0-9 .-]?(?P(?:-?[xX][0-9]{1,3})+)[^0-9]', 1.0, (1, -1)), # ... s02 ... #(sep + r's(?P[0-9]{1,2})' + sep, 0.6, (1, -1)), @@ -122,9 +124,12 @@ prop_multi = { 'format': { 'DVD': [ 'DVD', 'DVD-Rip', 'VIDEO-TS', 'DVDivX' ], 'VHS': [ 'VHS' ], 'WEB-DL': [ 'WEB-DL' ] }, + 'is3D': { True: [ '3D' ] }, + 'screenSize': { '480p': [ '480[pi]?' ], '720p': [ '720[pi]?' ], - '1080p': [ '1080[pi]?' ] }, + '1080i': [ '1080i' ], + '1080p': [ '1080p', '1080[^i]' ] }, 'videoCodec': { 'XviD': [ 'Xvid' ], 'DivX': [ 'DVDivX', 'DivX' ], @@ -140,7 +145,7 @@ prop_multi = { 'format': { 'DVD': [ 'DVD', 'DVD-Rip', 'VIDEO-TS', 'DVDivX' ], 'DTS': [ 'DTS' ], 'AAC': [ 'He-AAC', 'AAC-He', 'AAC' ] }, - 'audioChannels': { '5.1': [ r'5\.1', 'DD5[\._ ]1', '5ch' ] }, + 'audioChannels': { '5.1': [ r'5\.1', 'DD5[._ ]1', '5ch' ] }, 'episodeFormat': { 'Minisode': [ 'Minisodes?' ] } @@ -170,7 +175,7 @@ prop_single = { 'releaseGroup': [ 'ESiR', 'WAF', 'SEPTiC', r'\[XCT\]', 'iNT', 'P } _dash = '-' -_psep = '[-\. _]?' +_psep = '[-. _]?' def _to_rexp(prop): return re.compile(prop.replace(_dash, _psep), re.IGNORECASE) @@ -237,8 +242,9 @@ def canonical_form(string): def compute_canonical_form(property_name, value): """Return the canonical form of a property given its type if it is a valid one, None otherwise.""" - for canonical_form, rexps in properties_rexps[property_name].items(): - for rexp in rexps: - if rexp.match(value): - return canonical_form + if isinstance(value, basestring): + for canonical_form, rexps in properties_rexps[property_name].items(): + for rexp in rexps: + if rexp.match(value): + return canonical_form return None diff --git a/libs/guessit/slogging.py b/libs/guessit/slogging.py index 75e261cf..39591a20 100755 --- a/libs/guessit/slogging.py +++ b/libs/guessit/slogging.py @@ -31,14 +31,15 @@ RED_FONT = "\x1B[0;31m" RESET_FONT = "\x1B[0m" -def setupLogging(colored=True, with_time=False, with_thread=False, filename=None): +def setupLogging(colored=True, with_time=False, with_thread=False, filename=None, with_lineno=False): """Set up a nice colored logger as the main application logger.""" class SimpleFormatter(logging.Formatter): def __init__(self, with_time, with_thread): self.fmt = (('%(asctime)s ' if with_time else '') + '%(levelname)-8s ' + - '[%(name)s:%(funcName)s]' + + '[%(name)s:%(funcName)s' + + (':%(lineno)s' if with_lineno else '') + ']' + ('[%(threadName)s]' if with_thread else '') + ' -- %(message)s') logging.Formatter.__init__(self, self.fmt) @@ -47,7 +48,8 @@ def setupLogging(colored=True, with_time=False, with_thread=False, filename=None def __init__(self, with_time, with_thread): self.fmt = (('%(asctime)s ' if with_time else '') + '-CC-%(levelname)-8s ' + - BLUE_FONT + '[%(name)s:%(funcName)s]' + + BLUE_FONT + '[%(name)s:%(funcName)s' + + (':%(lineno)s' if with_lineno else '') + ']' + RESET_FONT + ('[%(threadName)s]' if with_thread else '') + ' -- %(message)s') diff --git a/libs/guessit/textutils.py b/libs/guessit/textutils.py index f195e2b7..ae9d28c3 100755 --- a/libs/guessit/textutils.py +++ b/libs/guessit/textutils.py @@ -43,10 +43,13 @@ def strip_brackets(s): return s -def clean_string(s): - for c in sep[:-2]: # do not remove dashes ('-') - s = s.replace(c, ' ') - parts = s.split() +def clean_string(st): + for c in sep: + # do not remove certain chars + if c in ['-', ',']: + continue + st = st.replace(c, ' ') + parts = st.split() result = ' '.join(p for p in parts if p != '') # now also remove dashes on the outer part of the string diff --git a/libs/guessit/transfo/__init__.py b/libs/guessit/transfo/__init__.py index 820690a7..a28aa988 100755 --- a/libs/guessit/transfo/__init__.py +++ b/libs/guessit/transfo/__init__.py @@ -28,7 +28,7 @@ log = logging.getLogger(__name__) def found_property(node, name, confidence): - node.guess = Guess({name: node.clean_value}, confidence=confidence) + node.guess = Guess({name: node.clean_value}, confidence=confidence, raw=node.value) log.debug('Found with confidence %.2f: %s' % (confidence, node.guess)) @@ -52,11 +52,17 @@ def format_guess(guess): def find_and_split_node(node, strategy, logger): string = ' %s ' % node.value # add sentinels - for matcher, confidence in strategy: + for matcher, confidence, args, kwargs in strategy: + all_args = [string] if getattr(matcher, 'use_node', False): - result, span = matcher(string, node) + all_args.append(node) + if args: + all_args.append(args) + + if kwargs: + result, span = matcher(*all_args, **kwargs) else: - result, span = matcher(string) + result, span = matcher(*all_args) if result: # readjust span to compensate for sentinels @@ -69,7 +75,7 @@ def find_and_split_node(node, strategy, logger): if confidence is None: confidence = 1.0 - guess = format_guess(Guess(result, confidence=confidence)) + guess = format_guess(Guess(result, confidence=confidence, raw=string[span[0] + 1:span[1] + 1])) msg = 'Found with confidence %.2f: %s' % (confidence, guess) (logger or log).debug(msg) @@ -84,10 +90,12 @@ def find_and_split_node(node, strategy, logger): class SingleNodeGuesser(object): - def __init__(self, guess_func, confidence, logger=None): + def __init__(self, guess_func, confidence, logger, *args, **kwargs): self.guess_func = guess_func self.confidence = confidence self.logger = logger + self.args = args + self.kwargs = kwargs def process(self, mtree): # strategy is a list of pairs (guesser, confidence) @@ -95,7 +103,7 @@ class SingleNodeGuesser(object): # it will override it, otherwise it will leave the guess confidence # - if the guesser returns a simple dict as a guess and confidence is # specified, it will use it, or 1.0 otherwise - strategy = [ (self.guess_func, self.confidence) ] + strategy = [ (self.guess_func, self.confidence, self.args, self.kwargs) ] for node in mtree.unidentified_leaves(): find_and_split_node(node, strategy, self.logger) diff --git a/libs/guessit/transfo/guess_country.py b/libs/guessit/transfo/guess_country.py index 1d690698..aadb84f7 100755 --- a/libs/guessit/transfo/guess_country.py +++ b/libs/guessit/transfo/guess_country.py @@ -45,4 +45,4 @@ def process(mtree): except ValueError: continue - node.guess = Guess(country=country, confidence=1.0) + node.guess = Guess(country=country, confidence=1.0, raw=c) diff --git a/libs/guessit/transfo/guess_episodes_rexps.py b/libs/guessit/transfo/guess_episodes_rexps.py index 29562be2..30c2ca2f 100755 --- a/libs/guessit/transfo/guess_episodes_rexps.py +++ b/libs/guessit/transfo/guess_episodes_rexps.py @@ -40,27 +40,22 @@ def guess_episodes_rexps(string): for rexp, confidence, span_adjust in episode_rexps: match = re.search(rexp, string, re.IGNORECASE) if match: - guess = Guess(match.groupdict(), confidence=confidence) - span = (match.start() + span_adjust[0], + span = (match.start() + span_adjust[0], match.end() + span_adjust[1]) - - # episodes which have a season > 30 are most likely errors - # (Simpsons is at 24!) - if int(guess.get('season', 0)) > 30: - continue + guess = Guess(match.groupdict(), confidence=confidence, raw=string[span[0]:span[1]]) # decide whether we have only a single episode number or an # episode list if guess.get('episodeNumber'): eplist = number_list(guess['episodeNumber']) - guess.set('episodeNumber', eplist[0], confidence=confidence) + guess.set('episodeNumber', eplist[0], confidence=confidence, raw=string[span[0]:span[1]]) if len(eplist) > 1: - guess.set('episodeList', eplist, confidence=confidence) + guess.set('episodeList', eplist, confidence=confidence, raw=string[span[0]:span[1]]) if guess.get('bonusNumber'): eplist = number_list(guess['bonusNumber']) - guess.set('bonusNumber', eplist[0], confidence=confidence) + guess.set('bonusNumber', eplist[0], confidence=confidence, raw=string[span[0]:span[1]]) return guess, span diff --git a/libs/guessit/transfo/guess_filetype.py b/libs/guessit/transfo/guess_filetype.py index 4d98d016..4279c0b0 100755 --- a/libs/guessit/transfo/guess_filetype.py +++ b/libs/guessit/transfo/guess_filetype.py @@ -20,7 +20,7 @@ from __future__ import unicode_literals from guessit import Guess -from guessit.patterns import (subtitle_exts, video_exts, episode_rexps, +from guessit.patterns import (subtitle_exts, info_exts, video_exts, episode_rexps, find_properties, compute_canonical_form) from guessit.date import valid_year from guessit.textutils import clean_string @@ -53,12 +53,16 @@ def guess_filetype(mtree, filetype): filetype_container[0] = 'episode' elif filetype_container[0] == 'subtitle': filetype_container[0] = 'episodesubtitle' + elif filetype_container[0] == 'info': + filetype_container[0] = 'episodeinfo' def upgrade_movie(): if filetype_container[0] == 'video': filetype_container[0] = 'movie' elif filetype_container[0] == 'subtitle': filetype_container[0] = 'moviesubtitle' + elif filetype_container[0] == 'info': + filetype_container[0] = 'movieinfo' def upgrade_subtitle(): if 'movie' in filetype_container[0]: @@ -68,6 +72,14 @@ def guess_filetype(mtree, filetype): else: filetype_container[0] = 'subtitle' + def upgrade_info(): + if 'movie' in filetype_container[0]: + filetype_container[0] = 'movieinfo' + elif 'episode' in filetype_container[0]: + filetype_container[0] = 'episodeinfo' + else: + filetype_container[0] = 'info' + def upgrade(type='unknown'): if filetype_container[0] == 'autodetect': filetype_container[0] = type @@ -78,6 +90,9 @@ def guess_filetype(mtree, filetype): if fileext in subtitle_exts: upgrade_subtitle() other = { 'container': fileext } + elif fileext in info_exts: + upgrade_info() + other = { 'container': fileext } elif fileext in video_exts: upgrade(type='video') other = { 'container': fileext } @@ -104,17 +119,20 @@ def guess_filetype(mtree, filetype): fname = clean_string(filename).lower() for m in MOVIES: if m in fname: + log.debug('Found in exception list of movies -> type = movie') upgrade_movie() for s in SERIES: if s in fname: + log.debug('Found in exception list of series -> type = episode') upgrade_episode() # now look whether there are some specific hints for episode vs movie - if filetype_container[0] in ('video', 'subtitle'): + if filetype_container[0] in ('video', 'subtitle', 'info'): # if we have an episode_rexp (eg: s02e13), it is an episode for rexp, _, _ in episode_rexps: match = re.search(rexp, filename, re.IGNORECASE) if match: + log.debug('Found matching regexp: "%s" (string = "%s") -> type = episode', rexp, match.group()) upgrade_episode() break @@ -133,24 +151,29 @@ def guess_filetype(mtree, filetype): possible = False if possible: + log.debug('Found possible episode number: %s (from string "%s") -> type = episode', epnumber, match.group()) upgrade_episode() # if we have certain properties characteristic of episodes, it is an ep for prop, value, _, _ in find_properties(filename): log.debug('prop: %s = %s' % (prop, value)) if prop == 'episodeFormat': + log.debug('Found characteristic property of episodes: %s = "%s"', prop, value) upgrade_episode() break elif compute_canonical_form('format', value) == 'DVB': + log.debug('Found characteristic property of episodes: %s = "%s"', prop, value) upgrade_episode() break # origin-specific type if 'tvu.org.ru' in filename: + log.debug('Found characteristic property of episodes: %s = "%s"', prop, value) upgrade_episode() # if no episode info found, assume it's a movie + log.debug('Nothing characteristic found, assuming type = movie') upgrade_movie() filetype = filetype_container[0] diff --git a/libs/guessit/transfo/guess_language.py b/libs/guessit/transfo/guess_language.py index 86c1cf55..648a06b1 100755 --- a/libs/guessit/transfo/guess_language.py +++ b/libs/guessit/transfo/guess_language.py @@ -22,22 +22,34 @@ from __future__ import unicode_literals from guessit import Guess from guessit.transfo import SingleNodeGuesser from guessit.language import search_language -from guessit.textutils import clean_string, find_words import logging log = logging.getLogger(__name__) -def guess_language(string): - language, span, confidence = search_language(string) +def guess_language(string, node, skip=None): + if skip: + relative_skip = [] + for entry in skip: + node_idx = entry['node_idx'] + span = entry['span'] + if node_idx == node.node_idx[:len(node_idx)]: + relative_span = (span[0] - node.offset + 1, span[1] - node.offset + 1) + relative_skip.append(relative_span) + skip = relative_skip + + language, span, confidence = search_language(string, skip=skip) if language: return (Guess({'language': language}, - confidence=confidence), + confidence=confidence, + raw= string[span[0]:span[1]]), span) return None, None +guess_language.use_node = True -def process(mtree): - SingleNodeGuesser(guess_language, None, log).process(mtree) + +def process(mtree, *args, **kwargs): + SingleNodeGuesser(guess_language, None, log, *args, **kwargs).process(mtree) # Note: 'language' is promoted to 'subtitleLanguage' in the post_process transfo diff --git a/libs/guessit/transfo/guess_movie_title_from_position.py b/libs/guessit/transfo/guess_movie_title_from_position.py index d2e2deb2..bcb42b45 100755 --- a/libs/guessit/transfo/guess_movie_title_from_position.py +++ b/libs/guessit/transfo/guess_movie_title_from_position.py @@ -29,7 +29,8 @@ log = logging.getLogger(__name__) def process(mtree): def found_property(node, name, value, confidence): node.guess = Guess({ name: value }, - confidence=confidence) + confidence=confidence, + raw=value) log.debug('Found with confidence %.2f: %s' % (confidence, node.guess)) def found_title(node, confidence): diff --git a/libs/guessit/transfo/guess_video_rexps.py b/libs/guessit/transfo/guess_video_rexps.py index 8ae9e6c6..1b511f15 100755 --- a/libs/guessit/transfo/guess_video_rexps.py +++ b/libs/guessit/transfo/guess_video_rexps.py @@ -38,9 +38,10 @@ def guess_video_rexps(string): # the soonest that we can catch it) if metadata.get('cdNumberTotal', -1) is None: del metadata['cdNumberTotal'] - return (Guess(metadata, confidence=confidence), - (match.start() + span_adjust[0], - match.end() + span_adjust[1] - 2)) + span = (match.start() + span_adjust[0], + match.end() + span_adjust[1] - 2) + return (Guess(metadata, confidence=confidence, raw=string[span[0]:span[1]]), + span) return None, None diff --git a/libs/guessit/transfo/guess_weak_episodes_rexps.py b/libs/guessit/transfo/guess_weak_episodes_rexps.py index 8436ade8..18306b43 100755 --- a/libs/guessit/transfo/guess_weak_episodes_rexps.py +++ b/libs/guessit/transfo/guess_weak_episodes_rexps.py @@ -48,9 +48,9 @@ def guess_weak_episodes_rexps(string, node): continue return Guess({ 'season': season, 'episodeNumber': epnum }, - confidence=0.6), span + confidence=0.6, raw=string[span[0]:span[1]]), span else: - return Guess(metadata, confidence=0.3), span + return Guess(metadata, confidence=0.3, raw=string[span[0]:span[1]]), span return None, None diff --git a/libs/html5lib/__init__.py b/libs/html5lib/__init__.py index 16537aad..66c1a8eb 100644 --- a/libs/html5lib/__init__.py +++ b/libs/html5lib/__init__.py @@ -1,4 +1,4 @@ -""" +""" HTML parsing library based on the WHATWG "HTML5" specification. The parser is designed to be compatible with existing HTML found in the wild and implements well-defined error recovery that @@ -8,10 +8,16 @@ Example usage: import html5lib f = open("my_document.html") -tree = html5lib.parse(f) +tree = html5lib.parse(f) """ -__version__ = "0.95-dev" -from html5parser import HTMLParser, parse, parseFragment -from treebuilders import getTreeBuilder -from treewalkers import getTreeWalker -from serializer import serialize + +from __future__ import absolute_import, division, unicode_literals + +from .html5parser import HTMLParser, parse, parseFragment +from .treebuilders import getTreeBuilder +from .treewalkers import getTreeWalker +from .serializer import serialize + +__all__ = ["HTMLParser", "parse", "parseFragment", "getTreeBuilder", + "getTreeWalker", "serialize"] +__version__ = "0.99" diff --git a/libs/html5lib/constants.py b/libs/html5lib/constants.py index b533018e..e7089846 100644 --- a/libs/html5lib/constants.py +++ b/libs/html5lib/constants.py @@ -1,302 +1,301 @@ -import string, gettext -_ = gettext.gettext +from __future__ import absolute_import, division, unicode_literals -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +import string +import gettext +_ = gettext.gettext EOF = None E = { - "null-character": - _(u"Null character in input stream, replaced with U+FFFD."), - "invalid-codepoint": - _(u"Invalid codepoint in stream."), + "null-character": + _("Null character in input stream, replaced with U+FFFD."), + "invalid-codepoint": + _("Invalid codepoint in stream."), "incorrectly-placed-solidus": - _(u"Solidus (/) incorrectly placed in tag."), + _("Solidus (/) incorrectly placed in tag."), "incorrect-cr-newline-entity": - _(u"Incorrect CR newline entity, replaced with LF."), + _("Incorrect CR newline entity, replaced with LF."), "illegal-windows-1252-entity": - _(u"Entity used with illegal number (windows-1252 reference)."), + _("Entity used with illegal number (windows-1252 reference)."), "cant-convert-numeric-entity": - _(u"Numeric entity couldn't be converted to character " - u"(codepoint U+%(charAsInt)08x)."), + _("Numeric entity couldn't be converted to character " + "(codepoint U+%(charAsInt)08x)."), "illegal-codepoint-for-numeric-entity": - _(u"Numeric entity represents an illegal codepoint: " - u"U+%(charAsInt)08x."), + _("Numeric entity represents an illegal codepoint: " + "U+%(charAsInt)08x."), "numeric-entity-without-semicolon": - _(u"Numeric entity didn't end with ';'."), + _("Numeric entity didn't end with ';'."), "expected-numeric-entity-but-got-eof": - _(u"Numeric entity expected. Got end of file instead."), + _("Numeric entity expected. Got end of file instead."), "expected-numeric-entity": - _(u"Numeric entity expected but none found."), + _("Numeric entity expected but none found."), "named-entity-without-semicolon": - _(u"Named entity didn't end with ';'."), + _("Named entity didn't end with ';'."), "expected-named-entity": - _(u"Named entity expected. Got none."), + _("Named entity expected. Got none."), "attributes-in-end-tag": - _(u"End tag contains unexpected attributes."), + _("End tag contains unexpected attributes."), 'self-closing-flag-on-end-tag': - _(u"End tag contains unexpected self-closing flag."), + _("End tag contains unexpected self-closing flag."), "expected-tag-name-but-got-right-bracket": - _(u"Expected tag name. Got '>' instead."), + _("Expected tag name. Got '>' instead."), "expected-tag-name-but-got-question-mark": - _(u"Expected tag name. Got '?' instead. (HTML doesn't " - u"support processing instructions.)"), + _("Expected tag name. Got '?' instead. (HTML doesn't " + "support processing instructions.)"), "expected-tag-name": - _(u"Expected tag name. Got something else instead"), + _("Expected tag name. Got something else instead"), "expected-closing-tag-but-got-right-bracket": - _(u"Expected closing tag. Got '>' instead. Ignoring '>'."), + _("Expected closing tag. Got '>' instead. Ignoring '>'."), "expected-closing-tag-but-got-eof": - _(u"Expected closing tag. Unexpected end of file."), + _("Expected closing tag. Unexpected end of file."), "expected-closing-tag-but-got-char": - _(u"Expected closing tag. Unexpected character '%(data)s' found."), + _("Expected closing tag. Unexpected character '%(data)s' found."), "eof-in-tag-name": - _(u"Unexpected end of file in the tag name."), + _("Unexpected end of file in the tag name."), "expected-attribute-name-but-got-eof": - _(u"Unexpected end of file. Expected attribute name instead."), + _("Unexpected end of file. Expected attribute name instead."), "eof-in-attribute-name": - _(u"Unexpected end of file in attribute name."), + _("Unexpected end of file in attribute name."), "invalid-character-in-attribute-name": - _(u"Invalid chracter in attribute name"), + _("Invalid character in attribute name"), "duplicate-attribute": - _(u"Dropped duplicate attribute on tag."), + _("Dropped duplicate attribute on tag."), "expected-end-of-tag-name-but-got-eof": - _(u"Unexpected end of file. Expected = or end of tag."), + _("Unexpected end of file. Expected = or end of tag."), "expected-attribute-value-but-got-eof": - _(u"Unexpected end of file. Expected attribute value."), + _("Unexpected end of file. Expected attribute value."), "expected-attribute-value-but-got-right-bracket": - _(u"Expected attribute value. Got '>' instead."), + _("Expected attribute value. Got '>' instead."), 'equals-in-unquoted-attribute-value': - _(u"Unexpected = in unquoted attribute"), + _("Unexpected = in unquoted attribute"), 'unexpected-character-in-unquoted-attribute-value': - _(u"Unexpected character in unquoted attribute"), + _("Unexpected character in unquoted attribute"), "invalid-character-after-attribute-name": - _(u"Unexpected character after attribute name."), + _("Unexpected character after attribute name."), "unexpected-character-after-attribute-value": - _(u"Unexpected character after attribute value."), + _("Unexpected character after attribute value."), "eof-in-attribute-value-double-quote": - _(u"Unexpected end of file in attribute value (\")."), + _("Unexpected end of file in attribute value (\")."), "eof-in-attribute-value-single-quote": - _(u"Unexpected end of file in attribute value (')."), + _("Unexpected end of file in attribute value (')."), "eof-in-attribute-value-no-quotes": - _(u"Unexpected end of file in attribute value."), + _("Unexpected end of file in attribute value."), "unexpected-EOF-after-solidus-in-tag": - _(u"Unexpected end of file in tag. Expected >"), - "unexpected-character-after-soldius-in-tag": - _(u"Unexpected character after / in tag. Expected >"), + _("Unexpected end of file in tag. Expected >"), + "unexpected-character-after-solidus-in-tag": + _("Unexpected character after / in tag. Expected >"), "expected-dashes-or-doctype": - _(u"Expected '--' or 'DOCTYPE'. Not found."), + _("Expected '--' or 'DOCTYPE'. Not found."), "unexpected-bang-after-double-dash-in-comment": - _(u"Unexpected ! after -- in comment"), + _("Unexpected ! after -- in comment"), "unexpected-space-after-double-dash-in-comment": - _(u"Unexpected space after -- in comment"), + _("Unexpected space after -- in comment"), "incorrect-comment": - _(u"Incorrect comment."), + _("Incorrect comment."), "eof-in-comment": - _(u"Unexpected end of file in comment."), + _("Unexpected end of file in comment."), "eof-in-comment-end-dash": - _(u"Unexpected end of file in comment (-)"), + _("Unexpected end of file in comment (-)"), "unexpected-dash-after-double-dash-in-comment": - _(u"Unexpected '-' after '--' found in comment."), + _("Unexpected '-' after '--' found in comment."), "eof-in-comment-double-dash": - _(u"Unexpected end of file in comment (--)."), + _("Unexpected end of file in comment (--)."), "eof-in-comment-end-space-state": - _(u"Unexpected end of file in comment."), + _("Unexpected end of file in comment."), "eof-in-comment-end-bang-state": - _(u"Unexpected end of file in comment."), + _("Unexpected end of file in comment."), "unexpected-char-in-comment": - _(u"Unexpected character in comment found."), + _("Unexpected character in comment found."), "need-space-after-doctype": - _(u"No space after literal string 'DOCTYPE'."), + _("No space after literal string 'DOCTYPE'."), "expected-doctype-name-but-got-right-bracket": - _(u"Unexpected > character. Expected DOCTYPE name."), + _("Unexpected > character. Expected DOCTYPE name."), "expected-doctype-name-but-got-eof": - _(u"Unexpected end of file. Expected DOCTYPE name."), + _("Unexpected end of file. Expected DOCTYPE name."), "eof-in-doctype-name": - _(u"Unexpected end of file in DOCTYPE name."), + _("Unexpected end of file in DOCTYPE name."), "eof-in-doctype": - _(u"Unexpected end of file in DOCTYPE."), + _("Unexpected end of file in DOCTYPE."), "expected-space-or-right-bracket-in-doctype": - _(u"Expected space or '>'. Got '%(data)s'"), + _("Expected space or '>'. Got '%(data)s'"), "unexpected-end-of-doctype": - _(u"Unexpected end of DOCTYPE."), + _("Unexpected end of DOCTYPE."), "unexpected-char-in-doctype": - _(u"Unexpected character in DOCTYPE."), + _("Unexpected character in DOCTYPE."), "eof-in-innerhtml": - _(u"XXX innerHTML EOF"), + _("XXX innerHTML EOF"), "unexpected-doctype": - _(u"Unexpected DOCTYPE. Ignored."), + _("Unexpected DOCTYPE. Ignored."), "non-html-root": - _(u"html needs to be the first start tag."), + _("html needs to be the first start tag."), "expected-doctype-but-got-eof": - _(u"Unexpected End of file. Expected DOCTYPE."), + _("Unexpected End of file. Expected DOCTYPE."), "unknown-doctype": - _(u"Erroneous DOCTYPE."), + _("Erroneous DOCTYPE."), "expected-doctype-but-got-chars": - _(u"Unexpected non-space characters. Expected DOCTYPE."), + _("Unexpected non-space characters. Expected DOCTYPE."), "expected-doctype-but-got-start-tag": - _(u"Unexpected start tag (%(name)s). Expected DOCTYPE."), + _("Unexpected start tag (%(name)s). Expected DOCTYPE."), "expected-doctype-but-got-end-tag": - _(u"Unexpected end tag (%(name)s). Expected DOCTYPE."), + _("Unexpected end tag (%(name)s). Expected DOCTYPE."), "end-tag-after-implied-root": - _(u"Unexpected end tag (%(name)s) after the (implied) root element."), + _("Unexpected end tag (%(name)s) after the (implied) root element."), "expected-named-closing-tag-but-got-eof": - _(u"Unexpected end of file. Expected end tag (%(name)s)."), + _("Unexpected end of file. Expected end tag (%(name)s)."), "two-heads-are-not-better-than-one": - _(u"Unexpected start tag head in existing head. Ignored."), + _("Unexpected start tag head in existing head. Ignored."), "unexpected-end-tag": - _(u"Unexpected end tag (%(name)s). Ignored."), + _("Unexpected end tag (%(name)s). Ignored."), "unexpected-start-tag-out-of-my-head": - _(u"Unexpected start tag (%(name)s) that can be in head. Moved."), + _("Unexpected start tag (%(name)s) that can be in head. Moved."), "unexpected-start-tag": - _(u"Unexpected start tag (%(name)s)."), + _("Unexpected start tag (%(name)s)."), "missing-end-tag": - _(u"Missing end tag (%(name)s)."), + _("Missing end tag (%(name)s)."), "missing-end-tags": - _(u"Missing end tags (%(name)s)."), + _("Missing end tags (%(name)s)."), "unexpected-start-tag-implies-end-tag": - _(u"Unexpected start tag (%(startName)s) " - u"implies end tag (%(endName)s)."), + _("Unexpected start tag (%(startName)s) " + "implies end tag (%(endName)s)."), "unexpected-start-tag-treated-as": - _(u"Unexpected start tag (%(originalName)s). Treated as %(newName)s."), + _("Unexpected start tag (%(originalName)s). Treated as %(newName)s."), "deprecated-tag": - _(u"Unexpected start tag %(name)s. Don't use it!"), + _("Unexpected start tag %(name)s. Don't use it!"), "unexpected-start-tag-ignored": - _(u"Unexpected start tag %(name)s. Ignored."), + _("Unexpected start tag %(name)s. Ignored."), "expected-one-end-tag-but-got-another": - _(u"Unexpected end tag (%(gotName)s). " - u"Missing end tag (%(expectedName)s)."), + _("Unexpected end tag (%(gotName)s). " + "Missing end tag (%(expectedName)s)."), "end-tag-too-early": - _(u"End tag (%(name)s) seen too early. Expected other end tag."), + _("End tag (%(name)s) seen too early. Expected other end tag."), "end-tag-too-early-named": - _(u"Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s)."), + _("Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s)."), "end-tag-too-early-ignored": - _(u"End tag (%(name)s) seen too early. Ignored."), + _("End tag (%(name)s) seen too early. Ignored."), "adoption-agency-1.1": - _(u"End tag (%(name)s) violates step 1, " - u"paragraph 1 of the adoption agency algorithm."), + _("End tag (%(name)s) violates step 1, " + "paragraph 1 of the adoption agency algorithm."), "adoption-agency-1.2": - _(u"End tag (%(name)s) violates step 1, " - u"paragraph 2 of the adoption agency algorithm."), + _("End tag (%(name)s) violates step 1, " + "paragraph 2 of the adoption agency algorithm."), "adoption-agency-1.3": - _(u"End tag (%(name)s) violates step 1, " - u"paragraph 3 of the adoption agency algorithm."), + _("End tag (%(name)s) violates step 1, " + "paragraph 3 of the adoption agency algorithm."), + "adoption-agency-4.4": + _("End tag (%(name)s) violates step 4, " + "paragraph 4 of the adoption agency algorithm."), "unexpected-end-tag-treated-as": - _(u"Unexpected end tag (%(originalName)s). Treated as %(newName)s."), + _("Unexpected end tag (%(originalName)s). Treated as %(newName)s."), "no-end-tag": - _(u"This element (%(name)s) has no end tag."), + _("This element (%(name)s) has no end tag."), "unexpected-implied-end-tag-in-table": - _(u"Unexpected implied end tag (%(name)s) in the table phase."), + _("Unexpected implied end tag (%(name)s) in the table phase."), "unexpected-implied-end-tag-in-table-body": - _(u"Unexpected implied end tag (%(name)s) in the table body phase."), + _("Unexpected implied end tag (%(name)s) in the table body phase."), "unexpected-char-implies-table-voodoo": - _(u"Unexpected non-space characters in " - u"table context caused voodoo mode."), + _("Unexpected non-space characters in " + "table context caused voodoo mode."), "unexpected-hidden-input-in-table": - _(u"Unexpected input with type hidden in table context."), + _("Unexpected input with type hidden in table context."), "unexpected-form-in-table": - _(u"Unexpected form in table context."), + _("Unexpected form in table context."), "unexpected-start-tag-implies-table-voodoo": - _(u"Unexpected start tag (%(name)s) in " - u"table context caused voodoo mode."), + _("Unexpected start tag (%(name)s) in " + "table context caused voodoo mode."), "unexpected-end-tag-implies-table-voodoo": - _(u"Unexpected end tag (%(name)s) in " - u"table context caused voodoo mode."), + _("Unexpected end tag (%(name)s) in " + "table context caused voodoo mode."), "unexpected-cell-in-table-body": - _(u"Unexpected table cell start tag (%(name)s) " - u"in the table body phase."), + _("Unexpected table cell start tag (%(name)s) " + "in the table body phase."), "unexpected-cell-end-tag": - _(u"Got table cell end tag (%(name)s) " - u"while required end tags are missing."), + _("Got table cell end tag (%(name)s) " + "while required end tags are missing."), "unexpected-end-tag-in-table-body": - _(u"Unexpected end tag (%(name)s) in the table body phase. Ignored."), + _("Unexpected end tag (%(name)s) in the table body phase. Ignored."), "unexpected-implied-end-tag-in-table-row": - _(u"Unexpected implied end tag (%(name)s) in the table row phase."), + _("Unexpected implied end tag (%(name)s) in the table row phase."), "unexpected-end-tag-in-table-row": - _(u"Unexpected end tag (%(name)s) in the table row phase. Ignored."), + _("Unexpected end tag (%(name)s) in the table row phase. Ignored."), "unexpected-select-in-select": - _(u"Unexpected select start tag in the select phase " - u"treated as select end tag."), + _("Unexpected select start tag in the select phase " + "treated as select end tag."), "unexpected-input-in-select": - _(u"Unexpected input start tag in the select phase."), + _("Unexpected input start tag in the select phase."), "unexpected-start-tag-in-select": - _(u"Unexpected start tag token (%(name)s in the select phase. " - u"Ignored."), + _("Unexpected start tag token (%(name)s in the select phase. " + "Ignored."), "unexpected-end-tag-in-select": - _(u"Unexpected end tag (%(name)s) in the select phase. Ignored."), + _("Unexpected end tag (%(name)s) in the select phase. Ignored."), "unexpected-table-element-start-tag-in-select-in-table": - _(u"Unexpected table element start tag (%(name)s) in the select in table phase."), + _("Unexpected table element start tag (%(name)s) in the select in table phase."), "unexpected-table-element-end-tag-in-select-in-table": - _(u"Unexpected table element end tag (%(name)s) in the select in table phase."), + _("Unexpected table element end tag (%(name)s) in the select in table phase."), "unexpected-char-after-body": - _(u"Unexpected non-space characters in the after body phase."), + _("Unexpected non-space characters in the after body phase."), "unexpected-start-tag-after-body": - _(u"Unexpected start tag token (%(name)s)" - u" in the after body phase."), + _("Unexpected start tag token (%(name)s)" + " in the after body phase."), "unexpected-end-tag-after-body": - _(u"Unexpected end tag token (%(name)s)" - u" in the after body phase."), + _("Unexpected end tag token (%(name)s)" + " in the after body phase."), "unexpected-char-in-frameset": - _(u"Unepxected characters in the frameset phase. Characters ignored."), + _("Unexpected characters in the frameset phase. Characters ignored."), "unexpected-start-tag-in-frameset": - _(u"Unexpected start tag token (%(name)s)" - u" in the frameset phase. Ignored."), + _("Unexpected start tag token (%(name)s)" + " in the frameset phase. Ignored."), "unexpected-frameset-in-frameset-innerhtml": - _(u"Unexpected end tag token (frameset) " - u"in the frameset phase (innerHTML)."), + _("Unexpected end tag token (frameset) " + "in the frameset phase (innerHTML)."), "unexpected-end-tag-in-frameset": - _(u"Unexpected end tag token (%(name)s)" - u" in the frameset phase. Ignored."), + _("Unexpected end tag token (%(name)s)" + " in the frameset phase. Ignored."), "unexpected-char-after-frameset": - _(u"Unexpected non-space characters in the " - u"after frameset phase. Ignored."), + _("Unexpected non-space characters in the " + "after frameset phase. Ignored."), "unexpected-start-tag-after-frameset": - _(u"Unexpected start tag (%(name)s)" - u" in the after frameset phase. Ignored."), + _("Unexpected start tag (%(name)s)" + " in the after frameset phase. Ignored."), "unexpected-end-tag-after-frameset": - _(u"Unexpected end tag (%(name)s)" - u" in the after frameset phase. Ignored."), + _("Unexpected end tag (%(name)s)" + " in the after frameset phase. Ignored."), "unexpected-end-tag-after-body-innerhtml": - _(u"Unexpected end tag after body(innerHtml)"), + _("Unexpected end tag after body(innerHtml)"), "expected-eof-but-got-char": - _(u"Unexpected non-space characters. Expected end of file."), + _("Unexpected non-space characters. Expected end of file."), "expected-eof-but-got-start-tag": - _(u"Unexpected start tag (%(name)s)" - u". Expected end of file."), + _("Unexpected start tag (%(name)s)" + ". Expected end of file."), "expected-eof-but-got-end-tag": - _(u"Unexpected end tag (%(name)s)" - u". Expected end of file."), + _("Unexpected end tag (%(name)s)" + ". Expected end of file."), "eof-in-table": - _(u"Unexpected end of file. Expected table content."), + _("Unexpected end of file. Expected table content."), "eof-in-select": - _(u"Unexpected end of file. Expected select content."), + _("Unexpected end of file. Expected select content."), "eof-in-frameset": - _(u"Unexpected end of file. Expected frameset content."), + _("Unexpected end of file. Expected frameset content."), "eof-in-script-in-script": - _(u"Unexpected end of file. Expected script content."), + _("Unexpected end of file. Expected script content."), "eof-in-foreign-lands": - _(u"Unexpected end of file. Expected foreign content"), + _("Unexpected end of file. Expected foreign content"), "non-void-element-with-trailing-solidus": - _(u"Trailing solidus not allowed on element %(name)s"), + _("Trailing solidus not allowed on element %(name)s"), "unexpected-html-element-in-foreign-content": - _(u"Element %(name)s not allowed in a non-html context"), + _("Element %(name)s not allowed in a non-html context"), "unexpected-end-tag-before-html": - _(u"Unexpected end tag (%(name)s) before html."), + _("Unexpected end tag (%(name)s) before html."), "XXX-undefined-error": - (u"Undefined error (this sucks and should be fixed)"), + _("Undefined error (this sucks and should be fixed)"), } namespaces = { - "html":"http://www.w3.org/1999/xhtml", - "mathml":"http://www.w3.org/1998/Math/MathML", - "svg":"http://www.w3.org/2000/svg", - "xlink":"http://www.w3.org/1999/xlink", - "xml":"http://www.w3.org/XML/1998/namespace", - "xmlns":"http://www.w3.org/2000/xmlns/" + "html": "http://www.w3.org/1999/xhtml", + "mathml": "http://www.w3.org/1998/Math/MathML", + "svg": "http://www.w3.org/2000/svg", + "xlink": "http://www.w3.org/1999/xlink", + "xml": "http://www.w3.org/XML/1998/namespace", + "xmlns": "http://www.w3.org/2000/xmlns/" } scopingElements = frozenset(( @@ -380,7 +379,7 @@ specialElements = frozenset(( (namespaces["html"], "iframe"), # Note that image is commented out in the spec as "this isn't an # element that can end up on the stack, so it doesn't matter," - (namespaces["html"], "image"), + (namespaces["html"], "image"), (namespaces["html"], "img"), (namespaces["html"], "input"), (namespaces["html"], "isindex"), @@ -434,12 +433,30 @@ mathmlTextIntegrationPointElements = frozenset(( (namespaces["mathml"], "mtext") )) +adjustForeignAttributes = { + "xlink:actuate": ("xlink", "actuate", namespaces["xlink"]), + "xlink:arcrole": ("xlink", "arcrole", namespaces["xlink"]), + "xlink:href": ("xlink", "href", namespaces["xlink"]), + "xlink:role": ("xlink", "role", namespaces["xlink"]), + "xlink:show": ("xlink", "show", namespaces["xlink"]), + "xlink:title": ("xlink", "title", namespaces["xlink"]), + "xlink:type": ("xlink", "type", namespaces["xlink"]), + "xml:base": ("xml", "base", namespaces["xml"]), + "xml:lang": ("xml", "lang", namespaces["xml"]), + "xml:space": ("xml", "space", namespaces["xml"]), + "xmlns": (None, "xmlns", namespaces["xmlns"]), + "xmlns:xlink": ("xmlns", "xlink", namespaces["xmlns"]) +} + +unadjustForeignAttributes = dict([((ns, local), qname) for qname, (prefix, local, ns) in + adjustForeignAttributes.items()]) + spaceCharacters = frozenset(( - u"\t", - u"\n", - u"\u000C", - u" ", - u"\r" + "\t", + "\n", + "\u000C", + " ", + "\r" )) tableInsertModeElements = frozenset(( @@ -456,8 +473,8 @@ asciiLetters = frozenset(string.ascii_letters) digits = frozenset(string.digits) hexDigits = frozenset(string.hexdigits) -asciiUpper2Lower = dict([(ord(c),ord(c.lower())) - for c in string.ascii_uppercase]) +asciiUpper2Lower = dict([(ord(c), ord(c.lower())) + for c in string.ascii_uppercase]) # Heading elements need to be ordered headingElements = ( @@ -503,8 +520,8 @@ booleanAttributes = { "": frozenset(("irrelevant",)), "style": frozenset(("scoped",)), "img": frozenset(("ismap",)), - "audio": frozenset(("autoplay","controls")), - "video": frozenset(("autoplay","controls")), + "audio": frozenset(("autoplay", "controls")), + "video": frozenset(("autoplay", "controls")), "script": frozenset(("defer", "async")), "details": frozenset(("open",)), "datagrid": frozenset(("multiple", "disabled")), @@ -523,2312 +540,2312 @@ booleanAttributes = { # entitiesWindows1252 has to be _ordered_ and needs to have an index. It # therefore can't be a frozenset. entitiesWindows1252 = ( - 8364, # 0x80 0x20AC EURO SIGN - 65533, # 0x81 UNDEFINED - 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK - 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK - 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK - 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS - 8224, # 0x86 0x2020 DAGGER - 8225, # 0x87 0x2021 DOUBLE DAGGER - 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT - 8240, # 0x89 0x2030 PER MILLE SIGN - 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON - 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK - 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE - 65533, # 0x8D UNDEFINED - 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON - 65533, # 0x8F UNDEFINED - 65533, # 0x90 UNDEFINED - 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK - 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK - 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK - 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK - 8226, # 0x95 0x2022 BULLET - 8211, # 0x96 0x2013 EN DASH - 8212, # 0x97 0x2014 EM DASH - 732, # 0x98 0x02DC SMALL TILDE - 8482, # 0x99 0x2122 TRADE MARK SIGN - 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON - 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE - 65533, # 0x9D UNDEFINED - 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON - 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS + 8364, # 0x80 0x20AC EURO SIGN + 65533, # 0x81 UNDEFINED + 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK + 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK + 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK + 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS + 8224, # 0x86 0x2020 DAGGER + 8225, # 0x87 0x2021 DOUBLE DAGGER + 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT + 8240, # 0x89 0x2030 PER MILLE SIGN + 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON + 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE + 65533, # 0x8D UNDEFINED + 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON + 65533, # 0x8F UNDEFINED + 65533, # 0x90 UNDEFINED + 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK + 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK + 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK + 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK + 8226, # 0x95 0x2022 BULLET + 8211, # 0x96 0x2013 EN DASH + 8212, # 0x97 0x2014 EM DASH + 732, # 0x98 0x02DC SMALL TILDE + 8482, # 0x99 0x2122 TRADE MARK SIGN + 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON + 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE + 65533, # 0x9D UNDEFINED + 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON + 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS ) xmlEntities = frozenset(('lt;', 'gt;', 'amp;', 'apos;', 'quot;')) entities = { - "AElig": u"\xc6", - "AElig;": u"\xc6", - "AMP": u"&", - "AMP;": u"&", - "Aacute": u"\xc1", - "Aacute;": u"\xc1", - "Abreve;": u"\u0102", - "Acirc": u"\xc2", - "Acirc;": u"\xc2", - "Acy;": u"\u0410", - "Afr;": u"\U0001d504", - "Agrave": u"\xc0", - "Agrave;": u"\xc0", - "Alpha;": u"\u0391", - "Amacr;": u"\u0100", - "And;": u"\u2a53", - "Aogon;": u"\u0104", - "Aopf;": u"\U0001d538", - "ApplyFunction;": u"\u2061", - "Aring": u"\xc5", - "Aring;": u"\xc5", - "Ascr;": u"\U0001d49c", - "Assign;": u"\u2254", - "Atilde": u"\xc3", - "Atilde;": u"\xc3", - "Auml": u"\xc4", - "Auml;": u"\xc4", - "Backslash;": u"\u2216", - "Barv;": u"\u2ae7", - "Barwed;": u"\u2306", - "Bcy;": u"\u0411", - "Because;": u"\u2235", - "Bernoullis;": u"\u212c", - "Beta;": u"\u0392", - "Bfr;": u"\U0001d505", - "Bopf;": u"\U0001d539", - "Breve;": u"\u02d8", - "Bscr;": u"\u212c", - "Bumpeq;": u"\u224e", - "CHcy;": u"\u0427", - "COPY": u"\xa9", - "COPY;": u"\xa9", - "Cacute;": u"\u0106", - "Cap;": u"\u22d2", - "CapitalDifferentialD;": u"\u2145", - "Cayleys;": u"\u212d", - "Ccaron;": u"\u010c", - "Ccedil": u"\xc7", - "Ccedil;": u"\xc7", - "Ccirc;": u"\u0108", - "Cconint;": u"\u2230", - "Cdot;": u"\u010a", - "Cedilla;": u"\xb8", - "CenterDot;": u"\xb7", - "Cfr;": u"\u212d", - "Chi;": u"\u03a7", - "CircleDot;": u"\u2299", - "CircleMinus;": u"\u2296", - "CirclePlus;": u"\u2295", - "CircleTimes;": u"\u2297", - "ClockwiseContourIntegral;": u"\u2232", - "CloseCurlyDoubleQuote;": u"\u201d", - "CloseCurlyQuote;": u"\u2019", - "Colon;": u"\u2237", - "Colone;": u"\u2a74", - "Congruent;": u"\u2261", - "Conint;": u"\u222f", - "ContourIntegral;": u"\u222e", - "Copf;": u"\u2102", - "Coproduct;": u"\u2210", - "CounterClockwiseContourIntegral;": u"\u2233", - "Cross;": u"\u2a2f", - "Cscr;": u"\U0001d49e", - "Cup;": u"\u22d3", - "CupCap;": u"\u224d", - "DD;": u"\u2145", - "DDotrahd;": u"\u2911", - "DJcy;": u"\u0402", - "DScy;": u"\u0405", - "DZcy;": u"\u040f", - "Dagger;": u"\u2021", - "Darr;": u"\u21a1", - "Dashv;": u"\u2ae4", - "Dcaron;": u"\u010e", - "Dcy;": u"\u0414", - "Del;": u"\u2207", - "Delta;": u"\u0394", - "Dfr;": u"\U0001d507", - "DiacriticalAcute;": u"\xb4", - "DiacriticalDot;": u"\u02d9", - "DiacriticalDoubleAcute;": u"\u02dd", - "DiacriticalGrave;": u"`", - "DiacriticalTilde;": u"\u02dc", - "Diamond;": u"\u22c4", - "DifferentialD;": u"\u2146", - "Dopf;": u"\U0001d53b", - "Dot;": u"\xa8", - "DotDot;": u"\u20dc", - "DotEqual;": u"\u2250", - "DoubleContourIntegral;": u"\u222f", - "DoubleDot;": u"\xa8", - "DoubleDownArrow;": u"\u21d3", - "DoubleLeftArrow;": u"\u21d0", - "DoubleLeftRightArrow;": u"\u21d4", - "DoubleLeftTee;": u"\u2ae4", - "DoubleLongLeftArrow;": u"\u27f8", - "DoubleLongLeftRightArrow;": u"\u27fa", - "DoubleLongRightArrow;": u"\u27f9", - "DoubleRightArrow;": u"\u21d2", - "DoubleRightTee;": u"\u22a8", - "DoubleUpArrow;": u"\u21d1", - "DoubleUpDownArrow;": u"\u21d5", - "DoubleVerticalBar;": u"\u2225", - "DownArrow;": u"\u2193", - "DownArrowBar;": u"\u2913", - "DownArrowUpArrow;": u"\u21f5", - "DownBreve;": u"\u0311", - "DownLeftRightVector;": u"\u2950", - "DownLeftTeeVector;": u"\u295e", - "DownLeftVector;": u"\u21bd", - "DownLeftVectorBar;": u"\u2956", - "DownRightTeeVector;": u"\u295f", - "DownRightVector;": u"\u21c1", - "DownRightVectorBar;": u"\u2957", - "DownTee;": u"\u22a4", - "DownTeeArrow;": u"\u21a7", - "Downarrow;": u"\u21d3", - "Dscr;": u"\U0001d49f", - "Dstrok;": u"\u0110", - "ENG;": u"\u014a", - "ETH": u"\xd0", - "ETH;": u"\xd0", - "Eacute": u"\xc9", - "Eacute;": u"\xc9", - "Ecaron;": u"\u011a", - "Ecirc": u"\xca", - "Ecirc;": u"\xca", - "Ecy;": u"\u042d", - "Edot;": u"\u0116", - "Efr;": u"\U0001d508", - "Egrave": u"\xc8", - "Egrave;": u"\xc8", - "Element;": u"\u2208", - "Emacr;": u"\u0112", - "EmptySmallSquare;": u"\u25fb", - "EmptyVerySmallSquare;": u"\u25ab", - "Eogon;": u"\u0118", - "Eopf;": u"\U0001d53c", - "Epsilon;": u"\u0395", - "Equal;": u"\u2a75", - "EqualTilde;": u"\u2242", - "Equilibrium;": u"\u21cc", - "Escr;": u"\u2130", - "Esim;": u"\u2a73", - "Eta;": u"\u0397", - "Euml": u"\xcb", - "Euml;": u"\xcb", - "Exists;": u"\u2203", - "ExponentialE;": u"\u2147", - "Fcy;": u"\u0424", - "Ffr;": u"\U0001d509", - "FilledSmallSquare;": u"\u25fc", - "FilledVerySmallSquare;": u"\u25aa", - "Fopf;": u"\U0001d53d", - "ForAll;": u"\u2200", - "Fouriertrf;": u"\u2131", - "Fscr;": u"\u2131", - "GJcy;": u"\u0403", - "GT": u">", - "GT;": u">", - "Gamma;": u"\u0393", - "Gammad;": u"\u03dc", - "Gbreve;": u"\u011e", - "Gcedil;": u"\u0122", - "Gcirc;": u"\u011c", - "Gcy;": u"\u0413", - "Gdot;": u"\u0120", - "Gfr;": u"\U0001d50a", - "Gg;": u"\u22d9", - "Gopf;": u"\U0001d53e", - "GreaterEqual;": u"\u2265", - "GreaterEqualLess;": u"\u22db", - "GreaterFullEqual;": u"\u2267", - "GreaterGreater;": u"\u2aa2", - "GreaterLess;": u"\u2277", - "GreaterSlantEqual;": u"\u2a7e", - "GreaterTilde;": u"\u2273", - "Gscr;": u"\U0001d4a2", - "Gt;": u"\u226b", - "HARDcy;": u"\u042a", - "Hacek;": u"\u02c7", - "Hat;": u"^", - "Hcirc;": u"\u0124", - "Hfr;": u"\u210c", - "HilbertSpace;": u"\u210b", - "Hopf;": u"\u210d", - "HorizontalLine;": u"\u2500", - "Hscr;": u"\u210b", - "Hstrok;": u"\u0126", - "HumpDownHump;": u"\u224e", - "HumpEqual;": u"\u224f", - "IEcy;": u"\u0415", - "IJlig;": u"\u0132", - "IOcy;": u"\u0401", - "Iacute": u"\xcd", - "Iacute;": u"\xcd", - "Icirc": u"\xce", - "Icirc;": u"\xce", - "Icy;": u"\u0418", - "Idot;": u"\u0130", - "Ifr;": u"\u2111", - "Igrave": u"\xcc", - "Igrave;": u"\xcc", - "Im;": u"\u2111", - "Imacr;": u"\u012a", - "ImaginaryI;": u"\u2148", - "Implies;": u"\u21d2", - "Int;": u"\u222c", - "Integral;": u"\u222b", - "Intersection;": u"\u22c2", - "InvisibleComma;": u"\u2063", - "InvisibleTimes;": u"\u2062", - "Iogon;": u"\u012e", - "Iopf;": u"\U0001d540", - "Iota;": u"\u0399", - "Iscr;": u"\u2110", - "Itilde;": u"\u0128", - "Iukcy;": u"\u0406", - "Iuml": u"\xcf", - "Iuml;": u"\xcf", - "Jcirc;": u"\u0134", - "Jcy;": u"\u0419", - "Jfr;": u"\U0001d50d", - "Jopf;": u"\U0001d541", - "Jscr;": u"\U0001d4a5", - "Jsercy;": u"\u0408", - "Jukcy;": u"\u0404", - "KHcy;": u"\u0425", - "KJcy;": u"\u040c", - "Kappa;": u"\u039a", - "Kcedil;": u"\u0136", - "Kcy;": u"\u041a", - "Kfr;": u"\U0001d50e", - "Kopf;": u"\U0001d542", - "Kscr;": u"\U0001d4a6", - "LJcy;": u"\u0409", - "LT": u"<", - "LT;": u"<", - "Lacute;": u"\u0139", - "Lambda;": u"\u039b", - "Lang;": u"\u27ea", - "Laplacetrf;": u"\u2112", - "Larr;": u"\u219e", - "Lcaron;": u"\u013d", - "Lcedil;": u"\u013b", - "Lcy;": u"\u041b", - "LeftAngleBracket;": u"\u27e8", - "LeftArrow;": u"\u2190", - "LeftArrowBar;": u"\u21e4", - "LeftArrowRightArrow;": u"\u21c6", - "LeftCeiling;": u"\u2308", - "LeftDoubleBracket;": u"\u27e6", - "LeftDownTeeVector;": u"\u2961", - "LeftDownVector;": u"\u21c3", - "LeftDownVectorBar;": u"\u2959", - "LeftFloor;": u"\u230a", - "LeftRightArrow;": u"\u2194", - "LeftRightVector;": u"\u294e", - "LeftTee;": u"\u22a3", - "LeftTeeArrow;": u"\u21a4", - "LeftTeeVector;": u"\u295a", - "LeftTriangle;": u"\u22b2", - "LeftTriangleBar;": u"\u29cf", - "LeftTriangleEqual;": u"\u22b4", - "LeftUpDownVector;": u"\u2951", - "LeftUpTeeVector;": u"\u2960", - "LeftUpVector;": u"\u21bf", - "LeftUpVectorBar;": u"\u2958", - "LeftVector;": u"\u21bc", - "LeftVectorBar;": u"\u2952", - "Leftarrow;": u"\u21d0", - "Leftrightarrow;": u"\u21d4", - "LessEqualGreater;": u"\u22da", - "LessFullEqual;": u"\u2266", - "LessGreater;": u"\u2276", - "LessLess;": u"\u2aa1", - "LessSlantEqual;": u"\u2a7d", - "LessTilde;": u"\u2272", - "Lfr;": u"\U0001d50f", - "Ll;": u"\u22d8", - "Lleftarrow;": u"\u21da", - "Lmidot;": u"\u013f", - "LongLeftArrow;": u"\u27f5", - "LongLeftRightArrow;": u"\u27f7", - "LongRightArrow;": u"\u27f6", - "Longleftarrow;": u"\u27f8", - "Longleftrightarrow;": u"\u27fa", - "Longrightarrow;": u"\u27f9", - "Lopf;": u"\U0001d543", - "LowerLeftArrow;": u"\u2199", - "LowerRightArrow;": u"\u2198", - "Lscr;": u"\u2112", - "Lsh;": u"\u21b0", - "Lstrok;": u"\u0141", - "Lt;": u"\u226a", - "Map;": u"\u2905", - "Mcy;": u"\u041c", - "MediumSpace;": u"\u205f", - "Mellintrf;": u"\u2133", - "Mfr;": u"\U0001d510", - "MinusPlus;": u"\u2213", - "Mopf;": u"\U0001d544", - "Mscr;": u"\u2133", - "Mu;": u"\u039c", - "NJcy;": u"\u040a", - "Nacute;": u"\u0143", - "Ncaron;": u"\u0147", - "Ncedil;": u"\u0145", - "Ncy;": u"\u041d", - "NegativeMediumSpace;": u"\u200b", - "NegativeThickSpace;": u"\u200b", - "NegativeThinSpace;": u"\u200b", - "NegativeVeryThinSpace;": u"\u200b", - "NestedGreaterGreater;": u"\u226b", - "NestedLessLess;": u"\u226a", - "NewLine;": u"\n", - "Nfr;": u"\U0001d511", - "NoBreak;": u"\u2060", - "NonBreakingSpace;": u"\xa0", - "Nopf;": u"\u2115", - "Not;": u"\u2aec", - "NotCongruent;": u"\u2262", - "NotCupCap;": u"\u226d", - "NotDoubleVerticalBar;": u"\u2226", - "NotElement;": u"\u2209", - "NotEqual;": u"\u2260", - "NotEqualTilde;": u"\u2242\u0338", - "NotExists;": u"\u2204", - "NotGreater;": u"\u226f", - "NotGreaterEqual;": u"\u2271", - "NotGreaterFullEqual;": u"\u2267\u0338", - "NotGreaterGreater;": u"\u226b\u0338", - "NotGreaterLess;": u"\u2279", - "NotGreaterSlantEqual;": u"\u2a7e\u0338", - "NotGreaterTilde;": u"\u2275", - "NotHumpDownHump;": u"\u224e\u0338", - "NotHumpEqual;": u"\u224f\u0338", - "NotLeftTriangle;": u"\u22ea", - "NotLeftTriangleBar;": u"\u29cf\u0338", - "NotLeftTriangleEqual;": u"\u22ec", - "NotLess;": u"\u226e", - "NotLessEqual;": u"\u2270", - "NotLessGreater;": u"\u2278", - "NotLessLess;": u"\u226a\u0338", - "NotLessSlantEqual;": u"\u2a7d\u0338", - "NotLessTilde;": u"\u2274", - "NotNestedGreaterGreater;": u"\u2aa2\u0338", - "NotNestedLessLess;": u"\u2aa1\u0338", - "NotPrecedes;": u"\u2280", - "NotPrecedesEqual;": u"\u2aaf\u0338", - "NotPrecedesSlantEqual;": u"\u22e0", - "NotReverseElement;": u"\u220c", - "NotRightTriangle;": u"\u22eb", - "NotRightTriangleBar;": u"\u29d0\u0338", - "NotRightTriangleEqual;": u"\u22ed", - "NotSquareSubset;": u"\u228f\u0338", - "NotSquareSubsetEqual;": u"\u22e2", - "NotSquareSuperset;": u"\u2290\u0338", - "NotSquareSupersetEqual;": u"\u22e3", - "NotSubset;": u"\u2282\u20d2", - "NotSubsetEqual;": u"\u2288", - "NotSucceeds;": u"\u2281", - "NotSucceedsEqual;": u"\u2ab0\u0338", - "NotSucceedsSlantEqual;": u"\u22e1", - "NotSucceedsTilde;": u"\u227f\u0338", - "NotSuperset;": u"\u2283\u20d2", - "NotSupersetEqual;": u"\u2289", - "NotTilde;": u"\u2241", - "NotTildeEqual;": u"\u2244", - "NotTildeFullEqual;": u"\u2247", - "NotTildeTilde;": u"\u2249", - "NotVerticalBar;": u"\u2224", - "Nscr;": u"\U0001d4a9", - "Ntilde": u"\xd1", - "Ntilde;": u"\xd1", - "Nu;": u"\u039d", - "OElig;": u"\u0152", - "Oacute": u"\xd3", - "Oacute;": u"\xd3", - "Ocirc": u"\xd4", - "Ocirc;": u"\xd4", - "Ocy;": u"\u041e", - "Odblac;": u"\u0150", - "Ofr;": u"\U0001d512", - "Ograve": u"\xd2", - "Ograve;": u"\xd2", - "Omacr;": u"\u014c", - "Omega;": u"\u03a9", - "Omicron;": u"\u039f", - "Oopf;": u"\U0001d546", - "OpenCurlyDoubleQuote;": u"\u201c", - "OpenCurlyQuote;": u"\u2018", - "Or;": u"\u2a54", - "Oscr;": u"\U0001d4aa", - "Oslash": u"\xd8", - "Oslash;": u"\xd8", - "Otilde": u"\xd5", - "Otilde;": u"\xd5", - "Otimes;": u"\u2a37", - "Ouml": u"\xd6", - "Ouml;": u"\xd6", - "OverBar;": u"\u203e", - "OverBrace;": u"\u23de", - "OverBracket;": u"\u23b4", - "OverParenthesis;": u"\u23dc", - "PartialD;": u"\u2202", - "Pcy;": u"\u041f", - "Pfr;": u"\U0001d513", - "Phi;": u"\u03a6", - "Pi;": u"\u03a0", - "PlusMinus;": u"\xb1", - "Poincareplane;": u"\u210c", - "Popf;": u"\u2119", - "Pr;": u"\u2abb", - "Precedes;": u"\u227a", - "PrecedesEqual;": u"\u2aaf", - "PrecedesSlantEqual;": u"\u227c", - "PrecedesTilde;": u"\u227e", - "Prime;": u"\u2033", - "Product;": u"\u220f", - "Proportion;": u"\u2237", - "Proportional;": u"\u221d", - "Pscr;": u"\U0001d4ab", - "Psi;": u"\u03a8", - "QUOT": u"\"", - "QUOT;": u"\"", - "Qfr;": u"\U0001d514", - "Qopf;": u"\u211a", - "Qscr;": u"\U0001d4ac", - "RBarr;": u"\u2910", - "REG": u"\xae", - "REG;": u"\xae", - "Racute;": u"\u0154", - "Rang;": u"\u27eb", - "Rarr;": u"\u21a0", - "Rarrtl;": u"\u2916", - "Rcaron;": u"\u0158", - "Rcedil;": u"\u0156", - "Rcy;": u"\u0420", - "Re;": u"\u211c", - "ReverseElement;": u"\u220b", - "ReverseEquilibrium;": u"\u21cb", - "ReverseUpEquilibrium;": u"\u296f", - "Rfr;": u"\u211c", - "Rho;": u"\u03a1", - "RightAngleBracket;": u"\u27e9", - "RightArrow;": u"\u2192", - "RightArrowBar;": u"\u21e5", - "RightArrowLeftArrow;": u"\u21c4", - "RightCeiling;": u"\u2309", - "RightDoubleBracket;": u"\u27e7", - "RightDownTeeVector;": u"\u295d", - "RightDownVector;": u"\u21c2", - "RightDownVectorBar;": u"\u2955", - "RightFloor;": u"\u230b", - "RightTee;": u"\u22a2", - "RightTeeArrow;": u"\u21a6", - "RightTeeVector;": u"\u295b", - "RightTriangle;": u"\u22b3", - "RightTriangleBar;": u"\u29d0", - "RightTriangleEqual;": u"\u22b5", - "RightUpDownVector;": u"\u294f", - "RightUpTeeVector;": u"\u295c", - "RightUpVector;": u"\u21be", - "RightUpVectorBar;": u"\u2954", - "RightVector;": u"\u21c0", - "RightVectorBar;": u"\u2953", - "Rightarrow;": u"\u21d2", - "Ropf;": u"\u211d", - "RoundImplies;": u"\u2970", - "Rrightarrow;": u"\u21db", - "Rscr;": u"\u211b", - "Rsh;": u"\u21b1", - "RuleDelayed;": u"\u29f4", - "SHCHcy;": u"\u0429", - "SHcy;": u"\u0428", - "SOFTcy;": u"\u042c", - "Sacute;": u"\u015a", - "Sc;": u"\u2abc", - "Scaron;": u"\u0160", - "Scedil;": u"\u015e", - "Scirc;": u"\u015c", - "Scy;": u"\u0421", - "Sfr;": u"\U0001d516", - "ShortDownArrow;": u"\u2193", - "ShortLeftArrow;": u"\u2190", - "ShortRightArrow;": u"\u2192", - "ShortUpArrow;": u"\u2191", - "Sigma;": u"\u03a3", - "SmallCircle;": u"\u2218", - "Sopf;": u"\U0001d54a", - "Sqrt;": u"\u221a", - "Square;": u"\u25a1", - "SquareIntersection;": u"\u2293", - "SquareSubset;": u"\u228f", - "SquareSubsetEqual;": u"\u2291", - "SquareSuperset;": u"\u2290", - "SquareSupersetEqual;": u"\u2292", - "SquareUnion;": u"\u2294", - "Sscr;": u"\U0001d4ae", - "Star;": u"\u22c6", - "Sub;": u"\u22d0", - "Subset;": u"\u22d0", - "SubsetEqual;": u"\u2286", - "Succeeds;": u"\u227b", - "SucceedsEqual;": u"\u2ab0", - "SucceedsSlantEqual;": u"\u227d", - "SucceedsTilde;": u"\u227f", - "SuchThat;": u"\u220b", - "Sum;": u"\u2211", - "Sup;": u"\u22d1", - "Superset;": u"\u2283", - "SupersetEqual;": u"\u2287", - "Supset;": u"\u22d1", - "THORN": u"\xde", - "THORN;": u"\xde", - "TRADE;": u"\u2122", - "TSHcy;": u"\u040b", - "TScy;": u"\u0426", - "Tab;": u"\t", - "Tau;": u"\u03a4", - "Tcaron;": u"\u0164", - "Tcedil;": u"\u0162", - "Tcy;": u"\u0422", - "Tfr;": u"\U0001d517", - "Therefore;": u"\u2234", - "Theta;": u"\u0398", - "ThickSpace;": u"\u205f\u200a", - "ThinSpace;": u"\u2009", - "Tilde;": u"\u223c", - "TildeEqual;": u"\u2243", - "TildeFullEqual;": u"\u2245", - "TildeTilde;": u"\u2248", - "Topf;": u"\U0001d54b", - "TripleDot;": u"\u20db", - "Tscr;": u"\U0001d4af", - "Tstrok;": u"\u0166", - "Uacute": u"\xda", - "Uacute;": u"\xda", - "Uarr;": u"\u219f", - "Uarrocir;": u"\u2949", - "Ubrcy;": u"\u040e", - "Ubreve;": u"\u016c", - "Ucirc": u"\xdb", - "Ucirc;": u"\xdb", - "Ucy;": u"\u0423", - "Udblac;": u"\u0170", - "Ufr;": u"\U0001d518", - "Ugrave": u"\xd9", - "Ugrave;": u"\xd9", - "Umacr;": u"\u016a", - "UnderBar;": u"_", - "UnderBrace;": u"\u23df", - "UnderBracket;": u"\u23b5", - "UnderParenthesis;": u"\u23dd", - "Union;": u"\u22c3", - "UnionPlus;": u"\u228e", - "Uogon;": u"\u0172", - "Uopf;": u"\U0001d54c", - "UpArrow;": u"\u2191", - "UpArrowBar;": u"\u2912", - "UpArrowDownArrow;": u"\u21c5", - "UpDownArrow;": u"\u2195", - "UpEquilibrium;": u"\u296e", - "UpTee;": u"\u22a5", - "UpTeeArrow;": u"\u21a5", - "Uparrow;": u"\u21d1", - "Updownarrow;": u"\u21d5", - "UpperLeftArrow;": u"\u2196", - "UpperRightArrow;": u"\u2197", - "Upsi;": u"\u03d2", - "Upsilon;": u"\u03a5", - "Uring;": u"\u016e", - "Uscr;": u"\U0001d4b0", - "Utilde;": u"\u0168", - "Uuml": u"\xdc", - "Uuml;": u"\xdc", - "VDash;": u"\u22ab", - "Vbar;": u"\u2aeb", - "Vcy;": u"\u0412", - "Vdash;": u"\u22a9", - "Vdashl;": u"\u2ae6", - "Vee;": u"\u22c1", - "Verbar;": u"\u2016", - "Vert;": u"\u2016", - "VerticalBar;": u"\u2223", - "VerticalLine;": u"|", - "VerticalSeparator;": u"\u2758", - "VerticalTilde;": u"\u2240", - "VeryThinSpace;": u"\u200a", - "Vfr;": u"\U0001d519", - "Vopf;": u"\U0001d54d", - "Vscr;": u"\U0001d4b1", - "Vvdash;": u"\u22aa", - "Wcirc;": u"\u0174", - "Wedge;": u"\u22c0", - "Wfr;": u"\U0001d51a", - "Wopf;": u"\U0001d54e", - "Wscr;": u"\U0001d4b2", - "Xfr;": u"\U0001d51b", - "Xi;": u"\u039e", - "Xopf;": u"\U0001d54f", - "Xscr;": u"\U0001d4b3", - "YAcy;": u"\u042f", - "YIcy;": u"\u0407", - "YUcy;": u"\u042e", - "Yacute": u"\xdd", - "Yacute;": u"\xdd", - "Ycirc;": u"\u0176", - "Ycy;": u"\u042b", - "Yfr;": u"\U0001d51c", - "Yopf;": u"\U0001d550", - "Yscr;": u"\U0001d4b4", - "Yuml;": u"\u0178", - "ZHcy;": u"\u0416", - "Zacute;": u"\u0179", - "Zcaron;": u"\u017d", - "Zcy;": u"\u0417", - "Zdot;": u"\u017b", - "ZeroWidthSpace;": u"\u200b", - "Zeta;": u"\u0396", - "Zfr;": u"\u2128", - "Zopf;": u"\u2124", - "Zscr;": u"\U0001d4b5", - "aacute": u"\xe1", - "aacute;": u"\xe1", - "abreve;": u"\u0103", - "ac;": u"\u223e", - "acE;": u"\u223e\u0333", - "acd;": u"\u223f", - "acirc": u"\xe2", - "acirc;": u"\xe2", - "acute": u"\xb4", - "acute;": u"\xb4", - "acy;": u"\u0430", - "aelig": u"\xe6", - "aelig;": u"\xe6", - "af;": u"\u2061", - "afr;": u"\U0001d51e", - "agrave": u"\xe0", - "agrave;": u"\xe0", - "alefsym;": u"\u2135", - "aleph;": u"\u2135", - "alpha;": u"\u03b1", - "amacr;": u"\u0101", - "amalg;": u"\u2a3f", - "amp": u"&", - "amp;": u"&", - "and;": u"\u2227", - "andand;": u"\u2a55", - "andd;": u"\u2a5c", - "andslope;": u"\u2a58", - "andv;": u"\u2a5a", - "ang;": u"\u2220", - "ange;": u"\u29a4", - "angle;": u"\u2220", - "angmsd;": u"\u2221", - "angmsdaa;": u"\u29a8", - "angmsdab;": u"\u29a9", - "angmsdac;": u"\u29aa", - "angmsdad;": u"\u29ab", - "angmsdae;": u"\u29ac", - "angmsdaf;": u"\u29ad", - "angmsdag;": u"\u29ae", - "angmsdah;": u"\u29af", - "angrt;": u"\u221f", - "angrtvb;": u"\u22be", - "angrtvbd;": u"\u299d", - "angsph;": u"\u2222", - "angst;": u"\xc5", - "angzarr;": u"\u237c", - "aogon;": u"\u0105", - "aopf;": u"\U0001d552", - "ap;": u"\u2248", - "apE;": u"\u2a70", - "apacir;": u"\u2a6f", - "ape;": u"\u224a", - "apid;": u"\u224b", - "apos;": u"'", - "approx;": u"\u2248", - "approxeq;": u"\u224a", - "aring": u"\xe5", - "aring;": u"\xe5", - "ascr;": u"\U0001d4b6", - "ast;": u"*", - "asymp;": u"\u2248", - "asympeq;": u"\u224d", - "atilde": u"\xe3", - "atilde;": u"\xe3", - "auml": u"\xe4", - "auml;": u"\xe4", - "awconint;": u"\u2233", - "awint;": u"\u2a11", - "bNot;": u"\u2aed", - "backcong;": u"\u224c", - "backepsilon;": u"\u03f6", - "backprime;": u"\u2035", - "backsim;": u"\u223d", - "backsimeq;": u"\u22cd", - "barvee;": u"\u22bd", - "barwed;": u"\u2305", - "barwedge;": u"\u2305", - "bbrk;": u"\u23b5", - "bbrktbrk;": u"\u23b6", - "bcong;": u"\u224c", - "bcy;": u"\u0431", - "bdquo;": u"\u201e", - "becaus;": u"\u2235", - "because;": u"\u2235", - "bemptyv;": u"\u29b0", - "bepsi;": u"\u03f6", - "bernou;": u"\u212c", - "beta;": u"\u03b2", - "beth;": u"\u2136", - "between;": u"\u226c", - "bfr;": u"\U0001d51f", - "bigcap;": u"\u22c2", - "bigcirc;": u"\u25ef", - "bigcup;": u"\u22c3", - "bigodot;": u"\u2a00", - "bigoplus;": u"\u2a01", - "bigotimes;": u"\u2a02", - "bigsqcup;": u"\u2a06", - "bigstar;": u"\u2605", - "bigtriangledown;": u"\u25bd", - "bigtriangleup;": u"\u25b3", - "biguplus;": u"\u2a04", - "bigvee;": u"\u22c1", - "bigwedge;": u"\u22c0", - "bkarow;": u"\u290d", - "blacklozenge;": u"\u29eb", - "blacksquare;": u"\u25aa", - "blacktriangle;": u"\u25b4", - "blacktriangledown;": u"\u25be", - "blacktriangleleft;": u"\u25c2", - "blacktriangleright;": u"\u25b8", - "blank;": u"\u2423", - "blk12;": u"\u2592", - "blk14;": u"\u2591", - "blk34;": u"\u2593", - "block;": u"\u2588", - "bne;": u"=\u20e5", - "bnequiv;": u"\u2261\u20e5", - "bnot;": u"\u2310", - "bopf;": u"\U0001d553", - "bot;": u"\u22a5", - "bottom;": u"\u22a5", - "bowtie;": u"\u22c8", - "boxDL;": u"\u2557", - "boxDR;": u"\u2554", - "boxDl;": u"\u2556", - "boxDr;": u"\u2553", - "boxH;": u"\u2550", - "boxHD;": u"\u2566", - "boxHU;": u"\u2569", - "boxHd;": u"\u2564", - "boxHu;": u"\u2567", - "boxUL;": u"\u255d", - "boxUR;": u"\u255a", - "boxUl;": u"\u255c", - "boxUr;": u"\u2559", - "boxV;": u"\u2551", - "boxVH;": u"\u256c", - "boxVL;": u"\u2563", - "boxVR;": u"\u2560", - "boxVh;": u"\u256b", - "boxVl;": u"\u2562", - "boxVr;": u"\u255f", - "boxbox;": u"\u29c9", - "boxdL;": u"\u2555", - "boxdR;": u"\u2552", - "boxdl;": u"\u2510", - "boxdr;": u"\u250c", - "boxh;": u"\u2500", - "boxhD;": u"\u2565", - "boxhU;": u"\u2568", - "boxhd;": u"\u252c", - "boxhu;": u"\u2534", - "boxminus;": u"\u229f", - "boxplus;": u"\u229e", - "boxtimes;": u"\u22a0", - "boxuL;": u"\u255b", - "boxuR;": u"\u2558", - "boxul;": u"\u2518", - "boxur;": u"\u2514", - "boxv;": u"\u2502", - "boxvH;": u"\u256a", - "boxvL;": u"\u2561", - "boxvR;": u"\u255e", - "boxvh;": u"\u253c", - "boxvl;": u"\u2524", - "boxvr;": u"\u251c", - "bprime;": u"\u2035", - "breve;": u"\u02d8", - "brvbar": u"\xa6", - "brvbar;": u"\xa6", - "bscr;": u"\U0001d4b7", - "bsemi;": u"\u204f", - "bsim;": u"\u223d", - "bsime;": u"\u22cd", - "bsol;": u"\\", - "bsolb;": u"\u29c5", - "bsolhsub;": u"\u27c8", - "bull;": u"\u2022", - "bullet;": u"\u2022", - "bump;": u"\u224e", - "bumpE;": u"\u2aae", - "bumpe;": u"\u224f", - "bumpeq;": u"\u224f", - "cacute;": u"\u0107", - "cap;": u"\u2229", - "capand;": u"\u2a44", - "capbrcup;": u"\u2a49", - "capcap;": u"\u2a4b", - "capcup;": u"\u2a47", - "capdot;": u"\u2a40", - "caps;": u"\u2229\ufe00", - "caret;": u"\u2041", - "caron;": u"\u02c7", - "ccaps;": u"\u2a4d", - "ccaron;": u"\u010d", - "ccedil": u"\xe7", - "ccedil;": u"\xe7", - "ccirc;": u"\u0109", - "ccups;": u"\u2a4c", - "ccupssm;": u"\u2a50", - "cdot;": u"\u010b", - "cedil": u"\xb8", - "cedil;": u"\xb8", - "cemptyv;": u"\u29b2", - "cent": u"\xa2", - "cent;": u"\xa2", - "centerdot;": u"\xb7", - "cfr;": u"\U0001d520", - "chcy;": u"\u0447", - "check;": u"\u2713", - "checkmark;": u"\u2713", - "chi;": u"\u03c7", - "cir;": u"\u25cb", - "cirE;": u"\u29c3", - "circ;": u"\u02c6", - "circeq;": u"\u2257", - "circlearrowleft;": u"\u21ba", - "circlearrowright;": u"\u21bb", - "circledR;": u"\xae", - "circledS;": u"\u24c8", - "circledast;": u"\u229b", - "circledcirc;": u"\u229a", - "circleddash;": u"\u229d", - "cire;": u"\u2257", - "cirfnint;": u"\u2a10", - "cirmid;": u"\u2aef", - "cirscir;": u"\u29c2", - "clubs;": u"\u2663", - "clubsuit;": u"\u2663", - "colon;": u":", - "colone;": u"\u2254", - "coloneq;": u"\u2254", - "comma;": u",", - "commat;": u"@", - "comp;": u"\u2201", - "compfn;": u"\u2218", - "complement;": u"\u2201", - "complexes;": u"\u2102", - "cong;": u"\u2245", - "congdot;": u"\u2a6d", - "conint;": u"\u222e", - "copf;": u"\U0001d554", - "coprod;": u"\u2210", - "copy": u"\xa9", - "copy;": u"\xa9", - "copysr;": u"\u2117", - "crarr;": u"\u21b5", - "cross;": u"\u2717", - "cscr;": u"\U0001d4b8", - "csub;": u"\u2acf", - "csube;": u"\u2ad1", - "csup;": u"\u2ad0", - "csupe;": u"\u2ad2", - "ctdot;": u"\u22ef", - "cudarrl;": u"\u2938", - "cudarrr;": u"\u2935", - "cuepr;": u"\u22de", - "cuesc;": u"\u22df", - "cularr;": u"\u21b6", - "cularrp;": u"\u293d", - "cup;": u"\u222a", - "cupbrcap;": u"\u2a48", - "cupcap;": u"\u2a46", - "cupcup;": u"\u2a4a", - "cupdot;": u"\u228d", - "cupor;": u"\u2a45", - "cups;": u"\u222a\ufe00", - "curarr;": u"\u21b7", - "curarrm;": u"\u293c", - "curlyeqprec;": u"\u22de", - "curlyeqsucc;": u"\u22df", - "curlyvee;": u"\u22ce", - "curlywedge;": u"\u22cf", - "curren": u"\xa4", - "curren;": u"\xa4", - "curvearrowleft;": u"\u21b6", - "curvearrowright;": u"\u21b7", - "cuvee;": u"\u22ce", - "cuwed;": u"\u22cf", - "cwconint;": u"\u2232", - "cwint;": u"\u2231", - "cylcty;": u"\u232d", - "dArr;": u"\u21d3", - "dHar;": u"\u2965", - "dagger;": u"\u2020", - "daleth;": u"\u2138", - "darr;": u"\u2193", - "dash;": u"\u2010", - "dashv;": u"\u22a3", - "dbkarow;": u"\u290f", - "dblac;": u"\u02dd", - "dcaron;": u"\u010f", - "dcy;": u"\u0434", - "dd;": u"\u2146", - "ddagger;": u"\u2021", - "ddarr;": u"\u21ca", - "ddotseq;": u"\u2a77", - "deg": u"\xb0", - "deg;": u"\xb0", - "delta;": u"\u03b4", - "demptyv;": u"\u29b1", - "dfisht;": u"\u297f", - "dfr;": u"\U0001d521", - "dharl;": u"\u21c3", - "dharr;": u"\u21c2", - "diam;": u"\u22c4", - "diamond;": u"\u22c4", - "diamondsuit;": u"\u2666", - "diams;": u"\u2666", - "die;": u"\xa8", - "digamma;": u"\u03dd", - "disin;": u"\u22f2", - "div;": u"\xf7", - "divide": u"\xf7", - "divide;": u"\xf7", - "divideontimes;": u"\u22c7", - "divonx;": u"\u22c7", - "djcy;": u"\u0452", - "dlcorn;": u"\u231e", - "dlcrop;": u"\u230d", - "dollar;": u"$", - "dopf;": u"\U0001d555", - "dot;": u"\u02d9", - "doteq;": u"\u2250", - "doteqdot;": u"\u2251", - "dotminus;": u"\u2238", - "dotplus;": u"\u2214", - "dotsquare;": u"\u22a1", - "doublebarwedge;": u"\u2306", - "downarrow;": u"\u2193", - "downdownarrows;": u"\u21ca", - "downharpoonleft;": u"\u21c3", - "downharpoonright;": u"\u21c2", - "drbkarow;": u"\u2910", - "drcorn;": u"\u231f", - "drcrop;": u"\u230c", - "dscr;": u"\U0001d4b9", - "dscy;": u"\u0455", - "dsol;": u"\u29f6", - "dstrok;": u"\u0111", - "dtdot;": u"\u22f1", - "dtri;": u"\u25bf", - "dtrif;": u"\u25be", - "duarr;": u"\u21f5", - "duhar;": u"\u296f", - "dwangle;": u"\u29a6", - "dzcy;": u"\u045f", - "dzigrarr;": u"\u27ff", - "eDDot;": u"\u2a77", - "eDot;": u"\u2251", - "eacute": u"\xe9", - "eacute;": u"\xe9", - "easter;": u"\u2a6e", - "ecaron;": u"\u011b", - "ecir;": u"\u2256", - "ecirc": u"\xea", - "ecirc;": u"\xea", - "ecolon;": u"\u2255", - "ecy;": u"\u044d", - "edot;": u"\u0117", - "ee;": u"\u2147", - "efDot;": u"\u2252", - "efr;": u"\U0001d522", - "eg;": u"\u2a9a", - "egrave": u"\xe8", - "egrave;": u"\xe8", - "egs;": u"\u2a96", - "egsdot;": u"\u2a98", - "el;": u"\u2a99", - "elinters;": u"\u23e7", - "ell;": u"\u2113", - "els;": u"\u2a95", - "elsdot;": u"\u2a97", - "emacr;": u"\u0113", - "empty;": u"\u2205", - "emptyset;": u"\u2205", - "emptyv;": u"\u2205", - "emsp13;": u"\u2004", - "emsp14;": u"\u2005", - "emsp;": u"\u2003", - "eng;": u"\u014b", - "ensp;": u"\u2002", - "eogon;": u"\u0119", - "eopf;": u"\U0001d556", - "epar;": u"\u22d5", - "eparsl;": u"\u29e3", - "eplus;": u"\u2a71", - "epsi;": u"\u03b5", - "epsilon;": u"\u03b5", - "epsiv;": u"\u03f5", - "eqcirc;": u"\u2256", - "eqcolon;": u"\u2255", - "eqsim;": u"\u2242", - "eqslantgtr;": u"\u2a96", - "eqslantless;": u"\u2a95", - "equals;": u"=", - "equest;": u"\u225f", - "equiv;": u"\u2261", - "equivDD;": u"\u2a78", - "eqvparsl;": u"\u29e5", - "erDot;": u"\u2253", - "erarr;": u"\u2971", - "escr;": u"\u212f", - "esdot;": u"\u2250", - "esim;": u"\u2242", - "eta;": u"\u03b7", - "eth": u"\xf0", - "eth;": u"\xf0", - "euml": u"\xeb", - "euml;": u"\xeb", - "euro;": u"\u20ac", - "excl;": u"!", - "exist;": u"\u2203", - "expectation;": u"\u2130", - "exponentiale;": u"\u2147", - "fallingdotseq;": u"\u2252", - "fcy;": u"\u0444", - "female;": u"\u2640", - "ffilig;": u"\ufb03", - "fflig;": u"\ufb00", - "ffllig;": u"\ufb04", - "ffr;": u"\U0001d523", - "filig;": u"\ufb01", - "fjlig;": u"fj", - "flat;": u"\u266d", - "fllig;": u"\ufb02", - "fltns;": u"\u25b1", - "fnof;": u"\u0192", - "fopf;": u"\U0001d557", - "forall;": u"\u2200", - "fork;": u"\u22d4", - "forkv;": u"\u2ad9", - "fpartint;": u"\u2a0d", - "frac12": u"\xbd", - "frac12;": u"\xbd", - "frac13;": u"\u2153", - "frac14": u"\xbc", - "frac14;": u"\xbc", - "frac15;": u"\u2155", - "frac16;": u"\u2159", - "frac18;": u"\u215b", - "frac23;": u"\u2154", - "frac25;": u"\u2156", - "frac34": u"\xbe", - "frac34;": u"\xbe", - "frac35;": u"\u2157", - "frac38;": u"\u215c", - "frac45;": u"\u2158", - "frac56;": u"\u215a", - "frac58;": u"\u215d", - "frac78;": u"\u215e", - "frasl;": u"\u2044", - "frown;": u"\u2322", - "fscr;": u"\U0001d4bb", - "gE;": u"\u2267", - "gEl;": u"\u2a8c", - "gacute;": u"\u01f5", - "gamma;": u"\u03b3", - "gammad;": u"\u03dd", - "gap;": u"\u2a86", - "gbreve;": u"\u011f", - "gcirc;": u"\u011d", - "gcy;": u"\u0433", - "gdot;": u"\u0121", - "ge;": u"\u2265", - "gel;": u"\u22db", - "geq;": u"\u2265", - "geqq;": u"\u2267", - "geqslant;": u"\u2a7e", - "ges;": u"\u2a7e", - "gescc;": u"\u2aa9", - "gesdot;": u"\u2a80", - "gesdoto;": u"\u2a82", - "gesdotol;": u"\u2a84", - "gesl;": u"\u22db\ufe00", - "gesles;": u"\u2a94", - "gfr;": u"\U0001d524", - "gg;": u"\u226b", - "ggg;": u"\u22d9", - "gimel;": u"\u2137", - "gjcy;": u"\u0453", - "gl;": u"\u2277", - "glE;": u"\u2a92", - "gla;": u"\u2aa5", - "glj;": u"\u2aa4", - "gnE;": u"\u2269", - "gnap;": u"\u2a8a", - "gnapprox;": u"\u2a8a", - "gne;": u"\u2a88", - "gneq;": u"\u2a88", - "gneqq;": u"\u2269", - "gnsim;": u"\u22e7", - "gopf;": u"\U0001d558", - "grave;": u"`", - "gscr;": u"\u210a", - "gsim;": u"\u2273", - "gsime;": u"\u2a8e", - "gsiml;": u"\u2a90", - "gt": u">", - "gt;": u">", - "gtcc;": u"\u2aa7", - "gtcir;": u"\u2a7a", - "gtdot;": u"\u22d7", - "gtlPar;": u"\u2995", - "gtquest;": u"\u2a7c", - "gtrapprox;": u"\u2a86", - "gtrarr;": u"\u2978", - "gtrdot;": u"\u22d7", - "gtreqless;": u"\u22db", - "gtreqqless;": u"\u2a8c", - "gtrless;": u"\u2277", - "gtrsim;": u"\u2273", - "gvertneqq;": u"\u2269\ufe00", - "gvnE;": u"\u2269\ufe00", - "hArr;": u"\u21d4", - "hairsp;": u"\u200a", - "half;": u"\xbd", - "hamilt;": u"\u210b", - "hardcy;": u"\u044a", - "harr;": u"\u2194", - "harrcir;": u"\u2948", - "harrw;": u"\u21ad", - "hbar;": u"\u210f", - "hcirc;": u"\u0125", - "hearts;": u"\u2665", - "heartsuit;": u"\u2665", - "hellip;": u"\u2026", - "hercon;": u"\u22b9", - "hfr;": u"\U0001d525", - "hksearow;": u"\u2925", - "hkswarow;": u"\u2926", - "hoarr;": u"\u21ff", - "homtht;": u"\u223b", - "hookleftarrow;": u"\u21a9", - "hookrightarrow;": u"\u21aa", - "hopf;": u"\U0001d559", - "horbar;": u"\u2015", - "hscr;": u"\U0001d4bd", - "hslash;": u"\u210f", - "hstrok;": u"\u0127", - "hybull;": u"\u2043", - "hyphen;": u"\u2010", - "iacute": u"\xed", - "iacute;": u"\xed", - "ic;": u"\u2063", - "icirc": u"\xee", - "icirc;": u"\xee", - "icy;": u"\u0438", - "iecy;": u"\u0435", - "iexcl": u"\xa1", - "iexcl;": u"\xa1", - "iff;": u"\u21d4", - "ifr;": u"\U0001d526", - "igrave": u"\xec", - "igrave;": u"\xec", - "ii;": u"\u2148", - "iiiint;": u"\u2a0c", - "iiint;": u"\u222d", - "iinfin;": u"\u29dc", - "iiota;": u"\u2129", - "ijlig;": u"\u0133", - "imacr;": u"\u012b", - "image;": u"\u2111", - "imagline;": u"\u2110", - "imagpart;": u"\u2111", - "imath;": u"\u0131", - "imof;": u"\u22b7", - "imped;": u"\u01b5", - "in;": u"\u2208", - "incare;": u"\u2105", - "infin;": u"\u221e", - "infintie;": u"\u29dd", - "inodot;": u"\u0131", - "int;": u"\u222b", - "intcal;": u"\u22ba", - "integers;": u"\u2124", - "intercal;": u"\u22ba", - "intlarhk;": u"\u2a17", - "intprod;": u"\u2a3c", - "iocy;": u"\u0451", - "iogon;": u"\u012f", - "iopf;": u"\U0001d55a", - "iota;": u"\u03b9", - "iprod;": u"\u2a3c", - "iquest": u"\xbf", - "iquest;": u"\xbf", - "iscr;": u"\U0001d4be", - "isin;": u"\u2208", - "isinE;": u"\u22f9", - "isindot;": u"\u22f5", - "isins;": u"\u22f4", - "isinsv;": u"\u22f3", - "isinv;": u"\u2208", - "it;": u"\u2062", - "itilde;": u"\u0129", - "iukcy;": u"\u0456", - "iuml": u"\xef", - "iuml;": u"\xef", - "jcirc;": u"\u0135", - "jcy;": u"\u0439", - "jfr;": u"\U0001d527", - "jmath;": u"\u0237", - "jopf;": u"\U0001d55b", - "jscr;": u"\U0001d4bf", - "jsercy;": u"\u0458", - "jukcy;": u"\u0454", - "kappa;": u"\u03ba", - "kappav;": u"\u03f0", - "kcedil;": u"\u0137", - "kcy;": u"\u043a", - "kfr;": u"\U0001d528", - "kgreen;": u"\u0138", - "khcy;": u"\u0445", - "kjcy;": u"\u045c", - "kopf;": u"\U0001d55c", - "kscr;": u"\U0001d4c0", - "lAarr;": u"\u21da", - "lArr;": u"\u21d0", - "lAtail;": u"\u291b", - "lBarr;": u"\u290e", - "lE;": u"\u2266", - "lEg;": u"\u2a8b", - "lHar;": u"\u2962", - "lacute;": u"\u013a", - "laemptyv;": u"\u29b4", - "lagran;": u"\u2112", - "lambda;": u"\u03bb", - "lang;": u"\u27e8", - "langd;": u"\u2991", - "langle;": u"\u27e8", - "lap;": u"\u2a85", - "laquo": u"\xab", - "laquo;": u"\xab", - "larr;": u"\u2190", - "larrb;": u"\u21e4", - "larrbfs;": u"\u291f", - "larrfs;": u"\u291d", - "larrhk;": u"\u21a9", - "larrlp;": u"\u21ab", - "larrpl;": u"\u2939", - "larrsim;": u"\u2973", - "larrtl;": u"\u21a2", - "lat;": u"\u2aab", - "latail;": u"\u2919", - "late;": u"\u2aad", - "lates;": u"\u2aad\ufe00", - "lbarr;": u"\u290c", - "lbbrk;": u"\u2772", - "lbrace;": u"{", - "lbrack;": u"[", - "lbrke;": u"\u298b", - "lbrksld;": u"\u298f", - "lbrkslu;": u"\u298d", - "lcaron;": u"\u013e", - "lcedil;": u"\u013c", - "lceil;": u"\u2308", - "lcub;": u"{", - "lcy;": u"\u043b", - "ldca;": u"\u2936", - "ldquo;": u"\u201c", - "ldquor;": u"\u201e", - "ldrdhar;": u"\u2967", - "ldrushar;": u"\u294b", - "ldsh;": u"\u21b2", - "le;": u"\u2264", - "leftarrow;": u"\u2190", - "leftarrowtail;": u"\u21a2", - "leftharpoondown;": u"\u21bd", - "leftharpoonup;": u"\u21bc", - "leftleftarrows;": u"\u21c7", - "leftrightarrow;": u"\u2194", - "leftrightarrows;": u"\u21c6", - "leftrightharpoons;": u"\u21cb", - "leftrightsquigarrow;": u"\u21ad", - "leftthreetimes;": u"\u22cb", - "leg;": u"\u22da", - "leq;": u"\u2264", - "leqq;": u"\u2266", - "leqslant;": u"\u2a7d", - "les;": u"\u2a7d", - "lescc;": u"\u2aa8", - "lesdot;": u"\u2a7f", - "lesdoto;": u"\u2a81", - "lesdotor;": u"\u2a83", - "lesg;": u"\u22da\ufe00", - "lesges;": u"\u2a93", - "lessapprox;": u"\u2a85", - "lessdot;": u"\u22d6", - "lesseqgtr;": u"\u22da", - "lesseqqgtr;": u"\u2a8b", - "lessgtr;": u"\u2276", - "lesssim;": u"\u2272", - "lfisht;": u"\u297c", - "lfloor;": u"\u230a", - "lfr;": u"\U0001d529", - "lg;": u"\u2276", - "lgE;": u"\u2a91", - "lhard;": u"\u21bd", - "lharu;": u"\u21bc", - "lharul;": u"\u296a", - "lhblk;": u"\u2584", - "ljcy;": u"\u0459", - "ll;": u"\u226a", - "llarr;": u"\u21c7", - "llcorner;": u"\u231e", - "llhard;": u"\u296b", - "lltri;": u"\u25fa", - "lmidot;": u"\u0140", - "lmoust;": u"\u23b0", - "lmoustache;": u"\u23b0", - "lnE;": u"\u2268", - "lnap;": u"\u2a89", - "lnapprox;": u"\u2a89", - "lne;": u"\u2a87", - "lneq;": u"\u2a87", - "lneqq;": u"\u2268", - "lnsim;": u"\u22e6", - "loang;": u"\u27ec", - "loarr;": u"\u21fd", - "lobrk;": u"\u27e6", - "longleftarrow;": u"\u27f5", - "longleftrightarrow;": u"\u27f7", - "longmapsto;": u"\u27fc", - "longrightarrow;": u"\u27f6", - "looparrowleft;": u"\u21ab", - "looparrowright;": u"\u21ac", - "lopar;": u"\u2985", - "lopf;": u"\U0001d55d", - "loplus;": u"\u2a2d", - "lotimes;": u"\u2a34", - "lowast;": u"\u2217", - "lowbar;": u"_", - "loz;": u"\u25ca", - "lozenge;": u"\u25ca", - "lozf;": u"\u29eb", - "lpar;": u"(", - "lparlt;": u"\u2993", - "lrarr;": u"\u21c6", - "lrcorner;": u"\u231f", - "lrhar;": u"\u21cb", - "lrhard;": u"\u296d", - "lrm;": u"\u200e", - "lrtri;": u"\u22bf", - "lsaquo;": u"\u2039", - "lscr;": u"\U0001d4c1", - "lsh;": u"\u21b0", - "lsim;": u"\u2272", - "lsime;": u"\u2a8d", - "lsimg;": u"\u2a8f", - "lsqb;": u"[", - "lsquo;": u"\u2018", - "lsquor;": u"\u201a", - "lstrok;": u"\u0142", - "lt": u"<", - "lt;": u"<", - "ltcc;": u"\u2aa6", - "ltcir;": u"\u2a79", - "ltdot;": u"\u22d6", - "lthree;": u"\u22cb", - "ltimes;": u"\u22c9", - "ltlarr;": u"\u2976", - "ltquest;": u"\u2a7b", - "ltrPar;": u"\u2996", - "ltri;": u"\u25c3", - "ltrie;": u"\u22b4", - "ltrif;": u"\u25c2", - "lurdshar;": u"\u294a", - "luruhar;": u"\u2966", - "lvertneqq;": u"\u2268\ufe00", - "lvnE;": u"\u2268\ufe00", - "mDDot;": u"\u223a", - "macr": u"\xaf", - "macr;": u"\xaf", - "male;": u"\u2642", - "malt;": u"\u2720", - "maltese;": u"\u2720", - "map;": u"\u21a6", - "mapsto;": u"\u21a6", - "mapstodown;": u"\u21a7", - "mapstoleft;": u"\u21a4", - "mapstoup;": u"\u21a5", - "marker;": u"\u25ae", - "mcomma;": u"\u2a29", - "mcy;": u"\u043c", - "mdash;": u"\u2014", - "measuredangle;": u"\u2221", - "mfr;": u"\U0001d52a", - "mho;": u"\u2127", - "micro": u"\xb5", - "micro;": u"\xb5", - "mid;": u"\u2223", - "midast;": u"*", - "midcir;": u"\u2af0", - "middot": u"\xb7", - "middot;": u"\xb7", - "minus;": u"\u2212", - "minusb;": u"\u229f", - "minusd;": u"\u2238", - "minusdu;": u"\u2a2a", - "mlcp;": u"\u2adb", - "mldr;": u"\u2026", - "mnplus;": u"\u2213", - "models;": u"\u22a7", - "mopf;": u"\U0001d55e", - "mp;": u"\u2213", - "mscr;": u"\U0001d4c2", - "mstpos;": u"\u223e", - "mu;": u"\u03bc", - "multimap;": u"\u22b8", - "mumap;": u"\u22b8", - "nGg;": u"\u22d9\u0338", - "nGt;": u"\u226b\u20d2", - "nGtv;": u"\u226b\u0338", - "nLeftarrow;": u"\u21cd", - "nLeftrightarrow;": u"\u21ce", - "nLl;": u"\u22d8\u0338", - "nLt;": u"\u226a\u20d2", - "nLtv;": u"\u226a\u0338", - "nRightarrow;": u"\u21cf", - "nVDash;": u"\u22af", - "nVdash;": u"\u22ae", - "nabla;": u"\u2207", - "nacute;": u"\u0144", - "nang;": u"\u2220\u20d2", - "nap;": u"\u2249", - "napE;": u"\u2a70\u0338", - "napid;": u"\u224b\u0338", - "napos;": u"\u0149", - "napprox;": u"\u2249", - "natur;": u"\u266e", - "natural;": u"\u266e", - "naturals;": u"\u2115", - "nbsp": u"\xa0", - "nbsp;": u"\xa0", - "nbump;": u"\u224e\u0338", - "nbumpe;": u"\u224f\u0338", - "ncap;": u"\u2a43", - "ncaron;": u"\u0148", - "ncedil;": u"\u0146", - "ncong;": u"\u2247", - "ncongdot;": u"\u2a6d\u0338", - "ncup;": u"\u2a42", - "ncy;": u"\u043d", - "ndash;": u"\u2013", - "ne;": u"\u2260", - "neArr;": u"\u21d7", - "nearhk;": u"\u2924", - "nearr;": u"\u2197", - "nearrow;": u"\u2197", - "nedot;": u"\u2250\u0338", - "nequiv;": u"\u2262", - "nesear;": u"\u2928", - "nesim;": u"\u2242\u0338", - "nexist;": u"\u2204", - "nexists;": u"\u2204", - "nfr;": u"\U0001d52b", - "ngE;": u"\u2267\u0338", - "nge;": u"\u2271", - "ngeq;": u"\u2271", - "ngeqq;": u"\u2267\u0338", - "ngeqslant;": u"\u2a7e\u0338", - "nges;": u"\u2a7e\u0338", - "ngsim;": u"\u2275", - "ngt;": u"\u226f", - "ngtr;": u"\u226f", - "nhArr;": u"\u21ce", - "nharr;": u"\u21ae", - "nhpar;": u"\u2af2", - "ni;": u"\u220b", - "nis;": u"\u22fc", - "nisd;": u"\u22fa", - "niv;": u"\u220b", - "njcy;": u"\u045a", - "nlArr;": u"\u21cd", - "nlE;": u"\u2266\u0338", - "nlarr;": u"\u219a", - "nldr;": u"\u2025", - "nle;": u"\u2270", - "nleftarrow;": u"\u219a", - "nleftrightarrow;": u"\u21ae", - "nleq;": u"\u2270", - "nleqq;": u"\u2266\u0338", - "nleqslant;": u"\u2a7d\u0338", - "nles;": u"\u2a7d\u0338", - "nless;": u"\u226e", - "nlsim;": u"\u2274", - "nlt;": u"\u226e", - "nltri;": u"\u22ea", - "nltrie;": u"\u22ec", - "nmid;": u"\u2224", - "nopf;": u"\U0001d55f", - "not": u"\xac", - "not;": u"\xac", - "notin;": u"\u2209", - "notinE;": u"\u22f9\u0338", - "notindot;": u"\u22f5\u0338", - "notinva;": u"\u2209", - "notinvb;": u"\u22f7", - "notinvc;": u"\u22f6", - "notni;": u"\u220c", - "notniva;": u"\u220c", - "notnivb;": u"\u22fe", - "notnivc;": u"\u22fd", - "npar;": u"\u2226", - "nparallel;": u"\u2226", - "nparsl;": u"\u2afd\u20e5", - "npart;": u"\u2202\u0338", - "npolint;": u"\u2a14", - "npr;": u"\u2280", - "nprcue;": u"\u22e0", - "npre;": u"\u2aaf\u0338", - "nprec;": u"\u2280", - "npreceq;": u"\u2aaf\u0338", - "nrArr;": u"\u21cf", - "nrarr;": u"\u219b", - "nrarrc;": u"\u2933\u0338", - "nrarrw;": u"\u219d\u0338", - "nrightarrow;": u"\u219b", - "nrtri;": u"\u22eb", - "nrtrie;": u"\u22ed", - "nsc;": u"\u2281", - "nsccue;": u"\u22e1", - "nsce;": u"\u2ab0\u0338", - "nscr;": u"\U0001d4c3", - "nshortmid;": u"\u2224", - "nshortparallel;": u"\u2226", - "nsim;": u"\u2241", - "nsime;": u"\u2244", - "nsimeq;": u"\u2244", - "nsmid;": u"\u2224", - "nspar;": u"\u2226", - "nsqsube;": u"\u22e2", - "nsqsupe;": u"\u22e3", - "nsub;": u"\u2284", - "nsubE;": u"\u2ac5\u0338", - "nsube;": u"\u2288", - "nsubset;": u"\u2282\u20d2", - "nsubseteq;": u"\u2288", - "nsubseteqq;": u"\u2ac5\u0338", - "nsucc;": u"\u2281", - "nsucceq;": u"\u2ab0\u0338", - "nsup;": u"\u2285", - "nsupE;": u"\u2ac6\u0338", - "nsupe;": u"\u2289", - "nsupset;": u"\u2283\u20d2", - "nsupseteq;": u"\u2289", - "nsupseteqq;": u"\u2ac6\u0338", - "ntgl;": u"\u2279", - "ntilde": u"\xf1", - "ntilde;": u"\xf1", - "ntlg;": u"\u2278", - "ntriangleleft;": u"\u22ea", - "ntrianglelefteq;": u"\u22ec", - "ntriangleright;": u"\u22eb", - "ntrianglerighteq;": u"\u22ed", - "nu;": u"\u03bd", - "num;": u"#", - "numero;": u"\u2116", - "numsp;": u"\u2007", - "nvDash;": u"\u22ad", - "nvHarr;": u"\u2904", - "nvap;": u"\u224d\u20d2", - "nvdash;": u"\u22ac", - "nvge;": u"\u2265\u20d2", - "nvgt;": u">\u20d2", - "nvinfin;": u"\u29de", - "nvlArr;": u"\u2902", - "nvle;": u"\u2264\u20d2", - "nvlt;": u"<\u20d2", - "nvltrie;": u"\u22b4\u20d2", - "nvrArr;": u"\u2903", - "nvrtrie;": u"\u22b5\u20d2", - "nvsim;": u"\u223c\u20d2", - "nwArr;": u"\u21d6", - "nwarhk;": u"\u2923", - "nwarr;": u"\u2196", - "nwarrow;": u"\u2196", - "nwnear;": u"\u2927", - "oS;": u"\u24c8", - "oacute": u"\xf3", - "oacute;": u"\xf3", - "oast;": u"\u229b", - "ocir;": u"\u229a", - "ocirc": u"\xf4", - "ocirc;": u"\xf4", - "ocy;": u"\u043e", - "odash;": u"\u229d", - "odblac;": u"\u0151", - "odiv;": u"\u2a38", - "odot;": u"\u2299", - "odsold;": u"\u29bc", - "oelig;": u"\u0153", - "ofcir;": u"\u29bf", - "ofr;": u"\U0001d52c", - "ogon;": u"\u02db", - "ograve": u"\xf2", - "ograve;": u"\xf2", - "ogt;": u"\u29c1", - "ohbar;": u"\u29b5", - "ohm;": u"\u03a9", - "oint;": u"\u222e", - "olarr;": u"\u21ba", - "olcir;": u"\u29be", - "olcross;": u"\u29bb", - "oline;": u"\u203e", - "olt;": u"\u29c0", - "omacr;": u"\u014d", - "omega;": u"\u03c9", - "omicron;": u"\u03bf", - "omid;": u"\u29b6", - "ominus;": u"\u2296", - "oopf;": u"\U0001d560", - "opar;": u"\u29b7", - "operp;": u"\u29b9", - "oplus;": u"\u2295", - "or;": u"\u2228", - "orarr;": u"\u21bb", - "ord;": u"\u2a5d", - "order;": u"\u2134", - "orderof;": u"\u2134", - "ordf": u"\xaa", - "ordf;": u"\xaa", - "ordm": u"\xba", - "ordm;": u"\xba", - "origof;": u"\u22b6", - "oror;": u"\u2a56", - "orslope;": u"\u2a57", - "orv;": u"\u2a5b", - "oscr;": u"\u2134", - "oslash": u"\xf8", - "oslash;": u"\xf8", - "osol;": u"\u2298", - "otilde": u"\xf5", - "otilde;": u"\xf5", - "otimes;": u"\u2297", - "otimesas;": u"\u2a36", - "ouml": u"\xf6", - "ouml;": u"\xf6", - "ovbar;": u"\u233d", - "par;": u"\u2225", - "para": u"\xb6", - "para;": u"\xb6", - "parallel;": u"\u2225", - "parsim;": u"\u2af3", - "parsl;": u"\u2afd", - "part;": u"\u2202", - "pcy;": u"\u043f", - "percnt;": u"%", - "period;": u".", - "permil;": u"\u2030", - "perp;": u"\u22a5", - "pertenk;": u"\u2031", - "pfr;": u"\U0001d52d", - "phi;": u"\u03c6", - "phiv;": u"\u03d5", - "phmmat;": u"\u2133", - "phone;": u"\u260e", - "pi;": u"\u03c0", - "pitchfork;": u"\u22d4", - "piv;": u"\u03d6", - "planck;": u"\u210f", - "planckh;": u"\u210e", - "plankv;": u"\u210f", - "plus;": u"+", - "plusacir;": u"\u2a23", - "plusb;": u"\u229e", - "pluscir;": u"\u2a22", - "plusdo;": u"\u2214", - "plusdu;": u"\u2a25", - "pluse;": u"\u2a72", - "plusmn": u"\xb1", - "plusmn;": u"\xb1", - "plussim;": u"\u2a26", - "plustwo;": u"\u2a27", - "pm;": u"\xb1", - "pointint;": u"\u2a15", - "popf;": u"\U0001d561", - "pound": u"\xa3", - "pound;": u"\xa3", - "pr;": u"\u227a", - "prE;": u"\u2ab3", - "prap;": u"\u2ab7", - "prcue;": u"\u227c", - "pre;": u"\u2aaf", - "prec;": u"\u227a", - "precapprox;": u"\u2ab7", - "preccurlyeq;": u"\u227c", - "preceq;": u"\u2aaf", - "precnapprox;": u"\u2ab9", - "precneqq;": u"\u2ab5", - "precnsim;": u"\u22e8", - "precsim;": u"\u227e", - "prime;": u"\u2032", - "primes;": u"\u2119", - "prnE;": u"\u2ab5", - "prnap;": u"\u2ab9", - "prnsim;": u"\u22e8", - "prod;": u"\u220f", - "profalar;": u"\u232e", - "profline;": u"\u2312", - "profsurf;": u"\u2313", - "prop;": u"\u221d", - "propto;": u"\u221d", - "prsim;": u"\u227e", - "prurel;": u"\u22b0", - "pscr;": u"\U0001d4c5", - "psi;": u"\u03c8", - "puncsp;": u"\u2008", - "qfr;": u"\U0001d52e", - "qint;": u"\u2a0c", - "qopf;": u"\U0001d562", - "qprime;": u"\u2057", - "qscr;": u"\U0001d4c6", - "quaternions;": u"\u210d", - "quatint;": u"\u2a16", - "quest;": u"?", - "questeq;": u"\u225f", - "quot": u"\"", - "quot;": u"\"", - "rAarr;": u"\u21db", - "rArr;": u"\u21d2", - "rAtail;": u"\u291c", - "rBarr;": u"\u290f", - "rHar;": u"\u2964", - "race;": u"\u223d\u0331", - "racute;": u"\u0155", - "radic;": u"\u221a", - "raemptyv;": u"\u29b3", - "rang;": u"\u27e9", - "rangd;": u"\u2992", - "range;": u"\u29a5", - "rangle;": u"\u27e9", - "raquo": u"\xbb", - "raquo;": u"\xbb", - "rarr;": u"\u2192", - "rarrap;": u"\u2975", - "rarrb;": u"\u21e5", - "rarrbfs;": u"\u2920", - "rarrc;": u"\u2933", - "rarrfs;": u"\u291e", - "rarrhk;": u"\u21aa", - "rarrlp;": u"\u21ac", - "rarrpl;": u"\u2945", - "rarrsim;": u"\u2974", - "rarrtl;": u"\u21a3", - "rarrw;": u"\u219d", - "ratail;": u"\u291a", - "ratio;": u"\u2236", - "rationals;": u"\u211a", - "rbarr;": u"\u290d", - "rbbrk;": u"\u2773", - "rbrace;": u"}", - "rbrack;": u"]", - "rbrke;": u"\u298c", - "rbrksld;": u"\u298e", - "rbrkslu;": u"\u2990", - "rcaron;": u"\u0159", - "rcedil;": u"\u0157", - "rceil;": u"\u2309", - "rcub;": u"}", - "rcy;": u"\u0440", - "rdca;": u"\u2937", - "rdldhar;": u"\u2969", - "rdquo;": u"\u201d", - "rdquor;": u"\u201d", - "rdsh;": u"\u21b3", - "real;": u"\u211c", - "realine;": u"\u211b", - "realpart;": u"\u211c", - "reals;": u"\u211d", - "rect;": u"\u25ad", - "reg": u"\xae", - "reg;": u"\xae", - "rfisht;": u"\u297d", - "rfloor;": u"\u230b", - "rfr;": u"\U0001d52f", - "rhard;": u"\u21c1", - "rharu;": u"\u21c0", - "rharul;": u"\u296c", - "rho;": u"\u03c1", - "rhov;": u"\u03f1", - "rightarrow;": u"\u2192", - "rightarrowtail;": u"\u21a3", - "rightharpoondown;": u"\u21c1", - "rightharpoonup;": u"\u21c0", - "rightleftarrows;": u"\u21c4", - "rightleftharpoons;": u"\u21cc", - "rightrightarrows;": u"\u21c9", - "rightsquigarrow;": u"\u219d", - "rightthreetimes;": u"\u22cc", - "ring;": u"\u02da", - "risingdotseq;": u"\u2253", - "rlarr;": u"\u21c4", - "rlhar;": u"\u21cc", - "rlm;": u"\u200f", - "rmoust;": u"\u23b1", - "rmoustache;": u"\u23b1", - "rnmid;": u"\u2aee", - "roang;": u"\u27ed", - "roarr;": u"\u21fe", - "robrk;": u"\u27e7", - "ropar;": u"\u2986", - "ropf;": u"\U0001d563", - "roplus;": u"\u2a2e", - "rotimes;": u"\u2a35", - "rpar;": u")", - "rpargt;": u"\u2994", - "rppolint;": u"\u2a12", - "rrarr;": u"\u21c9", - "rsaquo;": u"\u203a", - "rscr;": u"\U0001d4c7", - "rsh;": u"\u21b1", - "rsqb;": u"]", - "rsquo;": u"\u2019", - "rsquor;": u"\u2019", - "rthree;": u"\u22cc", - "rtimes;": u"\u22ca", - "rtri;": u"\u25b9", - "rtrie;": u"\u22b5", - "rtrif;": u"\u25b8", - "rtriltri;": u"\u29ce", - "ruluhar;": u"\u2968", - "rx;": u"\u211e", - "sacute;": u"\u015b", - "sbquo;": u"\u201a", - "sc;": u"\u227b", - "scE;": u"\u2ab4", - "scap;": u"\u2ab8", - "scaron;": u"\u0161", - "sccue;": u"\u227d", - "sce;": u"\u2ab0", - "scedil;": u"\u015f", - "scirc;": u"\u015d", - "scnE;": u"\u2ab6", - "scnap;": u"\u2aba", - "scnsim;": u"\u22e9", - "scpolint;": u"\u2a13", - "scsim;": u"\u227f", - "scy;": u"\u0441", - "sdot;": u"\u22c5", - "sdotb;": u"\u22a1", - "sdote;": u"\u2a66", - "seArr;": u"\u21d8", - "searhk;": u"\u2925", - "searr;": u"\u2198", - "searrow;": u"\u2198", - "sect": u"\xa7", - "sect;": u"\xa7", - "semi;": u";", - "seswar;": u"\u2929", - "setminus;": u"\u2216", - "setmn;": u"\u2216", - "sext;": u"\u2736", - "sfr;": u"\U0001d530", - "sfrown;": u"\u2322", - "sharp;": u"\u266f", - "shchcy;": u"\u0449", - "shcy;": u"\u0448", - "shortmid;": u"\u2223", - "shortparallel;": u"\u2225", - "shy": u"\xad", - "shy;": u"\xad", - "sigma;": u"\u03c3", - "sigmaf;": u"\u03c2", - "sigmav;": u"\u03c2", - "sim;": u"\u223c", - "simdot;": u"\u2a6a", - "sime;": u"\u2243", - "simeq;": u"\u2243", - "simg;": u"\u2a9e", - "simgE;": u"\u2aa0", - "siml;": u"\u2a9d", - "simlE;": u"\u2a9f", - "simne;": u"\u2246", - "simplus;": u"\u2a24", - "simrarr;": u"\u2972", - "slarr;": u"\u2190", - "smallsetminus;": u"\u2216", - "smashp;": u"\u2a33", - "smeparsl;": u"\u29e4", - "smid;": u"\u2223", - "smile;": u"\u2323", - "smt;": u"\u2aaa", - "smte;": u"\u2aac", - "smtes;": u"\u2aac\ufe00", - "softcy;": u"\u044c", - "sol;": u"/", - "solb;": u"\u29c4", - "solbar;": u"\u233f", - "sopf;": u"\U0001d564", - "spades;": u"\u2660", - "spadesuit;": u"\u2660", - "spar;": u"\u2225", - "sqcap;": u"\u2293", - "sqcaps;": u"\u2293\ufe00", - "sqcup;": u"\u2294", - "sqcups;": u"\u2294\ufe00", - "sqsub;": u"\u228f", - "sqsube;": u"\u2291", - "sqsubset;": u"\u228f", - "sqsubseteq;": u"\u2291", - "sqsup;": u"\u2290", - "sqsupe;": u"\u2292", - "sqsupset;": u"\u2290", - "sqsupseteq;": u"\u2292", - "squ;": u"\u25a1", - "square;": u"\u25a1", - "squarf;": u"\u25aa", - "squf;": u"\u25aa", - "srarr;": u"\u2192", - "sscr;": u"\U0001d4c8", - "ssetmn;": u"\u2216", - "ssmile;": u"\u2323", - "sstarf;": u"\u22c6", - "star;": u"\u2606", - "starf;": u"\u2605", - "straightepsilon;": u"\u03f5", - "straightphi;": u"\u03d5", - "strns;": u"\xaf", - "sub;": u"\u2282", - "subE;": u"\u2ac5", - "subdot;": u"\u2abd", - "sube;": u"\u2286", - "subedot;": u"\u2ac3", - "submult;": u"\u2ac1", - "subnE;": u"\u2acb", - "subne;": u"\u228a", - "subplus;": u"\u2abf", - "subrarr;": u"\u2979", - "subset;": u"\u2282", - "subseteq;": u"\u2286", - "subseteqq;": u"\u2ac5", - "subsetneq;": u"\u228a", - "subsetneqq;": u"\u2acb", - "subsim;": u"\u2ac7", - "subsub;": u"\u2ad5", - "subsup;": u"\u2ad3", - "succ;": u"\u227b", - "succapprox;": u"\u2ab8", - "succcurlyeq;": u"\u227d", - "succeq;": u"\u2ab0", - "succnapprox;": u"\u2aba", - "succneqq;": u"\u2ab6", - "succnsim;": u"\u22e9", - "succsim;": u"\u227f", - "sum;": u"\u2211", - "sung;": u"\u266a", - "sup1": u"\xb9", - "sup1;": u"\xb9", - "sup2": u"\xb2", - "sup2;": u"\xb2", - "sup3": u"\xb3", - "sup3;": u"\xb3", - "sup;": u"\u2283", - "supE;": u"\u2ac6", - "supdot;": u"\u2abe", - "supdsub;": u"\u2ad8", - "supe;": u"\u2287", - "supedot;": u"\u2ac4", - "suphsol;": u"\u27c9", - "suphsub;": u"\u2ad7", - "suplarr;": u"\u297b", - "supmult;": u"\u2ac2", - "supnE;": u"\u2acc", - "supne;": u"\u228b", - "supplus;": u"\u2ac0", - "supset;": u"\u2283", - "supseteq;": u"\u2287", - "supseteqq;": u"\u2ac6", - "supsetneq;": u"\u228b", - "supsetneqq;": u"\u2acc", - "supsim;": u"\u2ac8", - "supsub;": u"\u2ad4", - "supsup;": u"\u2ad6", - "swArr;": u"\u21d9", - "swarhk;": u"\u2926", - "swarr;": u"\u2199", - "swarrow;": u"\u2199", - "swnwar;": u"\u292a", - "szlig": u"\xdf", - "szlig;": u"\xdf", - "target;": u"\u2316", - "tau;": u"\u03c4", - "tbrk;": u"\u23b4", - "tcaron;": u"\u0165", - "tcedil;": u"\u0163", - "tcy;": u"\u0442", - "tdot;": u"\u20db", - "telrec;": u"\u2315", - "tfr;": u"\U0001d531", - "there4;": u"\u2234", - "therefore;": u"\u2234", - "theta;": u"\u03b8", - "thetasym;": u"\u03d1", - "thetav;": u"\u03d1", - "thickapprox;": u"\u2248", - "thicksim;": u"\u223c", - "thinsp;": u"\u2009", - "thkap;": u"\u2248", - "thksim;": u"\u223c", - "thorn": u"\xfe", - "thorn;": u"\xfe", - "tilde;": u"\u02dc", - "times": u"\xd7", - "times;": u"\xd7", - "timesb;": u"\u22a0", - "timesbar;": u"\u2a31", - "timesd;": u"\u2a30", - "tint;": u"\u222d", - "toea;": u"\u2928", - "top;": u"\u22a4", - "topbot;": u"\u2336", - "topcir;": u"\u2af1", - "topf;": u"\U0001d565", - "topfork;": u"\u2ada", - "tosa;": u"\u2929", - "tprime;": u"\u2034", - "trade;": u"\u2122", - "triangle;": u"\u25b5", - "triangledown;": u"\u25bf", - "triangleleft;": u"\u25c3", - "trianglelefteq;": u"\u22b4", - "triangleq;": u"\u225c", - "triangleright;": u"\u25b9", - "trianglerighteq;": u"\u22b5", - "tridot;": u"\u25ec", - "trie;": u"\u225c", - "triminus;": u"\u2a3a", - "triplus;": u"\u2a39", - "trisb;": u"\u29cd", - "tritime;": u"\u2a3b", - "trpezium;": u"\u23e2", - "tscr;": u"\U0001d4c9", - "tscy;": u"\u0446", - "tshcy;": u"\u045b", - "tstrok;": u"\u0167", - "twixt;": u"\u226c", - "twoheadleftarrow;": u"\u219e", - "twoheadrightarrow;": u"\u21a0", - "uArr;": u"\u21d1", - "uHar;": u"\u2963", - "uacute": u"\xfa", - "uacute;": u"\xfa", - "uarr;": u"\u2191", - "ubrcy;": u"\u045e", - "ubreve;": u"\u016d", - "ucirc": u"\xfb", - "ucirc;": u"\xfb", - "ucy;": u"\u0443", - "udarr;": u"\u21c5", - "udblac;": u"\u0171", - "udhar;": u"\u296e", - "ufisht;": u"\u297e", - "ufr;": u"\U0001d532", - "ugrave": u"\xf9", - "ugrave;": u"\xf9", - "uharl;": u"\u21bf", - "uharr;": u"\u21be", - "uhblk;": u"\u2580", - "ulcorn;": u"\u231c", - "ulcorner;": u"\u231c", - "ulcrop;": u"\u230f", - "ultri;": u"\u25f8", - "umacr;": u"\u016b", - "uml": u"\xa8", - "uml;": u"\xa8", - "uogon;": u"\u0173", - "uopf;": u"\U0001d566", - "uparrow;": u"\u2191", - "updownarrow;": u"\u2195", - "upharpoonleft;": u"\u21bf", - "upharpoonright;": u"\u21be", - "uplus;": u"\u228e", - "upsi;": u"\u03c5", - "upsih;": u"\u03d2", - "upsilon;": u"\u03c5", - "upuparrows;": u"\u21c8", - "urcorn;": u"\u231d", - "urcorner;": u"\u231d", - "urcrop;": u"\u230e", - "uring;": u"\u016f", - "urtri;": u"\u25f9", - "uscr;": u"\U0001d4ca", - "utdot;": u"\u22f0", - "utilde;": u"\u0169", - "utri;": u"\u25b5", - "utrif;": u"\u25b4", - "uuarr;": u"\u21c8", - "uuml": u"\xfc", - "uuml;": u"\xfc", - "uwangle;": u"\u29a7", - "vArr;": u"\u21d5", - "vBar;": u"\u2ae8", - "vBarv;": u"\u2ae9", - "vDash;": u"\u22a8", - "vangrt;": u"\u299c", - "varepsilon;": u"\u03f5", - "varkappa;": u"\u03f0", - "varnothing;": u"\u2205", - "varphi;": u"\u03d5", - "varpi;": u"\u03d6", - "varpropto;": u"\u221d", - "varr;": u"\u2195", - "varrho;": u"\u03f1", - "varsigma;": u"\u03c2", - "varsubsetneq;": u"\u228a\ufe00", - "varsubsetneqq;": u"\u2acb\ufe00", - "varsupsetneq;": u"\u228b\ufe00", - "varsupsetneqq;": u"\u2acc\ufe00", - "vartheta;": u"\u03d1", - "vartriangleleft;": u"\u22b2", - "vartriangleright;": u"\u22b3", - "vcy;": u"\u0432", - "vdash;": u"\u22a2", - "vee;": u"\u2228", - "veebar;": u"\u22bb", - "veeeq;": u"\u225a", - "vellip;": u"\u22ee", - "verbar;": u"|", - "vert;": u"|", - "vfr;": u"\U0001d533", - "vltri;": u"\u22b2", - "vnsub;": u"\u2282\u20d2", - "vnsup;": u"\u2283\u20d2", - "vopf;": u"\U0001d567", - "vprop;": u"\u221d", - "vrtri;": u"\u22b3", - "vscr;": u"\U0001d4cb", - "vsubnE;": u"\u2acb\ufe00", - "vsubne;": u"\u228a\ufe00", - "vsupnE;": u"\u2acc\ufe00", - "vsupne;": u"\u228b\ufe00", - "vzigzag;": u"\u299a", - "wcirc;": u"\u0175", - "wedbar;": u"\u2a5f", - "wedge;": u"\u2227", - "wedgeq;": u"\u2259", - "weierp;": u"\u2118", - "wfr;": u"\U0001d534", - "wopf;": u"\U0001d568", - "wp;": u"\u2118", - "wr;": u"\u2240", - "wreath;": u"\u2240", - "wscr;": u"\U0001d4cc", - "xcap;": u"\u22c2", - "xcirc;": u"\u25ef", - "xcup;": u"\u22c3", - "xdtri;": u"\u25bd", - "xfr;": u"\U0001d535", - "xhArr;": u"\u27fa", - "xharr;": u"\u27f7", - "xi;": u"\u03be", - "xlArr;": u"\u27f8", - "xlarr;": u"\u27f5", - "xmap;": u"\u27fc", - "xnis;": u"\u22fb", - "xodot;": u"\u2a00", - "xopf;": u"\U0001d569", - "xoplus;": u"\u2a01", - "xotime;": u"\u2a02", - "xrArr;": u"\u27f9", - "xrarr;": u"\u27f6", - "xscr;": u"\U0001d4cd", - "xsqcup;": u"\u2a06", - "xuplus;": u"\u2a04", - "xutri;": u"\u25b3", - "xvee;": u"\u22c1", - "xwedge;": u"\u22c0", - "yacute": u"\xfd", - "yacute;": u"\xfd", - "yacy;": u"\u044f", - "ycirc;": u"\u0177", - "ycy;": u"\u044b", - "yen": u"\xa5", - "yen;": u"\xa5", - "yfr;": u"\U0001d536", - "yicy;": u"\u0457", - "yopf;": u"\U0001d56a", - "yscr;": u"\U0001d4ce", - "yucy;": u"\u044e", - "yuml": u"\xff", - "yuml;": u"\xff", - "zacute;": u"\u017a", - "zcaron;": u"\u017e", - "zcy;": u"\u0437", - "zdot;": u"\u017c", - "zeetrf;": u"\u2128", - "zeta;": u"\u03b6", - "zfr;": u"\U0001d537", - "zhcy;": u"\u0436", - "zigrarr;": u"\u21dd", - "zopf;": u"\U0001d56b", - "zscr;": u"\U0001d4cf", - "zwj;": u"\u200d", - "zwnj;": u"\u200c", + "AElig": "\xc6", + "AElig;": "\xc6", + "AMP": "&", + "AMP;": "&", + "Aacute": "\xc1", + "Aacute;": "\xc1", + "Abreve;": "\u0102", + "Acirc": "\xc2", + "Acirc;": "\xc2", + "Acy;": "\u0410", + "Afr;": "\U0001d504", + "Agrave": "\xc0", + "Agrave;": "\xc0", + "Alpha;": "\u0391", + "Amacr;": "\u0100", + "And;": "\u2a53", + "Aogon;": "\u0104", + "Aopf;": "\U0001d538", + "ApplyFunction;": "\u2061", + "Aring": "\xc5", + "Aring;": "\xc5", + "Ascr;": "\U0001d49c", + "Assign;": "\u2254", + "Atilde": "\xc3", + "Atilde;": "\xc3", + "Auml": "\xc4", + "Auml;": "\xc4", + "Backslash;": "\u2216", + "Barv;": "\u2ae7", + "Barwed;": "\u2306", + "Bcy;": "\u0411", + "Because;": "\u2235", + "Bernoullis;": "\u212c", + "Beta;": "\u0392", + "Bfr;": "\U0001d505", + "Bopf;": "\U0001d539", + "Breve;": "\u02d8", + "Bscr;": "\u212c", + "Bumpeq;": "\u224e", + "CHcy;": "\u0427", + "COPY": "\xa9", + "COPY;": "\xa9", + "Cacute;": "\u0106", + "Cap;": "\u22d2", + "CapitalDifferentialD;": "\u2145", + "Cayleys;": "\u212d", + "Ccaron;": "\u010c", + "Ccedil": "\xc7", + "Ccedil;": "\xc7", + "Ccirc;": "\u0108", + "Cconint;": "\u2230", + "Cdot;": "\u010a", + "Cedilla;": "\xb8", + "CenterDot;": "\xb7", + "Cfr;": "\u212d", + "Chi;": "\u03a7", + "CircleDot;": "\u2299", + "CircleMinus;": "\u2296", + "CirclePlus;": "\u2295", + "CircleTimes;": "\u2297", + "ClockwiseContourIntegral;": "\u2232", + "CloseCurlyDoubleQuote;": "\u201d", + "CloseCurlyQuote;": "\u2019", + "Colon;": "\u2237", + "Colone;": "\u2a74", + "Congruent;": "\u2261", + "Conint;": "\u222f", + "ContourIntegral;": "\u222e", + "Copf;": "\u2102", + "Coproduct;": "\u2210", + "CounterClockwiseContourIntegral;": "\u2233", + "Cross;": "\u2a2f", + "Cscr;": "\U0001d49e", + "Cup;": "\u22d3", + "CupCap;": "\u224d", + "DD;": "\u2145", + "DDotrahd;": "\u2911", + "DJcy;": "\u0402", + "DScy;": "\u0405", + "DZcy;": "\u040f", + "Dagger;": "\u2021", + "Darr;": "\u21a1", + "Dashv;": "\u2ae4", + "Dcaron;": "\u010e", + "Dcy;": "\u0414", + "Del;": "\u2207", + "Delta;": "\u0394", + "Dfr;": "\U0001d507", + "DiacriticalAcute;": "\xb4", + "DiacriticalDot;": "\u02d9", + "DiacriticalDoubleAcute;": "\u02dd", + "DiacriticalGrave;": "`", + "DiacriticalTilde;": "\u02dc", + "Diamond;": "\u22c4", + "DifferentialD;": "\u2146", + "Dopf;": "\U0001d53b", + "Dot;": "\xa8", + "DotDot;": "\u20dc", + "DotEqual;": "\u2250", + "DoubleContourIntegral;": "\u222f", + "DoubleDot;": "\xa8", + "DoubleDownArrow;": "\u21d3", + "DoubleLeftArrow;": "\u21d0", + "DoubleLeftRightArrow;": "\u21d4", + "DoubleLeftTee;": "\u2ae4", + "DoubleLongLeftArrow;": "\u27f8", + "DoubleLongLeftRightArrow;": "\u27fa", + "DoubleLongRightArrow;": "\u27f9", + "DoubleRightArrow;": "\u21d2", + "DoubleRightTee;": "\u22a8", + "DoubleUpArrow;": "\u21d1", + "DoubleUpDownArrow;": "\u21d5", + "DoubleVerticalBar;": "\u2225", + "DownArrow;": "\u2193", + "DownArrowBar;": "\u2913", + "DownArrowUpArrow;": "\u21f5", + "DownBreve;": "\u0311", + "DownLeftRightVector;": "\u2950", + "DownLeftTeeVector;": "\u295e", + "DownLeftVector;": "\u21bd", + "DownLeftVectorBar;": "\u2956", + "DownRightTeeVector;": "\u295f", + "DownRightVector;": "\u21c1", + "DownRightVectorBar;": "\u2957", + "DownTee;": "\u22a4", + "DownTeeArrow;": "\u21a7", + "Downarrow;": "\u21d3", + "Dscr;": "\U0001d49f", + "Dstrok;": "\u0110", + "ENG;": "\u014a", + "ETH": "\xd0", + "ETH;": "\xd0", + "Eacute": "\xc9", + "Eacute;": "\xc9", + "Ecaron;": "\u011a", + "Ecirc": "\xca", + "Ecirc;": "\xca", + "Ecy;": "\u042d", + "Edot;": "\u0116", + "Efr;": "\U0001d508", + "Egrave": "\xc8", + "Egrave;": "\xc8", + "Element;": "\u2208", + "Emacr;": "\u0112", + "EmptySmallSquare;": "\u25fb", + "EmptyVerySmallSquare;": "\u25ab", + "Eogon;": "\u0118", + "Eopf;": "\U0001d53c", + "Epsilon;": "\u0395", + "Equal;": "\u2a75", + "EqualTilde;": "\u2242", + "Equilibrium;": "\u21cc", + "Escr;": "\u2130", + "Esim;": "\u2a73", + "Eta;": "\u0397", + "Euml": "\xcb", + "Euml;": "\xcb", + "Exists;": "\u2203", + "ExponentialE;": "\u2147", + "Fcy;": "\u0424", + "Ffr;": "\U0001d509", + "FilledSmallSquare;": "\u25fc", + "FilledVerySmallSquare;": "\u25aa", + "Fopf;": "\U0001d53d", + "ForAll;": "\u2200", + "Fouriertrf;": "\u2131", + "Fscr;": "\u2131", + "GJcy;": "\u0403", + "GT": ">", + "GT;": ">", + "Gamma;": "\u0393", + "Gammad;": "\u03dc", + "Gbreve;": "\u011e", + "Gcedil;": "\u0122", + "Gcirc;": "\u011c", + "Gcy;": "\u0413", + "Gdot;": "\u0120", + "Gfr;": "\U0001d50a", + "Gg;": "\u22d9", + "Gopf;": "\U0001d53e", + "GreaterEqual;": "\u2265", + "GreaterEqualLess;": "\u22db", + "GreaterFullEqual;": "\u2267", + "GreaterGreater;": "\u2aa2", + "GreaterLess;": "\u2277", + "GreaterSlantEqual;": "\u2a7e", + "GreaterTilde;": "\u2273", + "Gscr;": "\U0001d4a2", + "Gt;": "\u226b", + "HARDcy;": "\u042a", + "Hacek;": "\u02c7", + "Hat;": "^", + "Hcirc;": "\u0124", + "Hfr;": "\u210c", + "HilbertSpace;": "\u210b", + "Hopf;": "\u210d", + "HorizontalLine;": "\u2500", + "Hscr;": "\u210b", + "Hstrok;": "\u0126", + "HumpDownHump;": "\u224e", + "HumpEqual;": "\u224f", + "IEcy;": "\u0415", + "IJlig;": "\u0132", + "IOcy;": "\u0401", + "Iacute": "\xcd", + "Iacute;": "\xcd", + "Icirc": "\xce", + "Icirc;": "\xce", + "Icy;": "\u0418", + "Idot;": "\u0130", + "Ifr;": "\u2111", + "Igrave": "\xcc", + "Igrave;": "\xcc", + "Im;": "\u2111", + "Imacr;": "\u012a", + "ImaginaryI;": "\u2148", + "Implies;": "\u21d2", + "Int;": "\u222c", + "Integral;": "\u222b", + "Intersection;": "\u22c2", + "InvisibleComma;": "\u2063", + "InvisibleTimes;": "\u2062", + "Iogon;": "\u012e", + "Iopf;": "\U0001d540", + "Iota;": "\u0399", + "Iscr;": "\u2110", + "Itilde;": "\u0128", + "Iukcy;": "\u0406", + "Iuml": "\xcf", + "Iuml;": "\xcf", + "Jcirc;": "\u0134", + "Jcy;": "\u0419", + "Jfr;": "\U0001d50d", + "Jopf;": "\U0001d541", + "Jscr;": "\U0001d4a5", + "Jsercy;": "\u0408", + "Jukcy;": "\u0404", + "KHcy;": "\u0425", + "KJcy;": "\u040c", + "Kappa;": "\u039a", + "Kcedil;": "\u0136", + "Kcy;": "\u041a", + "Kfr;": "\U0001d50e", + "Kopf;": "\U0001d542", + "Kscr;": "\U0001d4a6", + "LJcy;": "\u0409", + "LT": "<", + "LT;": "<", + "Lacute;": "\u0139", + "Lambda;": "\u039b", + "Lang;": "\u27ea", + "Laplacetrf;": "\u2112", + "Larr;": "\u219e", + "Lcaron;": "\u013d", + "Lcedil;": "\u013b", + "Lcy;": "\u041b", + "LeftAngleBracket;": "\u27e8", + "LeftArrow;": "\u2190", + "LeftArrowBar;": "\u21e4", + "LeftArrowRightArrow;": "\u21c6", + "LeftCeiling;": "\u2308", + "LeftDoubleBracket;": "\u27e6", + "LeftDownTeeVector;": "\u2961", + "LeftDownVector;": "\u21c3", + "LeftDownVectorBar;": "\u2959", + "LeftFloor;": "\u230a", + "LeftRightArrow;": "\u2194", + "LeftRightVector;": "\u294e", + "LeftTee;": "\u22a3", + "LeftTeeArrow;": "\u21a4", + "LeftTeeVector;": "\u295a", + "LeftTriangle;": "\u22b2", + "LeftTriangleBar;": "\u29cf", + "LeftTriangleEqual;": "\u22b4", + "LeftUpDownVector;": "\u2951", + "LeftUpTeeVector;": "\u2960", + "LeftUpVector;": "\u21bf", + "LeftUpVectorBar;": "\u2958", + "LeftVector;": "\u21bc", + "LeftVectorBar;": "\u2952", + "Leftarrow;": "\u21d0", + "Leftrightarrow;": "\u21d4", + "LessEqualGreater;": "\u22da", + "LessFullEqual;": "\u2266", + "LessGreater;": "\u2276", + "LessLess;": "\u2aa1", + "LessSlantEqual;": "\u2a7d", + "LessTilde;": "\u2272", + "Lfr;": "\U0001d50f", + "Ll;": "\u22d8", + "Lleftarrow;": "\u21da", + "Lmidot;": "\u013f", + "LongLeftArrow;": "\u27f5", + "LongLeftRightArrow;": "\u27f7", + "LongRightArrow;": "\u27f6", + "Longleftarrow;": "\u27f8", + "Longleftrightarrow;": "\u27fa", + "Longrightarrow;": "\u27f9", + "Lopf;": "\U0001d543", + "LowerLeftArrow;": "\u2199", + "LowerRightArrow;": "\u2198", + "Lscr;": "\u2112", + "Lsh;": "\u21b0", + "Lstrok;": "\u0141", + "Lt;": "\u226a", + "Map;": "\u2905", + "Mcy;": "\u041c", + "MediumSpace;": "\u205f", + "Mellintrf;": "\u2133", + "Mfr;": "\U0001d510", + "MinusPlus;": "\u2213", + "Mopf;": "\U0001d544", + "Mscr;": "\u2133", + "Mu;": "\u039c", + "NJcy;": "\u040a", + "Nacute;": "\u0143", + "Ncaron;": "\u0147", + "Ncedil;": "\u0145", + "Ncy;": "\u041d", + "NegativeMediumSpace;": "\u200b", + "NegativeThickSpace;": "\u200b", + "NegativeThinSpace;": "\u200b", + "NegativeVeryThinSpace;": "\u200b", + "NestedGreaterGreater;": "\u226b", + "NestedLessLess;": "\u226a", + "NewLine;": "\n", + "Nfr;": "\U0001d511", + "NoBreak;": "\u2060", + "NonBreakingSpace;": "\xa0", + "Nopf;": "\u2115", + "Not;": "\u2aec", + "NotCongruent;": "\u2262", + "NotCupCap;": "\u226d", + "NotDoubleVerticalBar;": "\u2226", + "NotElement;": "\u2209", + "NotEqual;": "\u2260", + "NotEqualTilde;": "\u2242\u0338", + "NotExists;": "\u2204", + "NotGreater;": "\u226f", + "NotGreaterEqual;": "\u2271", + "NotGreaterFullEqual;": "\u2267\u0338", + "NotGreaterGreater;": "\u226b\u0338", + "NotGreaterLess;": "\u2279", + "NotGreaterSlantEqual;": "\u2a7e\u0338", + "NotGreaterTilde;": "\u2275", + "NotHumpDownHump;": "\u224e\u0338", + "NotHumpEqual;": "\u224f\u0338", + "NotLeftTriangle;": "\u22ea", + "NotLeftTriangleBar;": "\u29cf\u0338", + "NotLeftTriangleEqual;": "\u22ec", + "NotLess;": "\u226e", + "NotLessEqual;": "\u2270", + "NotLessGreater;": "\u2278", + "NotLessLess;": "\u226a\u0338", + "NotLessSlantEqual;": "\u2a7d\u0338", + "NotLessTilde;": "\u2274", + "NotNestedGreaterGreater;": "\u2aa2\u0338", + "NotNestedLessLess;": "\u2aa1\u0338", + "NotPrecedes;": "\u2280", + "NotPrecedesEqual;": "\u2aaf\u0338", + "NotPrecedesSlantEqual;": "\u22e0", + "NotReverseElement;": "\u220c", + "NotRightTriangle;": "\u22eb", + "NotRightTriangleBar;": "\u29d0\u0338", + "NotRightTriangleEqual;": "\u22ed", + "NotSquareSubset;": "\u228f\u0338", + "NotSquareSubsetEqual;": "\u22e2", + "NotSquareSuperset;": "\u2290\u0338", + "NotSquareSupersetEqual;": "\u22e3", + "NotSubset;": "\u2282\u20d2", + "NotSubsetEqual;": "\u2288", + "NotSucceeds;": "\u2281", + "NotSucceedsEqual;": "\u2ab0\u0338", + "NotSucceedsSlantEqual;": "\u22e1", + "NotSucceedsTilde;": "\u227f\u0338", + "NotSuperset;": "\u2283\u20d2", + "NotSupersetEqual;": "\u2289", + "NotTilde;": "\u2241", + "NotTildeEqual;": "\u2244", + "NotTildeFullEqual;": "\u2247", + "NotTildeTilde;": "\u2249", + "NotVerticalBar;": "\u2224", + "Nscr;": "\U0001d4a9", + "Ntilde": "\xd1", + "Ntilde;": "\xd1", + "Nu;": "\u039d", + "OElig;": "\u0152", + "Oacute": "\xd3", + "Oacute;": "\xd3", + "Ocirc": "\xd4", + "Ocirc;": "\xd4", + "Ocy;": "\u041e", + "Odblac;": "\u0150", + "Ofr;": "\U0001d512", + "Ograve": "\xd2", + "Ograve;": "\xd2", + "Omacr;": "\u014c", + "Omega;": "\u03a9", + "Omicron;": "\u039f", + "Oopf;": "\U0001d546", + "OpenCurlyDoubleQuote;": "\u201c", + "OpenCurlyQuote;": "\u2018", + "Or;": "\u2a54", + "Oscr;": "\U0001d4aa", + "Oslash": "\xd8", + "Oslash;": "\xd8", + "Otilde": "\xd5", + "Otilde;": "\xd5", + "Otimes;": "\u2a37", + "Ouml": "\xd6", + "Ouml;": "\xd6", + "OverBar;": "\u203e", + "OverBrace;": "\u23de", + "OverBracket;": "\u23b4", + "OverParenthesis;": "\u23dc", + "PartialD;": "\u2202", + "Pcy;": "\u041f", + "Pfr;": "\U0001d513", + "Phi;": "\u03a6", + "Pi;": "\u03a0", + "PlusMinus;": "\xb1", + "Poincareplane;": "\u210c", + "Popf;": "\u2119", + "Pr;": "\u2abb", + "Precedes;": "\u227a", + "PrecedesEqual;": "\u2aaf", + "PrecedesSlantEqual;": "\u227c", + "PrecedesTilde;": "\u227e", + "Prime;": "\u2033", + "Product;": "\u220f", + "Proportion;": "\u2237", + "Proportional;": "\u221d", + "Pscr;": "\U0001d4ab", + "Psi;": "\u03a8", + "QUOT": "\"", + "QUOT;": "\"", + "Qfr;": "\U0001d514", + "Qopf;": "\u211a", + "Qscr;": "\U0001d4ac", + "RBarr;": "\u2910", + "REG": "\xae", + "REG;": "\xae", + "Racute;": "\u0154", + "Rang;": "\u27eb", + "Rarr;": "\u21a0", + "Rarrtl;": "\u2916", + "Rcaron;": "\u0158", + "Rcedil;": "\u0156", + "Rcy;": "\u0420", + "Re;": "\u211c", + "ReverseElement;": "\u220b", + "ReverseEquilibrium;": "\u21cb", + "ReverseUpEquilibrium;": "\u296f", + "Rfr;": "\u211c", + "Rho;": "\u03a1", + "RightAngleBracket;": "\u27e9", + "RightArrow;": "\u2192", + "RightArrowBar;": "\u21e5", + "RightArrowLeftArrow;": "\u21c4", + "RightCeiling;": "\u2309", + "RightDoubleBracket;": "\u27e7", + "RightDownTeeVector;": "\u295d", + "RightDownVector;": "\u21c2", + "RightDownVectorBar;": "\u2955", + "RightFloor;": "\u230b", + "RightTee;": "\u22a2", + "RightTeeArrow;": "\u21a6", + "RightTeeVector;": "\u295b", + "RightTriangle;": "\u22b3", + "RightTriangleBar;": "\u29d0", + "RightTriangleEqual;": "\u22b5", + "RightUpDownVector;": "\u294f", + "RightUpTeeVector;": "\u295c", + "RightUpVector;": "\u21be", + "RightUpVectorBar;": "\u2954", + "RightVector;": "\u21c0", + "RightVectorBar;": "\u2953", + "Rightarrow;": "\u21d2", + "Ropf;": "\u211d", + "RoundImplies;": "\u2970", + "Rrightarrow;": "\u21db", + "Rscr;": "\u211b", + "Rsh;": "\u21b1", + "RuleDelayed;": "\u29f4", + "SHCHcy;": "\u0429", + "SHcy;": "\u0428", + "SOFTcy;": "\u042c", + "Sacute;": "\u015a", + "Sc;": "\u2abc", + "Scaron;": "\u0160", + "Scedil;": "\u015e", + "Scirc;": "\u015c", + "Scy;": "\u0421", + "Sfr;": "\U0001d516", + "ShortDownArrow;": "\u2193", + "ShortLeftArrow;": "\u2190", + "ShortRightArrow;": "\u2192", + "ShortUpArrow;": "\u2191", + "Sigma;": "\u03a3", + "SmallCircle;": "\u2218", + "Sopf;": "\U0001d54a", + "Sqrt;": "\u221a", + "Square;": "\u25a1", + "SquareIntersection;": "\u2293", + "SquareSubset;": "\u228f", + "SquareSubsetEqual;": "\u2291", + "SquareSuperset;": "\u2290", + "SquareSupersetEqual;": "\u2292", + "SquareUnion;": "\u2294", + "Sscr;": "\U0001d4ae", + "Star;": "\u22c6", + "Sub;": "\u22d0", + "Subset;": "\u22d0", + "SubsetEqual;": "\u2286", + "Succeeds;": "\u227b", + "SucceedsEqual;": "\u2ab0", + "SucceedsSlantEqual;": "\u227d", + "SucceedsTilde;": "\u227f", + "SuchThat;": "\u220b", + "Sum;": "\u2211", + "Sup;": "\u22d1", + "Superset;": "\u2283", + "SupersetEqual;": "\u2287", + "Supset;": "\u22d1", + "THORN": "\xde", + "THORN;": "\xde", + "TRADE;": "\u2122", + "TSHcy;": "\u040b", + "TScy;": "\u0426", + "Tab;": "\t", + "Tau;": "\u03a4", + "Tcaron;": "\u0164", + "Tcedil;": "\u0162", + "Tcy;": "\u0422", + "Tfr;": "\U0001d517", + "Therefore;": "\u2234", + "Theta;": "\u0398", + "ThickSpace;": "\u205f\u200a", + "ThinSpace;": "\u2009", + "Tilde;": "\u223c", + "TildeEqual;": "\u2243", + "TildeFullEqual;": "\u2245", + "TildeTilde;": "\u2248", + "Topf;": "\U0001d54b", + "TripleDot;": "\u20db", + "Tscr;": "\U0001d4af", + "Tstrok;": "\u0166", + "Uacute": "\xda", + "Uacute;": "\xda", + "Uarr;": "\u219f", + "Uarrocir;": "\u2949", + "Ubrcy;": "\u040e", + "Ubreve;": "\u016c", + "Ucirc": "\xdb", + "Ucirc;": "\xdb", + "Ucy;": "\u0423", + "Udblac;": "\u0170", + "Ufr;": "\U0001d518", + "Ugrave": "\xd9", + "Ugrave;": "\xd9", + "Umacr;": "\u016a", + "UnderBar;": "_", + "UnderBrace;": "\u23df", + "UnderBracket;": "\u23b5", + "UnderParenthesis;": "\u23dd", + "Union;": "\u22c3", + "UnionPlus;": "\u228e", + "Uogon;": "\u0172", + "Uopf;": "\U0001d54c", + "UpArrow;": "\u2191", + "UpArrowBar;": "\u2912", + "UpArrowDownArrow;": "\u21c5", + "UpDownArrow;": "\u2195", + "UpEquilibrium;": "\u296e", + "UpTee;": "\u22a5", + "UpTeeArrow;": "\u21a5", + "Uparrow;": "\u21d1", + "Updownarrow;": "\u21d5", + "UpperLeftArrow;": "\u2196", + "UpperRightArrow;": "\u2197", + "Upsi;": "\u03d2", + "Upsilon;": "\u03a5", + "Uring;": "\u016e", + "Uscr;": "\U0001d4b0", + "Utilde;": "\u0168", + "Uuml": "\xdc", + "Uuml;": "\xdc", + "VDash;": "\u22ab", + "Vbar;": "\u2aeb", + "Vcy;": "\u0412", + "Vdash;": "\u22a9", + "Vdashl;": "\u2ae6", + "Vee;": "\u22c1", + "Verbar;": "\u2016", + "Vert;": "\u2016", + "VerticalBar;": "\u2223", + "VerticalLine;": "|", + "VerticalSeparator;": "\u2758", + "VerticalTilde;": "\u2240", + "VeryThinSpace;": "\u200a", + "Vfr;": "\U0001d519", + "Vopf;": "\U0001d54d", + "Vscr;": "\U0001d4b1", + "Vvdash;": "\u22aa", + "Wcirc;": "\u0174", + "Wedge;": "\u22c0", + "Wfr;": "\U0001d51a", + "Wopf;": "\U0001d54e", + "Wscr;": "\U0001d4b2", + "Xfr;": "\U0001d51b", + "Xi;": "\u039e", + "Xopf;": "\U0001d54f", + "Xscr;": "\U0001d4b3", + "YAcy;": "\u042f", + "YIcy;": "\u0407", + "YUcy;": "\u042e", + "Yacute": "\xdd", + "Yacute;": "\xdd", + "Ycirc;": "\u0176", + "Ycy;": "\u042b", + "Yfr;": "\U0001d51c", + "Yopf;": "\U0001d550", + "Yscr;": "\U0001d4b4", + "Yuml;": "\u0178", + "ZHcy;": "\u0416", + "Zacute;": "\u0179", + "Zcaron;": "\u017d", + "Zcy;": "\u0417", + "Zdot;": "\u017b", + "ZeroWidthSpace;": "\u200b", + "Zeta;": "\u0396", + "Zfr;": "\u2128", + "Zopf;": "\u2124", + "Zscr;": "\U0001d4b5", + "aacute": "\xe1", + "aacute;": "\xe1", + "abreve;": "\u0103", + "ac;": "\u223e", + "acE;": "\u223e\u0333", + "acd;": "\u223f", + "acirc": "\xe2", + "acirc;": "\xe2", + "acute": "\xb4", + "acute;": "\xb4", + "acy;": "\u0430", + "aelig": "\xe6", + "aelig;": "\xe6", + "af;": "\u2061", + "afr;": "\U0001d51e", + "agrave": "\xe0", + "agrave;": "\xe0", + "alefsym;": "\u2135", + "aleph;": "\u2135", + "alpha;": "\u03b1", + "amacr;": "\u0101", + "amalg;": "\u2a3f", + "amp": "&", + "amp;": "&", + "and;": "\u2227", + "andand;": "\u2a55", + "andd;": "\u2a5c", + "andslope;": "\u2a58", + "andv;": "\u2a5a", + "ang;": "\u2220", + "ange;": "\u29a4", + "angle;": "\u2220", + "angmsd;": "\u2221", + "angmsdaa;": "\u29a8", + "angmsdab;": "\u29a9", + "angmsdac;": "\u29aa", + "angmsdad;": "\u29ab", + "angmsdae;": "\u29ac", + "angmsdaf;": "\u29ad", + "angmsdag;": "\u29ae", + "angmsdah;": "\u29af", + "angrt;": "\u221f", + "angrtvb;": "\u22be", + "angrtvbd;": "\u299d", + "angsph;": "\u2222", + "angst;": "\xc5", + "angzarr;": "\u237c", + "aogon;": "\u0105", + "aopf;": "\U0001d552", + "ap;": "\u2248", + "apE;": "\u2a70", + "apacir;": "\u2a6f", + "ape;": "\u224a", + "apid;": "\u224b", + "apos;": "'", + "approx;": "\u2248", + "approxeq;": "\u224a", + "aring": "\xe5", + "aring;": "\xe5", + "ascr;": "\U0001d4b6", + "ast;": "*", + "asymp;": "\u2248", + "asympeq;": "\u224d", + "atilde": "\xe3", + "atilde;": "\xe3", + "auml": "\xe4", + "auml;": "\xe4", + "awconint;": "\u2233", + "awint;": "\u2a11", + "bNot;": "\u2aed", + "backcong;": "\u224c", + "backepsilon;": "\u03f6", + "backprime;": "\u2035", + "backsim;": "\u223d", + "backsimeq;": "\u22cd", + "barvee;": "\u22bd", + "barwed;": "\u2305", + "barwedge;": "\u2305", + "bbrk;": "\u23b5", + "bbrktbrk;": "\u23b6", + "bcong;": "\u224c", + "bcy;": "\u0431", + "bdquo;": "\u201e", + "becaus;": "\u2235", + "because;": "\u2235", + "bemptyv;": "\u29b0", + "bepsi;": "\u03f6", + "bernou;": "\u212c", + "beta;": "\u03b2", + "beth;": "\u2136", + "between;": "\u226c", + "bfr;": "\U0001d51f", + "bigcap;": "\u22c2", + "bigcirc;": "\u25ef", + "bigcup;": "\u22c3", + "bigodot;": "\u2a00", + "bigoplus;": "\u2a01", + "bigotimes;": "\u2a02", + "bigsqcup;": "\u2a06", + "bigstar;": "\u2605", + "bigtriangledown;": "\u25bd", + "bigtriangleup;": "\u25b3", + "biguplus;": "\u2a04", + "bigvee;": "\u22c1", + "bigwedge;": "\u22c0", + "bkarow;": "\u290d", + "blacklozenge;": "\u29eb", + "blacksquare;": "\u25aa", + "blacktriangle;": "\u25b4", + "blacktriangledown;": "\u25be", + "blacktriangleleft;": "\u25c2", + "blacktriangleright;": "\u25b8", + "blank;": "\u2423", + "blk12;": "\u2592", + "blk14;": "\u2591", + "blk34;": "\u2593", + "block;": "\u2588", + "bne;": "=\u20e5", + "bnequiv;": "\u2261\u20e5", + "bnot;": "\u2310", + "bopf;": "\U0001d553", + "bot;": "\u22a5", + "bottom;": "\u22a5", + "bowtie;": "\u22c8", + "boxDL;": "\u2557", + "boxDR;": "\u2554", + "boxDl;": "\u2556", + "boxDr;": "\u2553", + "boxH;": "\u2550", + "boxHD;": "\u2566", + "boxHU;": "\u2569", + "boxHd;": "\u2564", + "boxHu;": "\u2567", + "boxUL;": "\u255d", + "boxUR;": "\u255a", + "boxUl;": "\u255c", + "boxUr;": "\u2559", + "boxV;": "\u2551", + "boxVH;": "\u256c", + "boxVL;": "\u2563", + "boxVR;": "\u2560", + "boxVh;": "\u256b", + "boxVl;": "\u2562", + "boxVr;": "\u255f", + "boxbox;": "\u29c9", + "boxdL;": "\u2555", + "boxdR;": "\u2552", + "boxdl;": "\u2510", + "boxdr;": "\u250c", + "boxh;": "\u2500", + "boxhD;": "\u2565", + "boxhU;": "\u2568", + "boxhd;": "\u252c", + "boxhu;": "\u2534", + "boxminus;": "\u229f", + "boxplus;": "\u229e", + "boxtimes;": "\u22a0", + "boxuL;": "\u255b", + "boxuR;": "\u2558", + "boxul;": "\u2518", + "boxur;": "\u2514", + "boxv;": "\u2502", + "boxvH;": "\u256a", + "boxvL;": "\u2561", + "boxvR;": "\u255e", + "boxvh;": "\u253c", + "boxvl;": "\u2524", + "boxvr;": "\u251c", + "bprime;": "\u2035", + "breve;": "\u02d8", + "brvbar": "\xa6", + "brvbar;": "\xa6", + "bscr;": "\U0001d4b7", + "bsemi;": "\u204f", + "bsim;": "\u223d", + "bsime;": "\u22cd", + "bsol;": "\\", + "bsolb;": "\u29c5", + "bsolhsub;": "\u27c8", + "bull;": "\u2022", + "bullet;": "\u2022", + "bump;": "\u224e", + "bumpE;": "\u2aae", + "bumpe;": "\u224f", + "bumpeq;": "\u224f", + "cacute;": "\u0107", + "cap;": "\u2229", + "capand;": "\u2a44", + "capbrcup;": "\u2a49", + "capcap;": "\u2a4b", + "capcup;": "\u2a47", + "capdot;": "\u2a40", + "caps;": "\u2229\ufe00", + "caret;": "\u2041", + "caron;": "\u02c7", + "ccaps;": "\u2a4d", + "ccaron;": "\u010d", + "ccedil": "\xe7", + "ccedil;": "\xe7", + "ccirc;": "\u0109", + "ccups;": "\u2a4c", + "ccupssm;": "\u2a50", + "cdot;": "\u010b", + "cedil": "\xb8", + "cedil;": "\xb8", + "cemptyv;": "\u29b2", + "cent": "\xa2", + "cent;": "\xa2", + "centerdot;": "\xb7", + "cfr;": "\U0001d520", + "chcy;": "\u0447", + "check;": "\u2713", + "checkmark;": "\u2713", + "chi;": "\u03c7", + "cir;": "\u25cb", + "cirE;": "\u29c3", + "circ;": "\u02c6", + "circeq;": "\u2257", + "circlearrowleft;": "\u21ba", + "circlearrowright;": "\u21bb", + "circledR;": "\xae", + "circledS;": "\u24c8", + "circledast;": "\u229b", + "circledcirc;": "\u229a", + "circleddash;": "\u229d", + "cire;": "\u2257", + "cirfnint;": "\u2a10", + "cirmid;": "\u2aef", + "cirscir;": "\u29c2", + "clubs;": "\u2663", + "clubsuit;": "\u2663", + "colon;": ":", + "colone;": "\u2254", + "coloneq;": "\u2254", + "comma;": ",", + "commat;": "@", + "comp;": "\u2201", + "compfn;": "\u2218", + "complement;": "\u2201", + "complexes;": "\u2102", + "cong;": "\u2245", + "congdot;": "\u2a6d", + "conint;": "\u222e", + "copf;": "\U0001d554", + "coprod;": "\u2210", + "copy": "\xa9", + "copy;": "\xa9", + "copysr;": "\u2117", + "crarr;": "\u21b5", + "cross;": "\u2717", + "cscr;": "\U0001d4b8", + "csub;": "\u2acf", + "csube;": "\u2ad1", + "csup;": "\u2ad0", + "csupe;": "\u2ad2", + "ctdot;": "\u22ef", + "cudarrl;": "\u2938", + "cudarrr;": "\u2935", + "cuepr;": "\u22de", + "cuesc;": "\u22df", + "cularr;": "\u21b6", + "cularrp;": "\u293d", + "cup;": "\u222a", + "cupbrcap;": "\u2a48", + "cupcap;": "\u2a46", + "cupcup;": "\u2a4a", + "cupdot;": "\u228d", + "cupor;": "\u2a45", + "cups;": "\u222a\ufe00", + "curarr;": "\u21b7", + "curarrm;": "\u293c", + "curlyeqprec;": "\u22de", + "curlyeqsucc;": "\u22df", + "curlyvee;": "\u22ce", + "curlywedge;": "\u22cf", + "curren": "\xa4", + "curren;": "\xa4", + "curvearrowleft;": "\u21b6", + "curvearrowright;": "\u21b7", + "cuvee;": "\u22ce", + "cuwed;": "\u22cf", + "cwconint;": "\u2232", + "cwint;": "\u2231", + "cylcty;": "\u232d", + "dArr;": "\u21d3", + "dHar;": "\u2965", + "dagger;": "\u2020", + "daleth;": "\u2138", + "darr;": "\u2193", + "dash;": "\u2010", + "dashv;": "\u22a3", + "dbkarow;": "\u290f", + "dblac;": "\u02dd", + "dcaron;": "\u010f", + "dcy;": "\u0434", + "dd;": "\u2146", + "ddagger;": "\u2021", + "ddarr;": "\u21ca", + "ddotseq;": "\u2a77", + "deg": "\xb0", + "deg;": "\xb0", + "delta;": "\u03b4", + "demptyv;": "\u29b1", + "dfisht;": "\u297f", + "dfr;": "\U0001d521", + "dharl;": "\u21c3", + "dharr;": "\u21c2", + "diam;": "\u22c4", + "diamond;": "\u22c4", + "diamondsuit;": "\u2666", + "diams;": "\u2666", + "die;": "\xa8", + "digamma;": "\u03dd", + "disin;": "\u22f2", + "div;": "\xf7", + "divide": "\xf7", + "divide;": "\xf7", + "divideontimes;": "\u22c7", + "divonx;": "\u22c7", + "djcy;": "\u0452", + "dlcorn;": "\u231e", + "dlcrop;": "\u230d", + "dollar;": "$", + "dopf;": "\U0001d555", + "dot;": "\u02d9", + "doteq;": "\u2250", + "doteqdot;": "\u2251", + "dotminus;": "\u2238", + "dotplus;": "\u2214", + "dotsquare;": "\u22a1", + "doublebarwedge;": "\u2306", + "downarrow;": "\u2193", + "downdownarrows;": "\u21ca", + "downharpoonleft;": "\u21c3", + "downharpoonright;": "\u21c2", + "drbkarow;": "\u2910", + "drcorn;": "\u231f", + "drcrop;": "\u230c", + "dscr;": "\U0001d4b9", + "dscy;": "\u0455", + "dsol;": "\u29f6", + "dstrok;": "\u0111", + "dtdot;": "\u22f1", + "dtri;": "\u25bf", + "dtrif;": "\u25be", + "duarr;": "\u21f5", + "duhar;": "\u296f", + "dwangle;": "\u29a6", + "dzcy;": "\u045f", + "dzigrarr;": "\u27ff", + "eDDot;": "\u2a77", + "eDot;": "\u2251", + "eacute": "\xe9", + "eacute;": "\xe9", + "easter;": "\u2a6e", + "ecaron;": "\u011b", + "ecir;": "\u2256", + "ecirc": "\xea", + "ecirc;": "\xea", + "ecolon;": "\u2255", + "ecy;": "\u044d", + "edot;": "\u0117", + "ee;": "\u2147", + "efDot;": "\u2252", + "efr;": "\U0001d522", + "eg;": "\u2a9a", + "egrave": "\xe8", + "egrave;": "\xe8", + "egs;": "\u2a96", + "egsdot;": "\u2a98", + "el;": "\u2a99", + "elinters;": "\u23e7", + "ell;": "\u2113", + "els;": "\u2a95", + "elsdot;": "\u2a97", + "emacr;": "\u0113", + "empty;": "\u2205", + "emptyset;": "\u2205", + "emptyv;": "\u2205", + "emsp13;": "\u2004", + "emsp14;": "\u2005", + "emsp;": "\u2003", + "eng;": "\u014b", + "ensp;": "\u2002", + "eogon;": "\u0119", + "eopf;": "\U0001d556", + "epar;": "\u22d5", + "eparsl;": "\u29e3", + "eplus;": "\u2a71", + "epsi;": "\u03b5", + "epsilon;": "\u03b5", + "epsiv;": "\u03f5", + "eqcirc;": "\u2256", + "eqcolon;": "\u2255", + "eqsim;": "\u2242", + "eqslantgtr;": "\u2a96", + "eqslantless;": "\u2a95", + "equals;": "=", + "equest;": "\u225f", + "equiv;": "\u2261", + "equivDD;": "\u2a78", + "eqvparsl;": "\u29e5", + "erDot;": "\u2253", + "erarr;": "\u2971", + "escr;": "\u212f", + "esdot;": "\u2250", + "esim;": "\u2242", + "eta;": "\u03b7", + "eth": "\xf0", + "eth;": "\xf0", + "euml": "\xeb", + "euml;": "\xeb", + "euro;": "\u20ac", + "excl;": "!", + "exist;": "\u2203", + "expectation;": "\u2130", + "exponentiale;": "\u2147", + "fallingdotseq;": "\u2252", + "fcy;": "\u0444", + "female;": "\u2640", + "ffilig;": "\ufb03", + "fflig;": "\ufb00", + "ffllig;": "\ufb04", + "ffr;": "\U0001d523", + "filig;": "\ufb01", + "fjlig;": "fj", + "flat;": "\u266d", + "fllig;": "\ufb02", + "fltns;": "\u25b1", + "fnof;": "\u0192", + "fopf;": "\U0001d557", + "forall;": "\u2200", + "fork;": "\u22d4", + "forkv;": "\u2ad9", + "fpartint;": "\u2a0d", + "frac12": "\xbd", + "frac12;": "\xbd", + "frac13;": "\u2153", + "frac14": "\xbc", + "frac14;": "\xbc", + "frac15;": "\u2155", + "frac16;": "\u2159", + "frac18;": "\u215b", + "frac23;": "\u2154", + "frac25;": "\u2156", + "frac34": "\xbe", + "frac34;": "\xbe", + "frac35;": "\u2157", + "frac38;": "\u215c", + "frac45;": "\u2158", + "frac56;": "\u215a", + "frac58;": "\u215d", + "frac78;": "\u215e", + "frasl;": "\u2044", + "frown;": "\u2322", + "fscr;": "\U0001d4bb", + "gE;": "\u2267", + "gEl;": "\u2a8c", + "gacute;": "\u01f5", + "gamma;": "\u03b3", + "gammad;": "\u03dd", + "gap;": "\u2a86", + "gbreve;": "\u011f", + "gcirc;": "\u011d", + "gcy;": "\u0433", + "gdot;": "\u0121", + "ge;": "\u2265", + "gel;": "\u22db", + "geq;": "\u2265", + "geqq;": "\u2267", + "geqslant;": "\u2a7e", + "ges;": "\u2a7e", + "gescc;": "\u2aa9", + "gesdot;": "\u2a80", + "gesdoto;": "\u2a82", + "gesdotol;": "\u2a84", + "gesl;": "\u22db\ufe00", + "gesles;": "\u2a94", + "gfr;": "\U0001d524", + "gg;": "\u226b", + "ggg;": "\u22d9", + "gimel;": "\u2137", + "gjcy;": "\u0453", + "gl;": "\u2277", + "glE;": "\u2a92", + "gla;": "\u2aa5", + "glj;": "\u2aa4", + "gnE;": "\u2269", + "gnap;": "\u2a8a", + "gnapprox;": "\u2a8a", + "gne;": "\u2a88", + "gneq;": "\u2a88", + "gneqq;": "\u2269", + "gnsim;": "\u22e7", + "gopf;": "\U0001d558", + "grave;": "`", + "gscr;": "\u210a", + "gsim;": "\u2273", + "gsime;": "\u2a8e", + "gsiml;": "\u2a90", + "gt": ">", + "gt;": ">", + "gtcc;": "\u2aa7", + "gtcir;": "\u2a7a", + "gtdot;": "\u22d7", + "gtlPar;": "\u2995", + "gtquest;": "\u2a7c", + "gtrapprox;": "\u2a86", + "gtrarr;": "\u2978", + "gtrdot;": "\u22d7", + "gtreqless;": "\u22db", + "gtreqqless;": "\u2a8c", + "gtrless;": "\u2277", + "gtrsim;": "\u2273", + "gvertneqq;": "\u2269\ufe00", + "gvnE;": "\u2269\ufe00", + "hArr;": "\u21d4", + "hairsp;": "\u200a", + "half;": "\xbd", + "hamilt;": "\u210b", + "hardcy;": "\u044a", + "harr;": "\u2194", + "harrcir;": "\u2948", + "harrw;": "\u21ad", + "hbar;": "\u210f", + "hcirc;": "\u0125", + "hearts;": "\u2665", + "heartsuit;": "\u2665", + "hellip;": "\u2026", + "hercon;": "\u22b9", + "hfr;": "\U0001d525", + "hksearow;": "\u2925", + "hkswarow;": "\u2926", + "hoarr;": "\u21ff", + "homtht;": "\u223b", + "hookleftarrow;": "\u21a9", + "hookrightarrow;": "\u21aa", + "hopf;": "\U0001d559", + "horbar;": "\u2015", + "hscr;": "\U0001d4bd", + "hslash;": "\u210f", + "hstrok;": "\u0127", + "hybull;": "\u2043", + "hyphen;": "\u2010", + "iacute": "\xed", + "iacute;": "\xed", + "ic;": "\u2063", + "icirc": "\xee", + "icirc;": "\xee", + "icy;": "\u0438", + "iecy;": "\u0435", + "iexcl": "\xa1", + "iexcl;": "\xa1", + "iff;": "\u21d4", + "ifr;": "\U0001d526", + "igrave": "\xec", + "igrave;": "\xec", + "ii;": "\u2148", + "iiiint;": "\u2a0c", + "iiint;": "\u222d", + "iinfin;": "\u29dc", + "iiota;": "\u2129", + "ijlig;": "\u0133", + "imacr;": "\u012b", + "image;": "\u2111", + "imagline;": "\u2110", + "imagpart;": "\u2111", + "imath;": "\u0131", + "imof;": "\u22b7", + "imped;": "\u01b5", + "in;": "\u2208", + "incare;": "\u2105", + "infin;": "\u221e", + "infintie;": "\u29dd", + "inodot;": "\u0131", + "int;": "\u222b", + "intcal;": "\u22ba", + "integers;": "\u2124", + "intercal;": "\u22ba", + "intlarhk;": "\u2a17", + "intprod;": "\u2a3c", + "iocy;": "\u0451", + "iogon;": "\u012f", + "iopf;": "\U0001d55a", + "iota;": "\u03b9", + "iprod;": "\u2a3c", + "iquest": "\xbf", + "iquest;": "\xbf", + "iscr;": "\U0001d4be", + "isin;": "\u2208", + "isinE;": "\u22f9", + "isindot;": "\u22f5", + "isins;": "\u22f4", + "isinsv;": "\u22f3", + "isinv;": "\u2208", + "it;": "\u2062", + "itilde;": "\u0129", + "iukcy;": "\u0456", + "iuml": "\xef", + "iuml;": "\xef", + "jcirc;": "\u0135", + "jcy;": "\u0439", + "jfr;": "\U0001d527", + "jmath;": "\u0237", + "jopf;": "\U0001d55b", + "jscr;": "\U0001d4bf", + "jsercy;": "\u0458", + "jukcy;": "\u0454", + "kappa;": "\u03ba", + "kappav;": "\u03f0", + "kcedil;": "\u0137", + "kcy;": "\u043a", + "kfr;": "\U0001d528", + "kgreen;": "\u0138", + "khcy;": "\u0445", + "kjcy;": "\u045c", + "kopf;": "\U0001d55c", + "kscr;": "\U0001d4c0", + "lAarr;": "\u21da", + "lArr;": "\u21d0", + "lAtail;": "\u291b", + "lBarr;": "\u290e", + "lE;": "\u2266", + "lEg;": "\u2a8b", + "lHar;": "\u2962", + "lacute;": "\u013a", + "laemptyv;": "\u29b4", + "lagran;": "\u2112", + "lambda;": "\u03bb", + "lang;": "\u27e8", + "langd;": "\u2991", + "langle;": "\u27e8", + "lap;": "\u2a85", + "laquo": "\xab", + "laquo;": "\xab", + "larr;": "\u2190", + "larrb;": "\u21e4", + "larrbfs;": "\u291f", + "larrfs;": "\u291d", + "larrhk;": "\u21a9", + "larrlp;": "\u21ab", + "larrpl;": "\u2939", + "larrsim;": "\u2973", + "larrtl;": "\u21a2", + "lat;": "\u2aab", + "latail;": "\u2919", + "late;": "\u2aad", + "lates;": "\u2aad\ufe00", + "lbarr;": "\u290c", + "lbbrk;": "\u2772", + "lbrace;": "{", + "lbrack;": "[", + "lbrke;": "\u298b", + "lbrksld;": "\u298f", + "lbrkslu;": "\u298d", + "lcaron;": "\u013e", + "lcedil;": "\u013c", + "lceil;": "\u2308", + "lcub;": "{", + "lcy;": "\u043b", + "ldca;": "\u2936", + "ldquo;": "\u201c", + "ldquor;": "\u201e", + "ldrdhar;": "\u2967", + "ldrushar;": "\u294b", + "ldsh;": "\u21b2", + "le;": "\u2264", + "leftarrow;": "\u2190", + "leftarrowtail;": "\u21a2", + "leftharpoondown;": "\u21bd", + "leftharpoonup;": "\u21bc", + "leftleftarrows;": "\u21c7", + "leftrightarrow;": "\u2194", + "leftrightarrows;": "\u21c6", + "leftrightharpoons;": "\u21cb", + "leftrightsquigarrow;": "\u21ad", + "leftthreetimes;": "\u22cb", + "leg;": "\u22da", + "leq;": "\u2264", + "leqq;": "\u2266", + "leqslant;": "\u2a7d", + "les;": "\u2a7d", + "lescc;": "\u2aa8", + "lesdot;": "\u2a7f", + "lesdoto;": "\u2a81", + "lesdotor;": "\u2a83", + "lesg;": "\u22da\ufe00", + "lesges;": "\u2a93", + "lessapprox;": "\u2a85", + "lessdot;": "\u22d6", + "lesseqgtr;": "\u22da", + "lesseqqgtr;": "\u2a8b", + "lessgtr;": "\u2276", + "lesssim;": "\u2272", + "lfisht;": "\u297c", + "lfloor;": "\u230a", + "lfr;": "\U0001d529", + "lg;": "\u2276", + "lgE;": "\u2a91", + "lhard;": "\u21bd", + "lharu;": "\u21bc", + "lharul;": "\u296a", + "lhblk;": "\u2584", + "ljcy;": "\u0459", + "ll;": "\u226a", + "llarr;": "\u21c7", + "llcorner;": "\u231e", + "llhard;": "\u296b", + "lltri;": "\u25fa", + "lmidot;": "\u0140", + "lmoust;": "\u23b0", + "lmoustache;": "\u23b0", + "lnE;": "\u2268", + "lnap;": "\u2a89", + "lnapprox;": "\u2a89", + "lne;": "\u2a87", + "lneq;": "\u2a87", + "lneqq;": "\u2268", + "lnsim;": "\u22e6", + "loang;": "\u27ec", + "loarr;": "\u21fd", + "lobrk;": "\u27e6", + "longleftarrow;": "\u27f5", + "longleftrightarrow;": "\u27f7", + "longmapsto;": "\u27fc", + "longrightarrow;": "\u27f6", + "looparrowleft;": "\u21ab", + "looparrowright;": "\u21ac", + "lopar;": "\u2985", + "lopf;": "\U0001d55d", + "loplus;": "\u2a2d", + "lotimes;": "\u2a34", + "lowast;": "\u2217", + "lowbar;": "_", + "loz;": "\u25ca", + "lozenge;": "\u25ca", + "lozf;": "\u29eb", + "lpar;": "(", + "lparlt;": "\u2993", + "lrarr;": "\u21c6", + "lrcorner;": "\u231f", + "lrhar;": "\u21cb", + "lrhard;": "\u296d", + "lrm;": "\u200e", + "lrtri;": "\u22bf", + "lsaquo;": "\u2039", + "lscr;": "\U0001d4c1", + "lsh;": "\u21b0", + "lsim;": "\u2272", + "lsime;": "\u2a8d", + "lsimg;": "\u2a8f", + "lsqb;": "[", + "lsquo;": "\u2018", + "lsquor;": "\u201a", + "lstrok;": "\u0142", + "lt": "<", + "lt;": "<", + "ltcc;": "\u2aa6", + "ltcir;": "\u2a79", + "ltdot;": "\u22d6", + "lthree;": "\u22cb", + "ltimes;": "\u22c9", + "ltlarr;": "\u2976", + "ltquest;": "\u2a7b", + "ltrPar;": "\u2996", + "ltri;": "\u25c3", + "ltrie;": "\u22b4", + "ltrif;": "\u25c2", + "lurdshar;": "\u294a", + "luruhar;": "\u2966", + "lvertneqq;": "\u2268\ufe00", + "lvnE;": "\u2268\ufe00", + "mDDot;": "\u223a", + "macr": "\xaf", + "macr;": "\xaf", + "male;": "\u2642", + "malt;": "\u2720", + "maltese;": "\u2720", + "map;": "\u21a6", + "mapsto;": "\u21a6", + "mapstodown;": "\u21a7", + "mapstoleft;": "\u21a4", + "mapstoup;": "\u21a5", + "marker;": "\u25ae", + "mcomma;": "\u2a29", + "mcy;": "\u043c", + "mdash;": "\u2014", + "measuredangle;": "\u2221", + "mfr;": "\U0001d52a", + "mho;": "\u2127", + "micro": "\xb5", + "micro;": "\xb5", + "mid;": "\u2223", + "midast;": "*", + "midcir;": "\u2af0", + "middot": "\xb7", + "middot;": "\xb7", + "minus;": "\u2212", + "minusb;": "\u229f", + "minusd;": "\u2238", + "minusdu;": "\u2a2a", + "mlcp;": "\u2adb", + "mldr;": "\u2026", + "mnplus;": "\u2213", + "models;": "\u22a7", + "mopf;": "\U0001d55e", + "mp;": "\u2213", + "mscr;": "\U0001d4c2", + "mstpos;": "\u223e", + "mu;": "\u03bc", + "multimap;": "\u22b8", + "mumap;": "\u22b8", + "nGg;": "\u22d9\u0338", + "nGt;": "\u226b\u20d2", + "nGtv;": "\u226b\u0338", + "nLeftarrow;": "\u21cd", + "nLeftrightarrow;": "\u21ce", + "nLl;": "\u22d8\u0338", + "nLt;": "\u226a\u20d2", + "nLtv;": "\u226a\u0338", + "nRightarrow;": "\u21cf", + "nVDash;": "\u22af", + "nVdash;": "\u22ae", + "nabla;": "\u2207", + "nacute;": "\u0144", + "nang;": "\u2220\u20d2", + "nap;": "\u2249", + "napE;": "\u2a70\u0338", + "napid;": "\u224b\u0338", + "napos;": "\u0149", + "napprox;": "\u2249", + "natur;": "\u266e", + "natural;": "\u266e", + "naturals;": "\u2115", + "nbsp": "\xa0", + "nbsp;": "\xa0", + "nbump;": "\u224e\u0338", + "nbumpe;": "\u224f\u0338", + "ncap;": "\u2a43", + "ncaron;": "\u0148", + "ncedil;": "\u0146", + "ncong;": "\u2247", + "ncongdot;": "\u2a6d\u0338", + "ncup;": "\u2a42", + "ncy;": "\u043d", + "ndash;": "\u2013", + "ne;": "\u2260", + "neArr;": "\u21d7", + "nearhk;": "\u2924", + "nearr;": "\u2197", + "nearrow;": "\u2197", + "nedot;": "\u2250\u0338", + "nequiv;": "\u2262", + "nesear;": "\u2928", + "nesim;": "\u2242\u0338", + "nexist;": "\u2204", + "nexists;": "\u2204", + "nfr;": "\U0001d52b", + "ngE;": "\u2267\u0338", + "nge;": "\u2271", + "ngeq;": "\u2271", + "ngeqq;": "\u2267\u0338", + "ngeqslant;": "\u2a7e\u0338", + "nges;": "\u2a7e\u0338", + "ngsim;": "\u2275", + "ngt;": "\u226f", + "ngtr;": "\u226f", + "nhArr;": "\u21ce", + "nharr;": "\u21ae", + "nhpar;": "\u2af2", + "ni;": "\u220b", + "nis;": "\u22fc", + "nisd;": "\u22fa", + "niv;": "\u220b", + "njcy;": "\u045a", + "nlArr;": "\u21cd", + "nlE;": "\u2266\u0338", + "nlarr;": "\u219a", + "nldr;": "\u2025", + "nle;": "\u2270", + "nleftarrow;": "\u219a", + "nleftrightarrow;": "\u21ae", + "nleq;": "\u2270", + "nleqq;": "\u2266\u0338", + "nleqslant;": "\u2a7d\u0338", + "nles;": "\u2a7d\u0338", + "nless;": "\u226e", + "nlsim;": "\u2274", + "nlt;": "\u226e", + "nltri;": "\u22ea", + "nltrie;": "\u22ec", + "nmid;": "\u2224", + "nopf;": "\U0001d55f", + "not": "\xac", + "not;": "\xac", + "notin;": "\u2209", + "notinE;": "\u22f9\u0338", + "notindot;": "\u22f5\u0338", + "notinva;": "\u2209", + "notinvb;": "\u22f7", + "notinvc;": "\u22f6", + "notni;": "\u220c", + "notniva;": "\u220c", + "notnivb;": "\u22fe", + "notnivc;": "\u22fd", + "npar;": "\u2226", + "nparallel;": "\u2226", + "nparsl;": "\u2afd\u20e5", + "npart;": "\u2202\u0338", + "npolint;": "\u2a14", + "npr;": "\u2280", + "nprcue;": "\u22e0", + "npre;": "\u2aaf\u0338", + "nprec;": "\u2280", + "npreceq;": "\u2aaf\u0338", + "nrArr;": "\u21cf", + "nrarr;": "\u219b", + "nrarrc;": "\u2933\u0338", + "nrarrw;": "\u219d\u0338", + "nrightarrow;": "\u219b", + "nrtri;": "\u22eb", + "nrtrie;": "\u22ed", + "nsc;": "\u2281", + "nsccue;": "\u22e1", + "nsce;": "\u2ab0\u0338", + "nscr;": "\U0001d4c3", + "nshortmid;": "\u2224", + "nshortparallel;": "\u2226", + "nsim;": "\u2241", + "nsime;": "\u2244", + "nsimeq;": "\u2244", + "nsmid;": "\u2224", + "nspar;": "\u2226", + "nsqsube;": "\u22e2", + "nsqsupe;": "\u22e3", + "nsub;": "\u2284", + "nsubE;": "\u2ac5\u0338", + "nsube;": "\u2288", + "nsubset;": "\u2282\u20d2", + "nsubseteq;": "\u2288", + "nsubseteqq;": "\u2ac5\u0338", + "nsucc;": "\u2281", + "nsucceq;": "\u2ab0\u0338", + "nsup;": "\u2285", + "nsupE;": "\u2ac6\u0338", + "nsupe;": "\u2289", + "nsupset;": "\u2283\u20d2", + "nsupseteq;": "\u2289", + "nsupseteqq;": "\u2ac6\u0338", + "ntgl;": "\u2279", + "ntilde": "\xf1", + "ntilde;": "\xf1", + "ntlg;": "\u2278", + "ntriangleleft;": "\u22ea", + "ntrianglelefteq;": "\u22ec", + "ntriangleright;": "\u22eb", + "ntrianglerighteq;": "\u22ed", + "nu;": "\u03bd", + "num;": "#", + "numero;": "\u2116", + "numsp;": "\u2007", + "nvDash;": "\u22ad", + "nvHarr;": "\u2904", + "nvap;": "\u224d\u20d2", + "nvdash;": "\u22ac", + "nvge;": "\u2265\u20d2", + "nvgt;": ">\u20d2", + "nvinfin;": "\u29de", + "nvlArr;": "\u2902", + "nvle;": "\u2264\u20d2", + "nvlt;": "<\u20d2", + "nvltrie;": "\u22b4\u20d2", + "nvrArr;": "\u2903", + "nvrtrie;": "\u22b5\u20d2", + "nvsim;": "\u223c\u20d2", + "nwArr;": "\u21d6", + "nwarhk;": "\u2923", + "nwarr;": "\u2196", + "nwarrow;": "\u2196", + "nwnear;": "\u2927", + "oS;": "\u24c8", + "oacute": "\xf3", + "oacute;": "\xf3", + "oast;": "\u229b", + "ocir;": "\u229a", + "ocirc": "\xf4", + "ocirc;": "\xf4", + "ocy;": "\u043e", + "odash;": "\u229d", + "odblac;": "\u0151", + "odiv;": "\u2a38", + "odot;": "\u2299", + "odsold;": "\u29bc", + "oelig;": "\u0153", + "ofcir;": "\u29bf", + "ofr;": "\U0001d52c", + "ogon;": "\u02db", + "ograve": "\xf2", + "ograve;": "\xf2", + "ogt;": "\u29c1", + "ohbar;": "\u29b5", + "ohm;": "\u03a9", + "oint;": "\u222e", + "olarr;": "\u21ba", + "olcir;": "\u29be", + "olcross;": "\u29bb", + "oline;": "\u203e", + "olt;": "\u29c0", + "omacr;": "\u014d", + "omega;": "\u03c9", + "omicron;": "\u03bf", + "omid;": "\u29b6", + "ominus;": "\u2296", + "oopf;": "\U0001d560", + "opar;": "\u29b7", + "operp;": "\u29b9", + "oplus;": "\u2295", + "or;": "\u2228", + "orarr;": "\u21bb", + "ord;": "\u2a5d", + "order;": "\u2134", + "orderof;": "\u2134", + "ordf": "\xaa", + "ordf;": "\xaa", + "ordm": "\xba", + "ordm;": "\xba", + "origof;": "\u22b6", + "oror;": "\u2a56", + "orslope;": "\u2a57", + "orv;": "\u2a5b", + "oscr;": "\u2134", + "oslash": "\xf8", + "oslash;": "\xf8", + "osol;": "\u2298", + "otilde": "\xf5", + "otilde;": "\xf5", + "otimes;": "\u2297", + "otimesas;": "\u2a36", + "ouml": "\xf6", + "ouml;": "\xf6", + "ovbar;": "\u233d", + "par;": "\u2225", + "para": "\xb6", + "para;": "\xb6", + "parallel;": "\u2225", + "parsim;": "\u2af3", + "parsl;": "\u2afd", + "part;": "\u2202", + "pcy;": "\u043f", + "percnt;": "%", + "period;": ".", + "permil;": "\u2030", + "perp;": "\u22a5", + "pertenk;": "\u2031", + "pfr;": "\U0001d52d", + "phi;": "\u03c6", + "phiv;": "\u03d5", + "phmmat;": "\u2133", + "phone;": "\u260e", + "pi;": "\u03c0", + "pitchfork;": "\u22d4", + "piv;": "\u03d6", + "planck;": "\u210f", + "planckh;": "\u210e", + "plankv;": "\u210f", + "plus;": "+", + "plusacir;": "\u2a23", + "plusb;": "\u229e", + "pluscir;": "\u2a22", + "plusdo;": "\u2214", + "plusdu;": "\u2a25", + "pluse;": "\u2a72", + "plusmn": "\xb1", + "plusmn;": "\xb1", + "plussim;": "\u2a26", + "plustwo;": "\u2a27", + "pm;": "\xb1", + "pointint;": "\u2a15", + "popf;": "\U0001d561", + "pound": "\xa3", + "pound;": "\xa3", + "pr;": "\u227a", + "prE;": "\u2ab3", + "prap;": "\u2ab7", + "prcue;": "\u227c", + "pre;": "\u2aaf", + "prec;": "\u227a", + "precapprox;": "\u2ab7", + "preccurlyeq;": "\u227c", + "preceq;": "\u2aaf", + "precnapprox;": "\u2ab9", + "precneqq;": "\u2ab5", + "precnsim;": "\u22e8", + "precsim;": "\u227e", + "prime;": "\u2032", + "primes;": "\u2119", + "prnE;": "\u2ab5", + "prnap;": "\u2ab9", + "prnsim;": "\u22e8", + "prod;": "\u220f", + "profalar;": "\u232e", + "profline;": "\u2312", + "profsurf;": "\u2313", + "prop;": "\u221d", + "propto;": "\u221d", + "prsim;": "\u227e", + "prurel;": "\u22b0", + "pscr;": "\U0001d4c5", + "psi;": "\u03c8", + "puncsp;": "\u2008", + "qfr;": "\U0001d52e", + "qint;": "\u2a0c", + "qopf;": "\U0001d562", + "qprime;": "\u2057", + "qscr;": "\U0001d4c6", + "quaternions;": "\u210d", + "quatint;": "\u2a16", + "quest;": "?", + "questeq;": "\u225f", + "quot": "\"", + "quot;": "\"", + "rAarr;": "\u21db", + "rArr;": "\u21d2", + "rAtail;": "\u291c", + "rBarr;": "\u290f", + "rHar;": "\u2964", + "race;": "\u223d\u0331", + "racute;": "\u0155", + "radic;": "\u221a", + "raemptyv;": "\u29b3", + "rang;": "\u27e9", + "rangd;": "\u2992", + "range;": "\u29a5", + "rangle;": "\u27e9", + "raquo": "\xbb", + "raquo;": "\xbb", + "rarr;": "\u2192", + "rarrap;": "\u2975", + "rarrb;": "\u21e5", + "rarrbfs;": "\u2920", + "rarrc;": "\u2933", + "rarrfs;": "\u291e", + "rarrhk;": "\u21aa", + "rarrlp;": "\u21ac", + "rarrpl;": "\u2945", + "rarrsim;": "\u2974", + "rarrtl;": "\u21a3", + "rarrw;": "\u219d", + "ratail;": "\u291a", + "ratio;": "\u2236", + "rationals;": "\u211a", + "rbarr;": "\u290d", + "rbbrk;": "\u2773", + "rbrace;": "}", + "rbrack;": "]", + "rbrke;": "\u298c", + "rbrksld;": "\u298e", + "rbrkslu;": "\u2990", + "rcaron;": "\u0159", + "rcedil;": "\u0157", + "rceil;": "\u2309", + "rcub;": "}", + "rcy;": "\u0440", + "rdca;": "\u2937", + "rdldhar;": "\u2969", + "rdquo;": "\u201d", + "rdquor;": "\u201d", + "rdsh;": "\u21b3", + "real;": "\u211c", + "realine;": "\u211b", + "realpart;": "\u211c", + "reals;": "\u211d", + "rect;": "\u25ad", + "reg": "\xae", + "reg;": "\xae", + "rfisht;": "\u297d", + "rfloor;": "\u230b", + "rfr;": "\U0001d52f", + "rhard;": "\u21c1", + "rharu;": "\u21c0", + "rharul;": "\u296c", + "rho;": "\u03c1", + "rhov;": "\u03f1", + "rightarrow;": "\u2192", + "rightarrowtail;": "\u21a3", + "rightharpoondown;": "\u21c1", + "rightharpoonup;": "\u21c0", + "rightleftarrows;": "\u21c4", + "rightleftharpoons;": "\u21cc", + "rightrightarrows;": "\u21c9", + "rightsquigarrow;": "\u219d", + "rightthreetimes;": "\u22cc", + "ring;": "\u02da", + "risingdotseq;": "\u2253", + "rlarr;": "\u21c4", + "rlhar;": "\u21cc", + "rlm;": "\u200f", + "rmoust;": "\u23b1", + "rmoustache;": "\u23b1", + "rnmid;": "\u2aee", + "roang;": "\u27ed", + "roarr;": "\u21fe", + "robrk;": "\u27e7", + "ropar;": "\u2986", + "ropf;": "\U0001d563", + "roplus;": "\u2a2e", + "rotimes;": "\u2a35", + "rpar;": ")", + "rpargt;": "\u2994", + "rppolint;": "\u2a12", + "rrarr;": "\u21c9", + "rsaquo;": "\u203a", + "rscr;": "\U0001d4c7", + "rsh;": "\u21b1", + "rsqb;": "]", + "rsquo;": "\u2019", + "rsquor;": "\u2019", + "rthree;": "\u22cc", + "rtimes;": "\u22ca", + "rtri;": "\u25b9", + "rtrie;": "\u22b5", + "rtrif;": "\u25b8", + "rtriltri;": "\u29ce", + "ruluhar;": "\u2968", + "rx;": "\u211e", + "sacute;": "\u015b", + "sbquo;": "\u201a", + "sc;": "\u227b", + "scE;": "\u2ab4", + "scap;": "\u2ab8", + "scaron;": "\u0161", + "sccue;": "\u227d", + "sce;": "\u2ab0", + "scedil;": "\u015f", + "scirc;": "\u015d", + "scnE;": "\u2ab6", + "scnap;": "\u2aba", + "scnsim;": "\u22e9", + "scpolint;": "\u2a13", + "scsim;": "\u227f", + "scy;": "\u0441", + "sdot;": "\u22c5", + "sdotb;": "\u22a1", + "sdote;": "\u2a66", + "seArr;": "\u21d8", + "searhk;": "\u2925", + "searr;": "\u2198", + "searrow;": "\u2198", + "sect": "\xa7", + "sect;": "\xa7", + "semi;": ";", + "seswar;": "\u2929", + "setminus;": "\u2216", + "setmn;": "\u2216", + "sext;": "\u2736", + "sfr;": "\U0001d530", + "sfrown;": "\u2322", + "sharp;": "\u266f", + "shchcy;": "\u0449", + "shcy;": "\u0448", + "shortmid;": "\u2223", + "shortparallel;": "\u2225", + "shy": "\xad", + "shy;": "\xad", + "sigma;": "\u03c3", + "sigmaf;": "\u03c2", + "sigmav;": "\u03c2", + "sim;": "\u223c", + "simdot;": "\u2a6a", + "sime;": "\u2243", + "simeq;": "\u2243", + "simg;": "\u2a9e", + "simgE;": "\u2aa0", + "siml;": "\u2a9d", + "simlE;": "\u2a9f", + "simne;": "\u2246", + "simplus;": "\u2a24", + "simrarr;": "\u2972", + "slarr;": "\u2190", + "smallsetminus;": "\u2216", + "smashp;": "\u2a33", + "smeparsl;": "\u29e4", + "smid;": "\u2223", + "smile;": "\u2323", + "smt;": "\u2aaa", + "smte;": "\u2aac", + "smtes;": "\u2aac\ufe00", + "softcy;": "\u044c", + "sol;": "/", + "solb;": "\u29c4", + "solbar;": "\u233f", + "sopf;": "\U0001d564", + "spades;": "\u2660", + "spadesuit;": "\u2660", + "spar;": "\u2225", + "sqcap;": "\u2293", + "sqcaps;": "\u2293\ufe00", + "sqcup;": "\u2294", + "sqcups;": "\u2294\ufe00", + "sqsub;": "\u228f", + "sqsube;": "\u2291", + "sqsubset;": "\u228f", + "sqsubseteq;": "\u2291", + "sqsup;": "\u2290", + "sqsupe;": "\u2292", + "sqsupset;": "\u2290", + "sqsupseteq;": "\u2292", + "squ;": "\u25a1", + "square;": "\u25a1", + "squarf;": "\u25aa", + "squf;": "\u25aa", + "srarr;": "\u2192", + "sscr;": "\U0001d4c8", + "ssetmn;": "\u2216", + "ssmile;": "\u2323", + "sstarf;": "\u22c6", + "star;": "\u2606", + "starf;": "\u2605", + "straightepsilon;": "\u03f5", + "straightphi;": "\u03d5", + "strns;": "\xaf", + "sub;": "\u2282", + "subE;": "\u2ac5", + "subdot;": "\u2abd", + "sube;": "\u2286", + "subedot;": "\u2ac3", + "submult;": "\u2ac1", + "subnE;": "\u2acb", + "subne;": "\u228a", + "subplus;": "\u2abf", + "subrarr;": "\u2979", + "subset;": "\u2282", + "subseteq;": "\u2286", + "subseteqq;": "\u2ac5", + "subsetneq;": "\u228a", + "subsetneqq;": "\u2acb", + "subsim;": "\u2ac7", + "subsub;": "\u2ad5", + "subsup;": "\u2ad3", + "succ;": "\u227b", + "succapprox;": "\u2ab8", + "succcurlyeq;": "\u227d", + "succeq;": "\u2ab0", + "succnapprox;": "\u2aba", + "succneqq;": "\u2ab6", + "succnsim;": "\u22e9", + "succsim;": "\u227f", + "sum;": "\u2211", + "sung;": "\u266a", + "sup1": "\xb9", + "sup1;": "\xb9", + "sup2": "\xb2", + "sup2;": "\xb2", + "sup3": "\xb3", + "sup3;": "\xb3", + "sup;": "\u2283", + "supE;": "\u2ac6", + "supdot;": "\u2abe", + "supdsub;": "\u2ad8", + "supe;": "\u2287", + "supedot;": "\u2ac4", + "suphsol;": "\u27c9", + "suphsub;": "\u2ad7", + "suplarr;": "\u297b", + "supmult;": "\u2ac2", + "supnE;": "\u2acc", + "supne;": "\u228b", + "supplus;": "\u2ac0", + "supset;": "\u2283", + "supseteq;": "\u2287", + "supseteqq;": "\u2ac6", + "supsetneq;": "\u228b", + "supsetneqq;": "\u2acc", + "supsim;": "\u2ac8", + "supsub;": "\u2ad4", + "supsup;": "\u2ad6", + "swArr;": "\u21d9", + "swarhk;": "\u2926", + "swarr;": "\u2199", + "swarrow;": "\u2199", + "swnwar;": "\u292a", + "szlig": "\xdf", + "szlig;": "\xdf", + "target;": "\u2316", + "tau;": "\u03c4", + "tbrk;": "\u23b4", + "tcaron;": "\u0165", + "tcedil;": "\u0163", + "tcy;": "\u0442", + "tdot;": "\u20db", + "telrec;": "\u2315", + "tfr;": "\U0001d531", + "there4;": "\u2234", + "therefore;": "\u2234", + "theta;": "\u03b8", + "thetasym;": "\u03d1", + "thetav;": "\u03d1", + "thickapprox;": "\u2248", + "thicksim;": "\u223c", + "thinsp;": "\u2009", + "thkap;": "\u2248", + "thksim;": "\u223c", + "thorn": "\xfe", + "thorn;": "\xfe", + "tilde;": "\u02dc", + "times": "\xd7", + "times;": "\xd7", + "timesb;": "\u22a0", + "timesbar;": "\u2a31", + "timesd;": "\u2a30", + "tint;": "\u222d", + "toea;": "\u2928", + "top;": "\u22a4", + "topbot;": "\u2336", + "topcir;": "\u2af1", + "topf;": "\U0001d565", + "topfork;": "\u2ada", + "tosa;": "\u2929", + "tprime;": "\u2034", + "trade;": "\u2122", + "triangle;": "\u25b5", + "triangledown;": "\u25bf", + "triangleleft;": "\u25c3", + "trianglelefteq;": "\u22b4", + "triangleq;": "\u225c", + "triangleright;": "\u25b9", + "trianglerighteq;": "\u22b5", + "tridot;": "\u25ec", + "trie;": "\u225c", + "triminus;": "\u2a3a", + "triplus;": "\u2a39", + "trisb;": "\u29cd", + "tritime;": "\u2a3b", + "trpezium;": "\u23e2", + "tscr;": "\U0001d4c9", + "tscy;": "\u0446", + "tshcy;": "\u045b", + "tstrok;": "\u0167", + "twixt;": "\u226c", + "twoheadleftarrow;": "\u219e", + "twoheadrightarrow;": "\u21a0", + "uArr;": "\u21d1", + "uHar;": "\u2963", + "uacute": "\xfa", + "uacute;": "\xfa", + "uarr;": "\u2191", + "ubrcy;": "\u045e", + "ubreve;": "\u016d", + "ucirc": "\xfb", + "ucirc;": "\xfb", + "ucy;": "\u0443", + "udarr;": "\u21c5", + "udblac;": "\u0171", + "udhar;": "\u296e", + "ufisht;": "\u297e", + "ufr;": "\U0001d532", + "ugrave": "\xf9", + "ugrave;": "\xf9", + "uharl;": "\u21bf", + "uharr;": "\u21be", + "uhblk;": "\u2580", + "ulcorn;": "\u231c", + "ulcorner;": "\u231c", + "ulcrop;": "\u230f", + "ultri;": "\u25f8", + "umacr;": "\u016b", + "uml": "\xa8", + "uml;": "\xa8", + "uogon;": "\u0173", + "uopf;": "\U0001d566", + "uparrow;": "\u2191", + "updownarrow;": "\u2195", + "upharpoonleft;": "\u21bf", + "upharpoonright;": "\u21be", + "uplus;": "\u228e", + "upsi;": "\u03c5", + "upsih;": "\u03d2", + "upsilon;": "\u03c5", + "upuparrows;": "\u21c8", + "urcorn;": "\u231d", + "urcorner;": "\u231d", + "urcrop;": "\u230e", + "uring;": "\u016f", + "urtri;": "\u25f9", + "uscr;": "\U0001d4ca", + "utdot;": "\u22f0", + "utilde;": "\u0169", + "utri;": "\u25b5", + "utrif;": "\u25b4", + "uuarr;": "\u21c8", + "uuml": "\xfc", + "uuml;": "\xfc", + "uwangle;": "\u29a7", + "vArr;": "\u21d5", + "vBar;": "\u2ae8", + "vBarv;": "\u2ae9", + "vDash;": "\u22a8", + "vangrt;": "\u299c", + "varepsilon;": "\u03f5", + "varkappa;": "\u03f0", + "varnothing;": "\u2205", + "varphi;": "\u03d5", + "varpi;": "\u03d6", + "varpropto;": "\u221d", + "varr;": "\u2195", + "varrho;": "\u03f1", + "varsigma;": "\u03c2", + "varsubsetneq;": "\u228a\ufe00", + "varsubsetneqq;": "\u2acb\ufe00", + "varsupsetneq;": "\u228b\ufe00", + "varsupsetneqq;": "\u2acc\ufe00", + "vartheta;": "\u03d1", + "vartriangleleft;": "\u22b2", + "vartriangleright;": "\u22b3", + "vcy;": "\u0432", + "vdash;": "\u22a2", + "vee;": "\u2228", + "veebar;": "\u22bb", + "veeeq;": "\u225a", + "vellip;": "\u22ee", + "verbar;": "|", + "vert;": "|", + "vfr;": "\U0001d533", + "vltri;": "\u22b2", + "vnsub;": "\u2282\u20d2", + "vnsup;": "\u2283\u20d2", + "vopf;": "\U0001d567", + "vprop;": "\u221d", + "vrtri;": "\u22b3", + "vscr;": "\U0001d4cb", + "vsubnE;": "\u2acb\ufe00", + "vsubne;": "\u228a\ufe00", + "vsupnE;": "\u2acc\ufe00", + "vsupne;": "\u228b\ufe00", + "vzigzag;": "\u299a", + "wcirc;": "\u0175", + "wedbar;": "\u2a5f", + "wedge;": "\u2227", + "wedgeq;": "\u2259", + "weierp;": "\u2118", + "wfr;": "\U0001d534", + "wopf;": "\U0001d568", + "wp;": "\u2118", + "wr;": "\u2240", + "wreath;": "\u2240", + "wscr;": "\U0001d4cc", + "xcap;": "\u22c2", + "xcirc;": "\u25ef", + "xcup;": "\u22c3", + "xdtri;": "\u25bd", + "xfr;": "\U0001d535", + "xhArr;": "\u27fa", + "xharr;": "\u27f7", + "xi;": "\u03be", + "xlArr;": "\u27f8", + "xlarr;": "\u27f5", + "xmap;": "\u27fc", + "xnis;": "\u22fb", + "xodot;": "\u2a00", + "xopf;": "\U0001d569", + "xoplus;": "\u2a01", + "xotime;": "\u2a02", + "xrArr;": "\u27f9", + "xrarr;": "\u27f6", + "xscr;": "\U0001d4cd", + "xsqcup;": "\u2a06", + "xuplus;": "\u2a04", + "xutri;": "\u25b3", + "xvee;": "\u22c1", + "xwedge;": "\u22c0", + "yacute": "\xfd", + "yacute;": "\xfd", + "yacy;": "\u044f", + "ycirc;": "\u0177", + "ycy;": "\u044b", + "yen": "\xa5", + "yen;": "\xa5", + "yfr;": "\U0001d536", + "yicy;": "\u0457", + "yopf;": "\U0001d56a", + "yscr;": "\U0001d4ce", + "yucy;": "\u044e", + "yuml": "\xff", + "yuml;": "\xff", + "zacute;": "\u017a", + "zcaron;": "\u017e", + "zcy;": "\u0437", + "zdot;": "\u017c", + "zeetrf;": "\u2128", + "zeta;": "\u03b6", + "zfr;": "\U0001d537", + "zhcy;": "\u0436", + "zigrarr;": "\u21dd", + "zopf;": "\U0001d56b", + "zscr;": "\U0001d4cf", + "zwj;": "\u200d", + "zwnj;": "\u200c", } replacementCharacters = { - 0x0:u"\uFFFD", - 0x0d:u"\u000D", - 0x80:u"\u20AC", - 0x81:u"\u0081", - 0x81:u"\u0081", - 0x82:u"\u201A", - 0x83:u"\u0192", - 0x84:u"\u201E", - 0x85:u"\u2026", - 0x86:u"\u2020", - 0x87:u"\u2021", - 0x88:u"\u02C6", - 0x89:u"\u2030", - 0x8A:u"\u0160", - 0x8B:u"\u2039", - 0x8C:u"\u0152", - 0x8D:u"\u008D", - 0x8E:u"\u017D", - 0x8F:u"\u008F", - 0x90:u"\u0090", - 0x91:u"\u2018", - 0x92:u"\u2019", - 0x93:u"\u201C", - 0x94:u"\u201D", - 0x95:u"\u2022", - 0x96:u"\u2013", - 0x97:u"\u2014", - 0x98:u"\u02DC", - 0x99:u"\u2122", - 0x9A:u"\u0161", - 0x9B:u"\u203A", - 0x9C:u"\u0153", - 0x9D:u"\u009D", - 0x9E:u"\u017E", - 0x9F:u"\u0178", + 0x0: "\uFFFD", + 0x0d: "\u000D", + 0x80: "\u20AC", + 0x81: "\u0081", + 0x81: "\u0081", + 0x82: "\u201A", + 0x83: "\u0192", + 0x84: "\u201E", + 0x85: "\u2026", + 0x86: "\u2020", + 0x87: "\u2021", + 0x88: "\u02C6", + 0x89: "\u2030", + 0x8A: "\u0160", + 0x8B: "\u2039", + 0x8C: "\u0152", + 0x8D: "\u008D", + 0x8E: "\u017D", + 0x8F: "\u008F", + 0x90: "\u0090", + 0x91: "\u2018", + 0x92: "\u2019", + 0x93: "\u201C", + 0x94: "\u201D", + 0x95: "\u2022", + 0x96: "\u2013", + 0x97: "\u2014", + 0x98: "\u02DC", + 0x99: "\u2122", + 0x9A: "\u0161", + 0x9B: "\u203A", + 0x9C: "\u0153", + 0x9D: "\u009D", + 0x9E: "\u017E", + 0x9F: "\u0178", } encodings = { @@ -3061,25 +3078,27 @@ encodings = { 'x-x-big5': 'big5'} tokenTypes = { - "Doctype":0, - "Characters":1, - "SpaceCharacters":2, - "StartTag":3, - "EndTag":4, - "EmptyTag":5, - "Comment":6, - "ParseError":7 + "Doctype": 0, + "Characters": 1, + "SpaceCharacters": 2, + "StartTag": 3, + "EndTag": 4, + "EmptyTag": 5, + "Comment": 6, + "ParseError": 7 } -tagTokenTypes = frozenset((tokenTypes["StartTag"], tokenTypes["EndTag"], +tagTokenTypes = frozenset((tokenTypes["StartTag"], tokenTypes["EndTag"], tokenTypes["EmptyTag"])) -prefixes = dict([(v,k) for k,v in namespaces.iteritems()]) +prefixes = dict([(v, k) for k, v in namespaces.items()]) prefixes["http://www.w3.org/1998/Math/MathML"] = "math" + class DataLossWarning(UserWarning): pass + class ReparseException(Exception): pass diff --git a/libs/html5lib/filters/_base.py b/libs/html5lib/filters/_base.py index bca94ada..c7dbaed0 100644 --- a/libs/html5lib/filters/_base.py +++ b/libs/html5lib/filters/_base.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + class Filter(object): def __init__(self, source): diff --git a/libs/html5lib/filters/alphabeticalattributes.py b/libs/html5lib/filters/alphabeticalattributes.py new file mode 100644 index 00000000..fed6996c --- /dev/null +++ b/libs/html5lib/filters/alphabeticalattributes.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import, division, unicode_literals + +from . import _base + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +class Filter(_base.Filter): + def __iter__(self): + for token in _base.Filter.__iter__(self): + if token["type"] in ("StartTag", "EmptyTag"): + attrs = OrderedDict() + for name, value in sorted(token["data"].items(), + key=lambda x: x[0]): + attrs[name] = value + token["data"] = attrs + yield token diff --git a/libs/html5lib/filters/formfiller.py b/libs/html5lib/filters/formfiller.py deleted file mode 100644 index 94001714..00000000 --- a/libs/html5lib/filters/formfiller.py +++ /dev/null @@ -1,127 +0,0 @@ -# -# The goal is to finally have a form filler where you pass data for -# each form, using the algorithm for "Seeding a form with initial values" -# See http://www.whatwg.org/specs/web-forms/current-work/#seeding -# - -import _base - -from html5lib.constants import spaceCharacters -spaceCharacters = u"".join(spaceCharacters) - -class SimpleFilter(_base.Filter): - def __init__(self, source, fieldStorage): - _base.Filter.__init__(self, source) - self.fieldStorage = fieldStorage - - def __iter__(self): - field_indices = {} - state = None - field_name = None - for token in _base.Filter.__iter__(self): - type = token["type"] - if type in ("StartTag", "EmptyTag"): - name = token["name"].lower() - if name == "input": - field_name = None - field_type = None - input_value_index = -1 - input_checked_index = -1 - for i,(n,v) in enumerate(token["data"]): - n = n.lower() - if n == u"name": - field_name = v.strip(spaceCharacters) - elif n == u"type": - field_type = v.strip(spaceCharacters) - elif n == u"checked": - input_checked_index = i - elif n == u"value": - input_value_index = i - - value_list = self.fieldStorage.getlist(field_name) - field_index = field_indices.setdefault(field_name, 0) - if field_index < len(value_list): - value = value_list[field_index] - else: - value = "" - - if field_type in (u"checkbox", u"radio"): - if value_list: - if token["data"][input_value_index][1] == value: - if input_checked_index < 0: - token["data"].append((u"checked", u"")) - field_indices[field_name] = field_index + 1 - elif input_checked_index >= 0: - del token["data"][input_checked_index] - - elif field_type not in (u"button", u"submit", u"reset"): - if input_value_index >= 0: - token["data"][input_value_index] = (u"value", value) - else: - token["data"].append((u"value", value)) - field_indices[field_name] = field_index + 1 - - field_type = None - field_name = None - - elif name == "textarea": - field_type = "textarea" - field_name = dict((token["data"])[::-1])["name"] - - elif name == "select": - field_type = "select" - attributes = dict(token["data"][::-1]) - field_name = attributes.get("name") - is_select_multiple = "multiple" in attributes - is_selected_option_found = False - - elif field_type == "select" and field_name and name == "option": - option_selected_index = -1 - option_value = None - for i,(n,v) in enumerate(token["data"]): - n = n.lower() - if n == "selected": - option_selected_index = i - elif n == "value": - option_value = v.strip(spaceCharacters) - if option_value is None: - raise NotImplementedError("s without a value= attribute") - else: - value_list = self.fieldStorage.getlist(field_name) - if value_list: - field_index = field_indices.setdefault(field_name, 0) - if field_index < len(value_list): - value = value_list[field_index] - else: - value = "" - if (is_select_multiple or not is_selected_option_found) and option_value == value: - if option_selected_index < 0: - token["data"].append((u"selected", u"")) - field_indices[field_name] = field_index + 1 - is_selected_option_found = True - elif option_selected_index >= 0: - del token["data"][option_selected_index] - - elif field_type is not None and field_name and type == "EndTag": - name = token["name"].lower() - if name == field_type: - if name == "textarea": - value_list = self.fieldStorage.getlist(field_name) - if value_list: - field_index = field_indices.setdefault(field_name, 0) - if field_index < len(value_list): - value = value_list[field_index] - else: - value = "" - yield {"type": "Characters", "data": value} - field_indices[field_name] = field_index + 1 - - field_name = None - - elif name == "option" and field_type == "select": - pass # TODO: part of "option without value= attribute" processing - - elif field_type == "textarea": - continue # ignore token - - yield token diff --git a/libs/html5lib/filters/inject_meta_charset.py b/libs/html5lib/filters/inject_meta_charset.py index 8e04d8ac..ca33b70b 100644 --- a/libs/html5lib/filters/inject_meta_charset.py +++ b/libs/html5lib/filters/inject_meta_charset.py @@ -1,4 +1,7 @@ -import _base +from __future__ import absolute_import, division, unicode_literals + +from . import _base + class Filter(_base.Filter): def __init__(self, source, encoding): @@ -13,44 +16,44 @@ class Filter(_base.Filter): for token in _base.Filter.__iter__(self): type = token["type"] if type == "StartTag": - if token["name"].lower() == u"head": + if token["name"].lower() == "head": state = "in_head" elif type == "EmptyTag": - if token["name"].lower() == u"meta": - # replace charset with actual encoding - has_http_equiv_content_type = False - for (namespace,name),value in token["data"].iteritems(): - if namespace != None: - continue - elif name.lower() == u'charset': - token["data"][(namespace,name)] = self.encoding - meta_found = True - break - elif name == u'http-equiv' and value.lower() == u'content-type': - has_http_equiv_content_type = True - else: - if has_http_equiv_content_type and (None, u"content") in token["data"]: - token["data"][(None, u"content")] = u'text/html; charset=%s' % self.encoding - meta_found = True + if token["name"].lower() == "meta": + # replace charset with actual encoding + has_http_equiv_content_type = False + for (namespace, name), value in token["data"].items(): + if namespace is not None: + continue + elif name.lower() == 'charset': + token["data"][(namespace, name)] = self.encoding + meta_found = True + break + elif name == 'http-equiv' and value.lower() == 'content-type': + has_http_equiv_content_type = True + else: + if has_http_equiv_content_type and (None, "content") in token["data"]: + token["data"][(None, "content")] = 'text/html; charset=%s' % self.encoding + meta_found = True - elif token["name"].lower() == u"head" and not meta_found: + elif token["name"].lower() == "head" and not meta_found: # insert meta into empty head - yield {"type": "StartTag", "name": u"head", + yield {"type": "StartTag", "name": "head", "data": token["data"]} - yield {"type": "EmptyTag", "name": u"meta", - "data": {(None, u"charset"): self.encoding}} - yield {"type": "EndTag", "name": u"head"} + yield {"type": "EmptyTag", "name": "meta", + "data": {(None, "charset"): self.encoding}} + yield {"type": "EndTag", "name": "head"} meta_found = True continue elif type == "EndTag": - if token["name"].lower() == u"head" and pending: + if token["name"].lower() == "head" and pending: # insert meta into head (if necessary) and flush pending queue yield pending.pop(0) if not meta_found: - yield {"type": "EmptyTag", "name": u"meta", - "data": {(None, u"charset"): self.encoding}} + yield {"type": "EmptyTag", "name": "meta", + "data": {(None, "charset"): self.encoding}} while pending: yield pending.pop(0) meta_found = True diff --git a/libs/html5lib/filters/lint.py b/libs/html5lib/filters/lint.py index ea5c619f..7cc99a4b 100644 --- a/libs/html5lib/filters/lint.py +++ b/libs/html5lib/filters/lint.py @@ -1,13 +1,18 @@ +from __future__ import absolute_import, division, unicode_literals + from gettext import gettext _ = gettext -import _base -from html5lib.constants import cdataElements, rcdataElements, voidElements +from . import _base +from ..constants import cdataElements, rcdataElements, voidElements -from html5lib.constants import spaceCharacters -spaceCharacters = u"".join(spaceCharacters) +from ..constants import spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +class LintError(Exception): + pass -class LintError(Exception): pass class Filter(_base.Filter): def __iter__(self): @@ -18,24 +23,24 @@ class Filter(_base.Filter): if type in ("StartTag", "EmptyTag"): name = token["name"] if contentModelFlag != "PCDATA": - raise LintError(_("StartTag not in PCDATA content model flag: %s") % name) - if not isinstance(name, unicode): - raise LintError(_(u"Tag name is not a string: %r") % name) + raise LintError(_("StartTag not in PCDATA content model flag: %(tag)s") % {"tag": name}) + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) if not name: - raise LintError(_(u"Empty tag name")) + raise LintError(_("Empty tag name")) if type == "StartTag" and name in voidElements: - raise LintError(_(u"Void element reported as StartTag token: %s") % name) + raise LintError(_("Void element reported as StartTag token: %(tag)s") % {"tag": name}) elif type == "EmptyTag" and name not in voidElements: - raise LintError(_(u"Non-void element reported as EmptyTag token: %s") % token["name"]) + raise LintError(_("Non-void element reported as EmptyTag token: %(tag)s") % {"tag": token["name"]}) if type == "StartTag": open_elements.append(name) for name, value in token["data"]: - if not isinstance(name, unicode): - raise LintError(_("Attribute name is not a string: %r") % name) + if not isinstance(name, str): + raise LintError(_("Attribute name is not a string: %(name)r") % {"name": name}) if not name: - raise LintError(_(u"Empty attribute name")) - if not isinstance(value, unicode): - raise LintError(_("Attribute value is not a string: %r") % value) + raise LintError(_("Empty attribute name")) + if not isinstance(value, str): + raise LintError(_("Attribute value is not a string: %(value)r") % {"value": value}) if name in cdataElements: contentModelFlag = "CDATA" elif name in rcdataElements: @@ -45,15 +50,15 @@ class Filter(_base.Filter): elif type == "EndTag": name = token["name"] - if not isinstance(name, unicode): - raise LintError(_(u"Tag name is not a string: %r") % name) + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) if not name: - raise LintError(_(u"Empty tag name")) + raise LintError(_("Empty tag name")) if name in voidElements: - raise LintError(_(u"Void element reported as EndTag token: %s") % name) + raise LintError(_("Void element reported as EndTag token: %(tag)s") % {"tag": name}) start_name = open_elements.pop() if start_name != name: - raise LintError(_(u"EndTag (%s) does not match StartTag (%s)") % (name, start_name)) + raise LintError(_("EndTag (%(end)s) does not match StartTag (%(start)s)") % {"end": name, "start": start_name}) contentModelFlag = "PCDATA" elif type == "Comment": @@ -62,27 +67,27 @@ class Filter(_base.Filter): elif type in ("Characters", "SpaceCharacters"): data = token["data"] - if not isinstance(data, unicode): - raise LintError(_("Attribute name is not a string: %r") % data) + if not isinstance(data, str): + raise LintError(_("Attribute name is not a string: %(name)r") % {"name": data}) if not data: - raise LintError(_(u"%s token with empty data") % type) + raise LintError(_("%(type)s token with empty data") % {"type": type}) if type == "SpaceCharacters": data = data.strip(spaceCharacters) if data: - raise LintError(_(u"Non-space character(s) found in SpaceCharacters token: ") % data) + raise LintError(_("Non-space character(s) found in SpaceCharacters token: %(token)r") % {"token": data}) elif type == "Doctype": name = token["name"] if contentModelFlag != "PCDATA": - raise LintError(_("Doctype not in PCDATA content model flag: %s") % name) - if not isinstance(name, unicode): - raise LintError(_(u"Tag name is not a string: %r") % name) + raise LintError(_("Doctype not in PCDATA content model flag: %(name)s") % {"name": name}) + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) # XXX: what to do with token["data"] ? elif type in ("ParseError", "SerializeError"): pass else: - raise LintError(_(u"Unknown token type: %s") % type) + raise LintError(_("Unknown token type: %(type)s") % {"type": type}) yield token diff --git a/libs/html5lib/filters/optionaltags.py b/libs/html5lib/filters/optionaltags.py index a77aa72c..fefe0b30 100644 --- a/libs/html5lib/filters/optionaltags.py +++ b/libs/html5lib/filters/optionaltags.py @@ -1,4 +1,7 @@ -import _base +from __future__ import absolute_import, division, unicode_literals + +from . import _base + class Filter(_base.Filter): def slider(self): @@ -14,8 +17,8 @@ class Filter(_base.Filter): for previous, token, next in self.slider(): type = token["type"] if type == "StartTag": - if (token["data"] or - not self.is_optional_start(token["name"], previous, next)): + if (token["data"] or + not self.is_optional_start(token["name"], previous, next)): yield token elif type == "EndTag": if not self.is_optional_end(token["name"], next): @@ -73,7 +76,7 @@ class Filter(_base.Filter): # omit the thead and tfoot elements' end tag when they are # immediately followed by a tbody element. See is_optional_end. if previous and previous['type'] == 'EndTag' and \ - previous['name'] in ('tbody','thead','tfoot'): + previous['name'] in ('tbody', 'thead', 'tfoot'): return False return next["name"] == 'tr' else: @@ -121,10 +124,10 @@ class Filter(_base.Filter): # there is no more content in the parent element. if type in ("StartTag", "EmptyTag"): return next["name"] in ('address', 'article', 'aside', - 'blockquote', 'datagrid', 'dialog', + 'blockquote', 'datagrid', 'dialog', 'dir', 'div', 'dl', 'fieldset', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'header', 'hr', 'menu', 'nav', 'ol', + 'header', 'hr', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul') else: return type == "EndTag" or type is None diff --git a/libs/html5lib/filters/sanitizer.py b/libs/html5lib/filters/sanitizer.py index 00235278..b206b54e 100644 --- a/libs/html5lib/filters/sanitizer.py +++ b/libs/html5lib/filters/sanitizer.py @@ -1,8 +1,12 @@ -import _base -from html5lib.sanitizer import HTMLSanitizerMixin +from __future__ import absolute_import, division, unicode_literals + +from . import _base +from ..sanitizer import HTMLSanitizerMixin + class Filter(_base.Filter, HTMLSanitizerMixin): def __iter__(self): for token in _base.Filter.__iter__(self): token = self.sanitize_token(token) - if token: yield token + if token: + yield token diff --git a/libs/html5lib/filters/whitespace.py b/libs/html5lib/filters/whitespace.py index 74d6f4d8..dfc60eeb 100644 --- a/libs/html5lib/filters/whitespace.py +++ b/libs/html5lib/filters/whitespace.py @@ -1,16 +1,13 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals import re -import _base -from html5lib.constants import rcdataElements, spaceCharacters -spaceCharacters = u"".join(spaceCharacters) +from . import _base +from ..constants import rcdataElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + +SPACES_REGEX = re.compile("[%s]+" % spaceCharacters) -SPACES_REGEX = re.compile(u"[%s]+" % spaceCharacters) class Filter(_base.Filter): @@ -21,7 +18,7 @@ class Filter(_base.Filter): for token in _base.Filter.__iter__(self): type = token["type"] if type == "StartTag" \ - and (preserve or token["name"] in self.spacePreserveElements): + and (preserve or token["name"] in self.spacePreserveElements): preserve += 1 elif type == "EndTag" and preserve: @@ -29,13 +26,13 @@ class Filter(_base.Filter): elif not preserve and type == "SpaceCharacters" and token["data"]: # Test on token["data"] above to not introduce spaces where there were not - token["data"] = u" " + token["data"] = " " elif not preserve and type == "Characters": token["data"] = collapse_spaces(token["data"]) yield token + def collapse_spaces(text): return SPACES_REGEX.sub(' ', text) - diff --git a/libs/html5lib/html5parser.py b/libs/html5lib/html5parser.py index 08a8f8ad..b0f14f39 100644 --- a/libs/html5lib/html5parser.py +++ b/libs/html5lib/html5parser.py @@ -1,89 +1,65 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import with_metaclass -try: - any -except: - # Implement 'any' for python 2.4 and previous - def any(iterable): - for element in iterable: - if element: - return True - return False - -try: - "abc".startswith(("a", "b")) - def startswithany(str, prefixes): - return str.startswith(prefixes) -except: - # Python 2.4 doesn't accept a tuple as argument to string startswith - def startswithany(str, prefixes): - for prefix in prefixes: - if str.startswith(prefix): - return True - return False - -import sys import types -import inputstream -import tokenizer +from . import inputstream +from . import tokenizer -import treebuilders -from treebuilders._base import Marker -from treebuilders import simpletree +from . import treebuilders +from .treebuilders._base import Marker -import utils -import constants -from constants import spaceCharacters, asciiUpper2Lower -from constants import formattingElements, specialElements -from constants import headingElements, tableInsertModeElements -from constants import cdataElements, rcdataElements, voidElements -from constants import tokenTypes, ReparseException, namespaces, spaceCharacters -from constants import htmlIntegrationPointElements, mathmlTextIntegrationPointElements +from . import utils +from . import constants +from .constants import spaceCharacters, asciiUpper2Lower +from .constants import specialElements +from .constants import headingElements +from .constants import cdataElements, rcdataElements +from .constants import tokenTypes, ReparseException, namespaces +from .constants import htmlIntegrationPointElements, mathmlTextIntegrationPointElements +from .constants import adjustForeignAttributes as adjustForeignAttributesMap -def parse(doc, treebuilder="simpletree", encoding=None, + +def parse(doc, treebuilder="etree", encoding=None, namespaceHTMLElements=True): """Parse a string or file-like object into a tree""" tb = treebuilders.getTreeBuilder(treebuilder) p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) return p.parse(doc, encoding=encoding) -def parseFragment(doc, container="div", treebuilder="simpletree", encoding=None, + +def parseFragment(doc, container="div", treebuilder="etree", encoding=None, namespaceHTMLElements=True): tb = treebuilders.getTreeBuilder(treebuilder) p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) return p.parseFragment(doc, container=container, encoding=encoding) + def method_decorator_metaclass(function): class Decorated(type): def __new__(meta, classname, bases, classDict): - for attributeName, attribute in classDict.iteritems(): - if type(attribute) == types.FunctionType: + for attributeName, attribute in classDict.items(): + if isinstance(attribute, types.FunctionType): attribute = function(attribute) classDict[attributeName] = attribute - return type.__new__(meta, classname, bases, classDict) + return type.__new__(meta, classname, bases, classDict) return Decorated + class HTMLParser(object): """HTML parser. Generates a tree structure from a stream of (possibly malformed) HTML""" - def __init__(self, tree = simpletree.TreeBuilder, - tokenizer = tokenizer.HTMLTokenizer, strict = False, - namespaceHTMLElements = True, debug=False): + def __init__(self, tree=None, tokenizer=tokenizer.HTMLTokenizer, + strict=False, namespaceHTMLElements=True, debug=False): """ strict - raise an exception when a parse error is encountered tree - a treebuilder class controlling the type of tree that will be returned. Built in treebuilders can be accessed through html5lib.treebuilders.getTreeBuilder(treeType) - + tokenizer - a class that provides a stream of tokens to the treebuilder. This may be replaced for e.g. a sanitizer which converts some tags to text @@ -92,12 +68,14 @@ class HTMLParser(object): # Raise an exception on the first error encountered self.strict = strict + if tree is None: + tree = treebuilders.getTreeBuilder("etree") self.tree = tree(namespaceHTMLElements) self.tokenizer_class = tokenizer self.errors = [] self.phases = dict([(name, cls(self, self.tree)) for name, cls in - getPhases(debug).iteritems()]) + getPhases(debug).items()]) def _parse(self, stream, innerHTML=False, container="div", encoding=None, parseMeta=True, useChardet=True, **kwargs): @@ -106,7 +84,7 @@ class HTMLParser(object): self.container = container self.tokenizer = self.tokenizer_class(stream, encoding=encoding, parseMeta=parseMeta, - useChardet=useChardet, + useChardet=useChardet, parser=self, **kwargs) self.reset() @@ -114,14 +92,14 @@ class HTMLParser(object): try: self.mainLoop() break - except ReparseException, e: + except ReparseException: self.reset() def reset(self): self.tree.reset() self.firstStartTag = False self.errors = [] - self.log = [] #only used with debug mode + self.log = [] # only used with debug mode # "quirks" / "limited quirks" / "no quirks" self.compatMode = "no quirks" @@ -152,18 +130,18 @@ class HTMLParser(object): self.framesetOK = True def isHTMLIntegrationPoint(self, element): - if (element.name == "annotation-xml" and - element.namespace == namespaces["mathml"]): + if (element.name == "annotation-xml" and + element.namespace == namespaces["mathml"]): return ("encoding" in element.attributes and element.attributes["encoding"].translate( - asciiUpper2Lower) in + asciiUpper2Lower) in ("text/html", "application/xhtml+xml")) else: return (element.namespace, element.name) in htmlIntegrationPointElements def isMathMLTextIntegrationPoint(self, element): return (element.namespace, element.name) in mathmlTextIntegrationPointElements - + def mainLoop(self): CharactersToken = tokenTypes["Characters"] SpaceCharactersToken = tokenTypes["SpaceCharacters"] @@ -172,7 +150,7 @@ class HTMLParser(object): CommentToken = tokenTypes["Comment"] DoctypeToken = tokenTypes["Doctype"] ParseErrorToken = tokenTypes["ParseError"] - + for token in self.normalizedTokens(): new_token = token while new_token is not None: @@ -181,7 +159,7 @@ class HTMLParser(object): currentNodeName = currentNode.name if currentNode else None type = new_token["type"] - + if type == ParseErrorToken: self.parseError(new_token["data"], new_token.get("datavars", {})) new_token = None @@ -191,7 +169,7 @@ class HTMLParser(object): (self.isMathMLTextIntegrationPoint(currentNode) and ((type == StartTagToken and token["name"] not in frozenset(["mglyph", "malignmark"])) or - type in (CharactersToken, SpaceCharactersToken))) or + type in (CharactersToken, SpaceCharactersToken))) or (currentNodeNamespace == namespaces["mathml"] and currentNodeName == "annotation-xml" and token["name"] == "svg") or @@ -204,7 +182,7 @@ class HTMLParser(object): if type == CharactersToken: new_token = phase.processCharacters(new_token) elif type == SpaceCharactersToken: - new_token= phase.processSpaceCharacters(new_token) + new_token = phase.processSpaceCharacters(new_token) elif type == StartTagToken: new_token = phase.processStartTag(new_token) elif type == EndTagToken: @@ -215,10 +193,9 @@ class HTMLParser(object): new_token = phase.processDoctype(new_token) if (type == StartTagToken and token["selfClosing"] - and not token["selfClosingAcknowledged"]): + and not token["selfClosingAcknowledged"]): self.parseError("non-void-element-with-trailing-solidus", - {"name":token["name"]}) - + {"name": token["name"]}) # When the loop finishes it's EOF reprocess = True @@ -243,14 +220,14 @@ class HTMLParser(object): regardless of any BOM or later declaration (such as in a meta element) """ - self._parse(stream, innerHTML=False, encoding=encoding, + self._parse(stream, innerHTML=False, encoding=encoding, parseMeta=parseMeta, useChardet=useChardet) return self.tree.getDocument() - + def parseFragment(self, stream, container="div", encoding=None, parseMeta=False, useChardet=True): """Parse a HTML fragment into a well-formed tree fragment - + container - name of the element we're setting the innerHTML property if set to None, default to 'div' @@ -279,100 +256,87 @@ class HTMLParser(object): return token def adjustMathMLAttributes(self, token): - replacements = {"definitionurl":u"definitionURL"} - for k,v in replacements.iteritems(): + replacements = {"definitionurl": "definitionURL"} + for k, v in replacements.items(): if k in token["data"]: token["data"][v] = token["data"][k] del token["data"][k] def adjustSVGAttributes(self, token): replacements = { - "attributename":u"attributeName", - "attributetype":u"attributeType", - "basefrequency":u"baseFrequency", - "baseprofile":u"baseProfile", - "calcmode":u"calcMode", - "clippathunits":u"clipPathUnits", - "contentscripttype":u"contentScriptType", - "contentstyletype":u"contentStyleType", - "diffuseconstant":u"diffuseConstant", - "edgemode":u"edgeMode", - "externalresourcesrequired":u"externalResourcesRequired", - "filterres":u"filterRes", - "filterunits":u"filterUnits", - "glyphref":u"glyphRef", - "gradienttransform":u"gradientTransform", - "gradientunits":u"gradientUnits", - "kernelmatrix":u"kernelMatrix", - "kernelunitlength":u"kernelUnitLength", - "keypoints":u"keyPoints", - "keysplines":u"keySplines", - "keytimes":u"keyTimes", - "lengthadjust":u"lengthAdjust", - "limitingconeangle":u"limitingConeAngle", - "markerheight":u"markerHeight", - "markerunits":u"markerUnits", - "markerwidth":u"markerWidth", - "maskcontentunits":u"maskContentUnits", - "maskunits":u"maskUnits", - "numoctaves":u"numOctaves", - "pathlength":u"pathLength", - "patterncontentunits":u"patternContentUnits", - "patterntransform":u"patternTransform", - "patternunits":u"patternUnits", - "pointsatx":u"pointsAtX", - "pointsaty":u"pointsAtY", - "pointsatz":u"pointsAtZ", - "preservealpha":u"preserveAlpha", - "preserveaspectratio":u"preserveAspectRatio", - "primitiveunits":u"primitiveUnits", - "refx":u"refX", - "refy":u"refY", - "repeatcount":u"repeatCount", - "repeatdur":u"repeatDur", - "requiredextensions":u"requiredExtensions", - "requiredfeatures":u"requiredFeatures", - "specularconstant":u"specularConstant", - "specularexponent":u"specularExponent", - "spreadmethod":u"spreadMethod", - "startoffset":u"startOffset", - "stddeviation":u"stdDeviation", - "stitchtiles":u"stitchTiles", - "surfacescale":u"surfaceScale", - "systemlanguage":u"systemLanguage", - "tablevalues":u"tableValues", - "targetx":u"targetX", - "targety":u"targetY", - "textlength":u"textLength", - "viewbox":u"viewBox", - "viewtarget":u"viewTarget", - "xchannelselector":u"xChannelSelector", - "ychannelselector":u"yChannelSelector", - "zoomandpan":u"zoomAndPan" - } - for originalName in token["data"].keys(): + "attributename": "attributeName", + "attributetype": "attributeType", + "basefrequency": "baseFrequency", + "baseprofile": "baseProfile", + "calcmode": "calcMode", + "clippathunits": "clipPathUnits", + "contentscripttype": "contentScriptType", + "contentstyletype": "contentStyleType", + "diffuseconstant": "diffuseConstant", + "edgemode": "edgeMode", + "externalresourcesrequired": "externalResourcesRequired", + "filterres": "filterRes", + "filterunits": "filterUnits", + "glyphref": "glyphRef", + "gradienttransform": "gradientTransform", + "gradientunits": "gradientUnits", + "kernelmatrix": "kernelMatrix", + "kernelunitlength": "kernelUnitLength", + "keypoints": "keyPoints", + "keysplines": "keySplines", + "keytimes": "keyTimes", + "lengthadjust": "lengthAdjust", + "limitingconeangle": "limitingConeAngle", + "markerheight": "markerHeight", + "markerunits": "markerUnits", + "markerwidth": "markerWidth", + "maskcontentunits": "maskContentUnits", + "maskunits": "maskUnits", + "numoctaves": "numOctaves", + "pathlength": "pathLength", + "patterncontentunits": "patternContentUnits", + "patterntransform": "patternTransform", + "patternunits": "patternUnits", + "pointsatx": "pointsAtX", + "pointsaty": "pointsAtY", + "pointsatz": "pointsAtZ", + "preservealpha": "preserveAlpha", + "preserveaspectratio": "preserveAspectRatio", + "primitiveunits": "primitiveUnits", + "refx": "refX", + "refy": "refY", + "repeatcount": "repeatCount", + "repeatdur": "repeatDur", + "requiredextensions": "requiredExtensions", + "requiredfeatures": "requiredFeatures", + "specularconstant": "specularConstant", + "specularexponent": "specularExponent", + "spreadmethod": "spreadMethod", + "startoffset": "startOffset", + "stddeviation": "stdDeviation", + "stitchtiles": "stitchTiles", + "surfacescale": "surfaceScale", + "systemlanguage": "systemLanguage", + "tablevalues": "tableValues", + "targetx": "targetX", + "targety": "targetY", + "textlength": "textLength", + "viewbox": "viewBox", + "viewtarget": "viewTarget", + "xchannelselector": "xChannelSelector", + "ychannelselector": "yChannelSelector", + "zoomandpan": "zoomAndPan" + } + for originalName in list(token["data"].keys()): if originalName in replacements: svgName = replacements[originalName] token["data"][svgName] = token["data"][originalName] del token["data"][originalName] def adjustForeignAttributes(self, token): - replacements = { - "xlink:actuate":("xlink", "actuate", namespaces["xlink"]), - "xlink:arcrole":("xlink", "arcrole", namespaces["xlink"]), - "xlink:href":("xlink", "href", namespaces["xlink"]), - "xlink:role":("xlink", "role", namespaces["xlink"]), - "xlink:show":("xlink", "show", namespaces["xlink"]), - "xlink:title":("xlink", "title", namespaces["xlink"]), - "xlink:type":("xlink", "type", namespaces["xlink"]), - "xml:base":("xml", "base", namespaces["xml"]), - "xml:lang":("xml", "lang", namespaces["xml"]), - "xml:space":("xml", "space", namespaces["xml"]), - "xmlns":(None, "xmlns", namespaces["xmlns"]), - "xmlns:xlink":("xmlns", "xlink", namespaces["xmlns"]) - } + replacements = adjustForeignAttributesMap - for originalName in token["data"].iterkeys(): + for originalName in token["data"].keys(): if originalName in replacements: foreignName = replacements[originalName] token["data"][foreignName] = token["data"][originalName] @@ -386,20 +350,20 @@ class HTMLParser(object): # specification.) last = False newModes = { - "select":"inSelect", - "td":"inCell", - "th":"inCell", - "tr":"inRow", - "tbody":"inTableBody", - "thead":"inTableBody", - "tfoot":"inTableBody", - "caption":"inCaption", - "colgroup":"inColumnGroup", - "table":"inTable", - "head":"inBody", - "body":"inBody", - "frameset":"inFrameset", - "html":"beforeHead" + "select": "inSelect", + "td": "inCell", + "th": "inCell", + "tr": "inRow", + "tbody": "inTableBody", + "thead": "inTableBody", + "tfoot": "inTableBody", + "caption": "inCaption", + "colgroup": "inColumnGroup", + "table": "inTable", + "head": "inBody", + "body": "inBody", + "frameset": "inFrameset", + "html": "beforeHead" } for node in self.tree.openElements[::-1]: nodeName = node.name @@ -430,9 +394,9 @@ class HTMLParser(object): contentType - RCDATA or RAWTEXT """ assert contentType in ("RAWTEXT", "RCDATA") - - element = self.tree.insertElement(token) - + + self.tree.insertElement(token) + if contentType == "RAWTEXT": self.tokenizer.state = self.tokenizer.rawtextState else: @@ -442,25 +406,27 @@ class HTMLParser(object): self.phase = self.phases["text"] + def getPhases(debug): def log(function): """Logger that records which phase processes each token""" - type_names = dict((value, key) for key, value in - constants.tokenTypes.iteritems()) + type_names = dict((value, key) for key, value in + constants.tokenTypes.items()) + def wrapped(self, *args, **kwargs): if function.__name__.startswith("process") and len(args) > 0: token = args[0] try: - info = {"type":type_names[token['type']]} + info = {"type": type_names[token['type']]} except: raise if token['type'] in constants.tagTokenTypes: info["name"] = token['name'] self.parser.log.append((self.parser.tokenizer.state.__name__, - self.parser.phase.__class__.__name__, - self.__class__.__name__, - function.__name__, + self.parser.phase.__class__.__name__, + self.__class__.__name__, + function.__name__, info)) return function(self, *args, **kwargs) else: @@ -473,21 +439,9 @@ def getPhases(debug): else: return type - class Phase(object): + class Phase(with_metaclass(getMetaclass(debug, log))): """Base class for helper object that implements each phase of processing """ - # Order should be (they can be omitted): - # * EOF - # * Comment - # * Doctype - # * SpaceCharacters - # * Characters - # * StartTag - # - startTag* methods - # * EndTag - # - endTag* methods - - __metaclass__ = getMetaclass(debug, log) def __init__(self, parser, tree): self.parser = parser @@ -514,11 +468,11 @@ def getPhases(debug): return self.startTagHandler[token["name"]](token) def startTagHtml(self, token): - if self.parser.firstStartTag == False and token["name"] == "html": - self.parser.parseError("non-html-root") + if not self.parser.firstStartTag and token["name"] == "html": + self.parser.parseError("non-html-root") # XXX Need a check here to see if the first start tag token emitted is # this token... If it's not, invoke self.parser.parseError(). - for attr, value in token["data"].iteritems(): + for attr, value in token["data"].items(): if attr not in self.tree.openElements[0].attributes: self.tree.openElements[0].attributes[attr] = value self.parser.firstStartTag = False @@ -539,8 +493,8 @@ def getPhases(debug): systemId = token["systemId"] correct = token["correct"] - if (name != "html" or publicId != None or - systemId != None and systemId != "about:legacy-compat"): + if (name != "html" or publicId is not None or + systemId is not None and systemId != "about:legacy-compat"): self.parser.parseError("unknown-doctype") if publicId is None: @@ -552,79 +506,79 @@ def getPhases(debug): publicId = publicId.translate(asciiUpper2Lower) if (not correct or token["name"] != "html" - or startswithany(publicId, - ("+//silmaril//dtd html pro v0r11 19970101//", - "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", - "-//as//dtd html 3.0 aswedit + extensions//", - "-//ietf//dtd html 2.0 level 1//", - "-//ietf//dtd html 2.0 level 2//", - "-//ietf//dtd html 2.0 strict level 1//", - "-//ietf//dtd html 2.0 strict level 2//", - "-//ietf//dtd html 2.0 strict//", - "-//ietf//dtd html 2.0//", - "-//ietf//dtd html 2.1e//", - "-//ietf//dtd html 3.0//", - "-//ietf//dtd html 3.2 final//", - "-//ietf//dtd html 3.2//", - "-//ietf//dtd html 3//", - "-//ietf//dtd html level 0//", - "-//ietf//dtd html level 1//", - "-//ietf//dtd html level 2//", - "-//ietf//dtd html level 3//", - "-//ietf//dtd html strict level 0//", - "-//ietf//dtd html strict level 1//", - "-//ietf//dtd html strict level 2//", - "-//ietf//dtd html strict level 3//", - "-//ietf//dtd html strict//", - "-//ietf//dtd html//", - "-//metrius//dtd metrius presentational//", - "-//microsoft//dtd internet explorer 2.0 html strict//", - "-//microsoft//dtd internet explorer 2.0 html//", - "-//microsoft//dtd internet explorer 2.0 tables//", - "-//microsoft//dtd internet explorer 3.0 html strict//", - "-//microsoft//dtd internet explorer 3.0 html//", - "-//microsoft//dtd internet explorer 3.0 tables//", - "-//netscape comm. corp.//dtd html//", - "-//netscape comm. corp.//dtd strict html//", - "-//o'reilly and associates//dtd html 2.0//", - "-//o'reilly and associates//dtd html extended 1.0//", - "-//o'reilly and associates//dtd html extended relaxed 1.0//", - "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", - "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", - "-//spyglass//dtd html 2.0 extended//", - "-//sq//dtd html 2.0 hotmetal + extensions//", - "-//sun microsystems corp.//dtd hotjava html//", - "-//sun microsystems corp.//dtd hotjava strict html//", - "-//w3c//dtd html 3 1995-03-24//", - "-//w3c//dtd html 3.2 draft//", - "-//w3c//dtd html 3.2 final//", - "-//w3c//dtd html 3.2//", - "-//w3c//dtd html 3.2s draft//", - "-//w3c//dtd html 4.0 frameset//", - "-//w3c//dtd html 4.0 transitional//", - "-//w3c//dtd html experimental 19960712//", - "-//w3c//dtd html experimental 970421//", - "-//w3c//dtd w3 html//", - "-//w3o//dtd w3 html 3.0//", - "-//webtechs//dtd mozilla html 2.0//", - "-//webtechs//dtd mozilla html//")) + or publicId.startswith( + ("+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", + "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//")) or publicId in ("-//w3o//dtd w3 html strict 3.0//en//", "-/w3c/dtd html 4.0 transitional/en", "html") - or startswithany(publicId, + or publicId.startswith( ("-//w3c//dtd html 4.01 frameset//", - "-//w3c//dtd html 4.01 transitional//")) and - systemId == None - or systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"): + "-//w3c//dtd html 4.01 transitional//")) and + systemId is None + or systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"): self.parser.compatMode = "quirks" - elif (startswithany(publicId, + elif (publicId.startswith( ("-//w3c//dtd xhtml 1.0 frameset//", "-//w3c//dtd xhtml 1.0 transitional//")) - or startswithany(publicId, + or publicId.startswith( ("-//w3c//dtd html 4.01 frameset//", - "-//w3c//dtd html 4.01 transitional//")) and - systemId != None): + "-//w3c//dtd html 4.01 transitional//")) and + systemId is not None): self.parser.compatMode = "limited quirks" self.parser.phase = self.parser.phases["beforeHtml"] @@ -640,13 +594,13 @@ def getPhases(debug): def processStartTag(self, token): self.parser.parseError("expected-doctype-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.anythingElse() return token def processEndTag(self, token): self.parser.parseError("expected-doctype-but-got-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.anythingElse() return token @@ -655,7 +609,6 @@ def getPhases(debug): self.anythingElse() return True - class BeforeHtmlPhase(Phase): # helper methods def insertHtmlElement(self): @@ -686,12 +639,11 @@ def getPhases(debug): def processEndTag(self, token): if token["name"] not in ("head", "body", "html", "br"): self.parser.parseError("unexpected-end-tag-before-html", - {"name": token["name"]}) + {"name": token["name"]}) else: self.insertHtmlElement() return token - class BeforeHeadPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) @@ -736,18 +688,18 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("end-tag-after-implied-root", - {"name": token["name"]}) + {"name": token["name"]}) class InHeadPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) - self.startTagHandler = utils.MethodDispatcher([ + self.startTagHandler = utils.MethodDispatcher([ ("html", self.startTagHtml), ("title", self.startTagTitle), (("noscript", "noframes", "style"), self.startTagNoScriptNoFramesStyle), ("script", self.startTagScript), - (("base", "basefont", "bgsound", "command", "link"), + (("base", "basefont", "bgsound", "command", "link"), self.startTagBaseLinkCommand), ("meta", self.startTagMeta), ("head", self.startTagHead) @@ -761,7 +713,7 @@ def getPhases(debug): self.endTagHandler.default = self.endTagOther # the real thing - def processEOF (self): + def processEOF(self): self.anythingElse() return True @@ -789,7 +741,9 @@ def getPhases(debug): if self.parser.tokenizer.stream.charEncoding[1] == "tentative": if "charset" in attributes: self.parser.tokenizer.stream.changeEncoding(attributes["charset"]) - elif "content" in attributes: + elif ("content" in attributes and + "http-equiv" in attributes and + attributes["http-equiv"].lower() == "content-type"): # Encoding it as UTF-8 here is a hack, as really we should pass # the abstract Unicode string, and just use the # ContentAttrParser on that, but using UTF-8 allows all chars @@ -803,7 +757,7 @@ def getPhases(debug): self.parser.parseRCDataRawtext(token, "RCDATA") def startTagNoScriptNoFramesStyle(self, token): - #Need to decide whether to implement the scripting-disabled case + # Need to decide whether to implement the scripting-disabled case self.parser.parseRCDataRawtext(token, "RAWTEXT") def startTagScript(self, token): @@ -818,7 +772,7 @@ def getPhases(debug): def endTagHead(self, token): node = self.parser.tree.openElements.pop() - assert node.name == "head", "Expected head got %s"%node.name + assert node.name == "head", "Expected head got %s" % node.name self.parser.phase = self.parser.phases["afterHead"] def endTagHtmlBodyBr(self, token): @@ -831,12 +785,10 @@ def getPhases(debug): def anythingElse(self): self.endTagHead(impliedTagToken("head")) - # XXX If we implement a parser for which scripting is disabled we need to # implement this phase. # # class InHeadNoScriptPhase(Phase): - class AfterHeadPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) @@ -845,13 +797,13 @@ def getPhases(debug): ("html", self.startTagHtml), ("body", self.startTagBody), ("frameset", self.startTagFrameset), - (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", + (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title"), - self.startTagFromHead), + self.startTagFromHead), ("head", self.startTagHead) ]) self.startTagHandler.default = self.startTagOther - self.endTagHandler = utils.MethodDispatcher([(("body", "html", "br"), + self.endTagHandler = utils.MethodDispatcher([(("body", "html", "br"), self.endTagHtmlBodyBr)]) self.endTagHandler.default = self.endTagOther @@ -877,7 +829,7 @@ def getPhases(debug): def startTagFromHead(self, token): self.parser.parseError("unexpected-start-tag-out-of-my-head", - {"name": token["name"]}) + {"name": token["name"]}) self.tree.openElements.append(self.tree.headPointer) self.parser.phases["inHead"].processStartTag(token) for node in self.tree.openElements[::-1]: @@ -886,7 +838,7 @@ def getPhases(debug): break def startTagHead(self, token): - self.parser.parseError("unexpected-start-tag", {"name":token["name"]}) + self.parser.parseError("unexpected-start-tag", {"name": token["name"]}) def startTagOther(self, token): self.anythingElse() @@ -897,43 +849,42 @@ def getPhases(debug): return token def endTagOther(self, token): - self.parser.parseError("unexpected-end-tag", {"name":token["name"]}) + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) def anythingElse(self): self.tree.insertElement(impliedTagToken("body", "StartTag")) self.parser.phase = self.parser.phases["inBody"] self.parser.framesetOK = True - class InBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody # the really-really-really-very crazy mode def __init__(self, parser, tree): Phase.__init__(self, parser, tree) - #Keep a ref to this for special handling of whitespace in + # Keep a ref to this for special handling of whitespace in self.processSpaceCharactersNonPre = self.processSpaceCharacters self.startTagHandler = utils.MethodDispatcher([ ("html", self.startTagHtml), - (("base", "basefont", "bgsound", "command", "link", "meta", - "noframes", "script", "style", "title"), + (("base", "basefont", "bgsound", "command", "link", "meta", + "noframes", "script", "style", "title"), self.startTagProcessInHead), ("body", self.startTagBody), ("frameset", self.startTagFrameset), (("address", "article", "aside", "blockquote", "center", "details", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "menu", "nav", "ol", "p", + "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", "section", "summary", "ul"), - self.startTagCloseP), + self.startTagCloseP), (headingElements, self.startTagHeading), (("pre", "listing"), self.startTagPreListing), ("form", self.startTagForm), (("li", "dd", "dt"), self.startTagListItem), - ("plaintext",self.startTagPlaintext), + ("plaintext", self.startTagPlaintext), ("a", self.startTagA), - (("b", "big", "code", "em", "font", "i", "s", "small", "strike", - "strong", "tt", "u"),self.startTagFormatting), + (("b", "big", "code", "em", "font", "i", "s", "small", "strike", + "strong", "tt", "u"), self.startTagFormatting), ("nobr", self.startTagNobr), ("button", self.startTagButton), (("applet", "marquee", "object"), self.startTagAppletMarqueeObject), @@ -961,21 +912,21 @@ def getPhases(debug): self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("body",self.endTagBody), - ("html",self.endTagHtml), - (("address", "article", "aside", "blockquote", "center", - "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", + ("body", self.endTagBody), + ("html", self.endTagHtml), + (("address", "article", "aside", "blockquote", "button", "center", + "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "section", "summary", "ul"), self.endTagBlock), ("form", self.endTagForm), - ("p",self.endTagP), + ("p", self.endTagP), (("dd", "dt", "li"), self.endTagListItem), (headingElements, self.endTagHeading), (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"), self.endTagFormatting), - (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), + (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), ("br", self.endTagBr), - ]) + ]) self.endTagHandler.default = self.endTagOther def isMatchingFormattingElement(self, node1, node2): @@ -995,14 +946,14 @@ def getPhases(debug): def addFormattingElement(self, token): self.tree.insertElement(token) element = self.tree.openElements[-1] - + matchingElements = [] for node in self.tree.activeFormattingElements[::-1]: if node is Marker: break elif self.isMatchingFormattingElement(node, element): matchingElements.append(node) - + assert len(matchingElements) <= 3 if len(matchingElements) == 3: self.tree.activeFormattingElements.remove(matchingElements[-1]) @@ -1017,7 +968,7 @@ def getPhases(debug): if node.name not in allowed_elements: self.parser.parseError("expected-closing-tag-but-got-eof") break - #Stop parsing + # Stop parsing def processSpaceCharactersDropNewline(self, token): # Sometimes (start of , , and blocks) we @@ -1026,19 +977,19 @@ def getPhases(debug): self.processSpaceCharacters = self.processSpaceCharactersNonPre if (data.startswith("\n") and self.tree.openElements[-1].name in ("pre", "listing", "textarea") - and not self.tree.openElements[-1].hasContent()): + and not self.tree.openElements[-1].hasContent()): data = data[1:] if data: self.tree.reconstructActiveFormattingElements() self.tree.insertText(data) def processCharacters(self, token): - if token["data"] == u"\u0000": - #The tokenizer should always emit null on its own + if token["data"] == "\u0000": + # The tokenizer should always emit null on its own return self.tree.reconstructActiveFormattingElements() self.tree.insertText(token["data"]) - #This must be bad for performance + # This must be bad for performance if (self.parser.framesetOK and any([char not in spaceCharacters for char in token["data"]])): @@ -1054,11 +1005,11 @@ def getPhases(debug): def startTagBody(self, token): self.parser.parseError("unexpected-start-tag", {"name": "body"}) if (len(self.tree.openElements) == 1 - or self.tree.openElements[1].name != "body"): + or self.tree.openElements[1].name != "body"): assert self.parser.innerHTML else: self.parser.framesetOK = False - for attr, value in token["data"].iteritems(): + for attr, value in token["data"].items(): if attr not in self.tree.openElements[1].attributes: self.tree.openElements[1].attributes[attr] = value @@ -1090,7 +1041,7 @@ def getPhases(debug): def startTagForm(self, token): if self.tree.formPointer: - self.parser.parseError(u"unexpected-start-tag", {"name": "form"}) + self.parser.parseError("unexpected-start-tag", {"name": "form"}) else: if self.tree.elementInScope("p", variant="button"): self.endTagP(impliedTagToken("p")) @@ -1100,9 +1051,9 @@ def getPhases(debug): def startTagListItem(self, token): self.parser.framesetOK = False - stopNamesMap = {"li":["li"], - "dt":["dt", "dd"], - "dd":["dt", "dd"]} + stopNamesMap = {"li": ["li"], + "dt": ["dt", "dd"], + "dd": ["dt", "dd"]} stopNames = stopNamesMap[token["name"]] for node in reversed(self.tree.openElements): if node.name in stopNames: @@ -1110,7 +1061,7 @@ def getPhases(debug): impliedTagToken(node.name, "EndTag")) break if (node.nameTuple in specialElements and - node.name not in ("address", "div", "p")): + node.name not in ("address", "div", "p")): break if self.tree.elementInScope("p", variant="button"): @@ -1137,7 +1088,7 @@ def getPhases(debug): afeAElement = self.tree.elementInActiveFormattingElements("a") if afeAElement: self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "a", "endName": "a"}) + {"startName": "a", "endName": "a"}) self.endTagFormatting(impliedTagToken("a")) if afeAElement in self.tree.openElements: self.tree.openElements.remove(afeAElement) @@ -1154,7 +1105,7 @@ def getPhases(debug): self.tree.reconstructActiveFormattingElements() if self.tree.elementInScope("nobr"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "nobr", "endName": "nobr"}) + {"startName": "nobr", "endName": "nobr"}) self.processEndTag(impliedTagToken("nobr")) # XXX Need tests that trigger the following self.tree.reconstructActiveFormattingElements() @@ -1163,7 +1114,7 @@ def getPhases(debug): def startTagButton(self, token): if self.tree.elementInScope("button"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "button", "endName": "button"}) + {"startName": "button", "endName": "button"}) self.processEndTag(impliedTagToken("button")) return token else: @@ -1203,8 +1154,8 @@ def getPhases(debug): framesetOK = self.parser.framesetOK self.startTagVoidFormatting(token) if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): - #input type=hidden doesn't change framesetOK + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + # input type=hidden doesn't change framesetOK self.parser.framesetOK = framesetOK def startTagParamSource(self, token): @@ -1223,7 +1174,7 @@ def getPhases(debug): def startTagImage(self, token): # No really... self.parser.parseError("unexpected-start-tag-treated-as", - {"originalName": "image", "newName": "img"}) + {"originalName": "image", "newName": "img"}) self.processStartTag(impliedTagToken("img", "StartTag", attributes=token["data"], selfClosing=token["selfClosing"])) @@ -1243,18 +1194,18 @@ def getPhases(debug): if "prompt" in token["data"]: prompt = token["data"]["prompt"] else: - prompt = u"This is a searchable index. Enter search keywords: " + prompt = "This is a searchable index. Enter search keywords: " self.processCharacters( - {"type":tokenTypes["Characters"], "data":prompt}) + {"type": tokenTypes["Characters"], "data": prompt}) attributes = token["data"].copy() if "action" in attributes: del attributes["action"] if "prompt" in attributes: del attributes["prompt"] attributes["name"] = "isindex" - self.processStartTag(impliedTagToken("input", "StartTag", - attributes = attributes, - selfClosing = + self.processStartTag(impliedTagToken("input", "StartTag", + attributes=attributes, + selfClosing= token["selfClosing"])) self.processEndTag(impliedTagToken("label")) self.processStartTag(impliedTagToken("hr", "StartTag")) @@ -1287,7 +1238,7 @@ def getPhases(debug): if self.parser.phase in (self.parser.phases["inTable"], self.parser.phases["inCaption"], self.parser.phases["inColumnGroup"], - self.parser.phases["inTableBody"], + self.parser.phases["inTableBody"], self.parser.phases["inRow"], self.parser.phases["inCell"]): self.parser.phase = self.parser.phases["inSelectInTable"] @@ -1307,8 +1258,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["mathml"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1319,8 +1270,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["svg"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1362,7 +1313,7 @@ def getPhases(debug): "tbody", "td", "tfoot", "th", "thead", "tr", "body", "html")): - #Not sure this is the correct name for the parse error + # Not sure this is the correct name for the parse error self.parser.parseError( "expected-one-end-tag-but-got-another", {"expectedName": "body", "gotName": node.name}) @@ -1370,20 +1321,20 @@ def getPhases(debug): self.parser.phase = self.parser.phases["afterBody"] def endTagHtml(self, token): - #We repeat the test for the body end tag token being ignored here + # We repeat the test for the body end tag token being ignored here if self.tree.elementInScope("body"): self.endTagBody(impliedTagToken("body")) return token def endTagBlock(self, token): - #Put us back in the right whitespace handling mode + # Put us back in the right whitespace handling mode if token["name"] == "pre": self.processSpaceCharacters = self.processSpaceCharactersNonPre inScope = self.tree.elementInScope(token["name"]) if inScope: self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != token["name"]: - self.parser.parseError("end-tag-too-early", {"name": token["name"]}) + self.parser.parseError("end-tag-too-early", {"name": token["name"]}) if inScope: node = self.tree.openElements.pop() while node.name != token["name"]: @@ -1394,7 +1345,7 @@ def getPhases(debug): self.tree.formPointer = None if node is None or not self.tree.elementInScope(node): self.parser.parseError("unexpected-end-tag", - {"name":"form"}) + {"name": "form"}) else: self.tree.generateImpliedEndTags() if self.tree.openElements[-1] != node: @@ -1410,7 +1361,7 @@ def getPhases(debug): if not self.tree.elementInScope(token["name"], variant=variant): self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) else: - self.tree.generateImpliedEndTags(exclude = token["name"]) + self.tree.generateImpliedEndTags(exclude=token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError( "end-tag-too-early", @@ -1436,65 +1387,105 @@ def getPhases(debug): def endTagFormatting(self, token): """The much-feared adoption agency algorithm""" - # http://www.whatwg.org/specs/web-apps/current-work/#adoptionAgency + # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867 # XXX Better parseError messages appreciated. - name = token["name"] + # Step 1 outerLoopCounter = 0 + + # Step 2 while outerLoopCounter < 8: + + # Step 3 outerLoopCounter += 1 - # Step 1 paragraph 1 + # Step 4: + + # Let the formatting element be the last element in + # the list of active formatting elements that: + # - is between the end of the list and the last scope + # marker in the list, if any, or the start of the list + # otherwise, and + # - has the same tag name as the token. formattingElement = self.tree.elementInActiveFormattingElements( token["name"]) - if (not formattingElement or + if (not formattingElement or (formattingElement in self.tree.openElements and not self.tree.elementInScope(formattingElement.name))): - self.parser.parseError("adoption-agency-1.1", {"name": token["name"]}) + # If there is no such node, then abort these steps + # and instead act as described in the "any other + # end tag" entry below. + self.endTagOther(token) return - # Step 1 paragraph 2 + # Otherwise, if there is such a node, but that node is + # not in the stack of open elements, then this is a + # parse error; remove the element from the list, and + # abort these steps. elif formattingElement not in self.tree.openElements: self.parser.parseError("adoption-agency-1.2", {"name": token["name"]}) self.tree.activeFormattingElements.remove(formattingElement) return - # Step 1 paragraph 3 - if formattingElement != self.tree.openElements[-1]: - self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + # Otherwise, if there is such a node, and that node is + # also in the stack of open elements, but the element + # is not in scope, then this is a parse error; ignore + # the token, and abort these steps. + elif not self.tree.elementInScope(formattingElement.name): + self.parser.parseError("adoption-agency-4.4", {"name": token["name"]}) + return - # Step 2 - # Start of the adoption agency algorithm proper + # Otherwise, there is a formatting element and that + # element is in the stack and is in scope. If the + # element is not the current node, this is a parse + # error. In any case, proceed with the algorithm as + # written in the following steps. + else: + if formattingElement != self.tree.openElements[-1]: + self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + + # Step 5: + + # Let the furthest block be the topmost node in the + # stack of open elements that is lower in the stack + # than the formatting element, and is an element in + # the special category. There might not be one. afeIndex = self.tree.openElements.index(formattingElement) furthestBlock = None for element in self.tree.openElements[afeIndex:]: if element.nameTuple in specialElements: furthestBlock = element break - # Step 3 + + # Step 6: + + # If there is no furthest block, then the UA must + # first pop all the nodes from the bottom of the stack + # of open elements, from the current node up to and + # including the formatting element, then remove the + # formatting element from the list of active + # formatting elements, and finally abort these steps. if furthestBlock is None: element = self.tree.openElements.pop() while element != formattingElement: element = self.tree.openElements.pop() self.tree.activeFormattingElements.remove(element) return - commonAncestor = self.tree.openElements[afeIndex-1] - # Step 5 - #if furthestBlock.parent: - # furthestBlock.parent.removeChild(furthestBlock) + # Step 7 + commonAncestor = self.tree.openElements[afeIndex - 1] - # Step 5 + # Step 8: # The bookmark is supposed to help us identify where to reinsert - # nodes in step 12. We have to ensure that we reinsert nodes after + # nodes in step 15. We have to ensure that we reinsert nodes after # the node before the active formatting element. Note the bookmark - # can move in step 7.4 + # can move in step 9.7 bookmark = self.tree.activeFormattingElements.index(formattingElement) - # Step 6 + # Step 9 lastNode = node = furthestBlock innerLoopCounter = 0 - + index = self.tree.openElements.index(node) while innerLoopCounter < 3: innerLoopCounter += 1 @@ -1504,15 +1495,13 @@ def getPhases(debug): if node not in self.tree.activeFormattingElements: self.tree.openElements.remove(node) continue - # Step 6.3 + # Step 9.6 if node == formattingElement: break - # Step 6.4 + # Step 9.7 if lastNode == furthestBlock: - bookmark = (self.tree.activeFormattingElements.index(node) - + 1) - # Step 6.5 - #cite = node.parent + bookmark = self.tree.activeFormattingElements.index(node) + 1 + # Step 9.8 clone = node.cloneNode() # Replace node with clone self.tree.activeFormattingElements[ @@ -1520,20 +1509,18 @@ def getPhases(debug): self.tree.openElements[ self.tree.openElements.index(node)] = clone node = clone - - # Step 6.6 + # Step 9.9 # Remove lastNode from its parents, if any if lastNode.parent: lastNode.parent.removeChild(lastNode) node.appendChild(lastNode) - # Step 7.7 + # Step 9.10 lastNode = node - # End of inner loop - # Step 7 + # Step 10 # Foster parent lastNode if commonAncestor is a - # table, tbody, tfoot, thead, or tr we need to foster parent the - # lastNode + # table, tbody, tfoot, thead, or tr we need to foster + # parent the lastNode if lastNode.parent: lastNode.parent.removeChild(lastNode) @@ -1543,23 +1530,23 @@ def getPhases(debug): else: commonAncestor.appendChild(lastNode) - # Step 8 + # Step 11 clone = formattingElement.cloneNode() - # Step 9 + # Step 12 furthestBlock.reparentChildren(clone) - # Step 10 + # Step 13 furthestBlock.appendChild(clone) - # Step 11 + # Step 14 self.tree.activeFormattingElements.remove(formattingElement) self.tree.activeFormattingElements.insert(bookmark, clone) - # Step 12 + # Step 15 self.tree.openElements.remove(formattingElement) self.tree.openElements.insert( - self.tree.openElements.index(furthestBlock) + 1, clone) + self.tree.openElements.index(furthestBlock) + 1, clone) def endTagAppletMarqueeObject(self, token): if self.tree.elementInScope(token["name"]): @@ -1575,7 +1562,7 @@ def getPhases(debug): def endTagBr(self, token): self.parser.parseError("unexpected-end-tag-treated-as", - {"originalName": "br", "newName": "br element"}) + {"originalName": "br", "newName": "br element"}) self.tree.reconstructActiveFormattingElements() self.tree.insertElement(impliedTagToken("br", "StartTag")) self.tree.openElements.pop() @@ -1600,31 +1587,31 @@ def getPhases(debug): self.startTagHandler = utils.MethodDispatcher([]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("script", self.endTagScript)]) + ("script", self.endTagScript)]) self.endTagHandler.default = self.endTagOther def processCharacters(self, token): self.tree.insertText(token["data"]) def processEOF(self): - self.parser.parseError("expected-named-closing-tag-but-got-eof", - self.tree.openElements[-1].name) + self.parser.parseError("expected-named-closing-tag-but-got-eof", + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase return True def startTagOther(self, token): - assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode"%token['name'] + assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name'] def endTagScript(self, token): node = self.tree.openElements.pop() assert node.name == "script" self.parser.phase = self.parser.originalPhase - #The rest of this method is all stuff that only happens if - #document.write works + # The rest of this method is all stuff that only happens if + # document.write works def endTagOther(self, token): - node = self.tree.openElements.pop() + self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase class InTablePhase(Phase): @@ -1656,7 +1643,7 @@ def getPhases(debug): def clearStackToTableContext(self): # "clear the stack back to a table context" while self.tree.openElements[-1].name not in ("table", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() # When the current node is it's an innerHTML case @@ -1667,7 +1654,7 @@ def getPhases(debug): self.parser.parseError("eof-in-table") else: assert self.parser.innerHTML - #Stop parsing + # Stop parsing def processSpaceCharacters(self, token): originalPhase = self.parser.phase @@ -1682,7 +1669,7 @@ def getPhases(debug): self.parser.phase.processCharacters(token) def insertText(self, token): - #If we get here there must be at least one non-whitespace character + # If we get here there must be at least one non-whitespace character # Do the table magic! self.tree.insertFromTable = True self.parser.phases["inBody"].processCharacters(token) @@ -1714,7 +1701,7 @@ def getPhases(debug): def startTagTable(self, token): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "table", "endName": "table"}) + {"startName": "table", "endName": "table"}) self.parser.phase.processEndTag(impliedTagToken("table")) if not self.parser.innerHTML: return token @@ -1723,8 +1710,8 @@ def getPhases(debug): return self.parser.phases["inHead"].processStartTag(token) def startTagInput(self, token): - if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + if ("type" in token["data"] and + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): self.parser.parseError("unexpected-hidden-input-in-table") self.tree.insertElement(token) # XXX associate with form @@ -1751,8 +1738,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "table": self.parser.parseError("end-tag-too-early-named", - {"gotName": "table", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "table", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "table": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1781,7 +1768,7 @@ def getPhases(debug): def flushCharacters(self): data = "".join([item["data"] for item in self.characterTokens]) if any([item not in spaceCharacters for item in data]): - token = {"type":tokenTypes["Characters"], "data":data} + token = {"type": tokenTypes["Characters"], "data": data} self.parser.phases["inTable"].insertText(token) elif data: self.tree.insertText(data) @@ -1798,12 +1785,12 @@ def getPhases(debug): return True def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.characterTokens.append(token) def processSpaceCharacters(self, token): - #pretty sure we should never reach here + # pretty sure we should never reach here self.characterTokens.append(token) # assert False @@ -1817,7 +1804,6 @@ def getPhases(debug): self.parser.phase = self.originalPhase return token - class InCaptionPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-caption def __init__(self, parser, tree): @@ -1849,7 +1835,7 @@ def getPhases(debug): def startTagTableElement(self, token): self.parser.parseError() - #XXX Have to duplicate logic here to find out if the tag is ignored + # XXX Have to duplicate logic here to find out if the tag is ignored ignoreEndTag = self.ignoreEndTagCaption() self.parser.phase.processEndTag(impliedTagToken("caption")) if not ignoreEndTag: @@ -1864,8 +1850,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "caption": self.parser.parseError("expected-one-end-tag-but-got-another", - {"gotName": "caption", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "caption", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "caption": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1889,7 +1875,6 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) - class InColumnGroupPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-column @@ -1955,7 +1940,6 @@ def getPhases(debug): if not ignoreEndTag: return token - class InTableBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table0 def __init__(self, parser, tree): @@ -1980,8 +1964,8 @@ def getPhases(debug): # helper methods def clearStackToTableBodyContext(self): while self.tree.openElements[-1].name not in ("tbody", "tfoot", - "thead", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + "thead", "html"): + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() if self.tree.openElements[-1].name == "html": @@ -2003,7 +1987,7 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inRow"] def startTagTableCell(self, token): - self.parser.parseError("unexpected-cell-in-table-body", + self.parser.parseError("unexpected-cell-in-table-body", {"name": token["name"]}) self.startTagTr(impliedTagToken("tr", "StartTag")) return token @@ -2012,7 +1996,7 @@ def getPhases(debug): # XXX AT Any ideas on how to share this with endTagTable? if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2032,12 +2016,12 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inTable"] else: self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagTable(self, token): if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2049,12 +2033,11 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) - class InRowPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-row def __init__(self, parser, tree): @@ -2072,7 +2055,7 @@ def getPhases(debug): ("table", self.endTagTable), (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), (("body", "caption", "col", "colgroup", "html", "td", "th"), - self.endTagIgnore) + self.endTagIgnore) ]) self.endTagHandler.default = self.endTagOther @@ -2080,7 +2063,7 @@ def getPhases(debug): def clearStackToTableRowContext(self): while self.tree.openElements[-1].name not in ("tr", "html"): self.parser.parseError("unexpected-implied-end-tag-in-table-row", - {"name": self.tree.openElements[-1].name}) + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() def ignoreEndTagTr(self): @@ -2091,7 +2074,7 @@ def getPhases(debug): self.parser.phases["inTable"].processEOF() def processSpaceCharacters(self, token): - return self.parser.phases["inTable"].processSpaceCharacters(token) + return self.parser.phases["inTable"].processSpaceCharacters(token) def processCharacters(self, token): return self.parser.phases["inTable"].processCharacters(token) @@ -2139,7 +2122,7 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-row", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) @@ -2178,7 +2161,7 @@ def getPhases(debug): def startTagTableOther(self, token): if (self.tree.elementInScope("td", variant="table") or - self.tree.elementInScope("th", variant="table")): + self.tree.elementInScope("th", variant="table")): self.closeCell() return token else: @@ -2194,7 +2177,7 @@ def getPhases(debug): self.tree.generateImpliedEndTags(token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError("unexpected-cell-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) while True: node = self.tree.openElements.pop() if node.name == token["name"]: @@ -2249,7 +2232,7 @@ def getPhases(debug): assert self.parser.innerHTML def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.tree.insertText(token["data"]) @@ -2283,19 +2266,19 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-select", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOption(self, token): if self.tree.openElements[-1].name == "option": self.tree.openElements.pop() else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "option"}) + {"name": "option"}) def endTagOptgroup(self, token): # implicitly closes if (self.tree.openElements[-1].name == "option" and - self.tree.openElements[-2].name == "optgroup"): + self.tree.openElements[-2].name == "optgroup"): self.tree.openElements.pop() # It also closes if self.tree.openElements[-1].name == "optgroup": @@ -2303,7 +2286,7 @@ def getPhases(debug): # But nothing else else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "optgroup"}) + {"name": "optgroup"}) def endTagSelect(self, token): if self.tree.elementInScope("select", variant="select"): @@ -2318,8 +2301,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-select", - {"name": token["name"]}) - + {"name": token["name"]}) class InSelectInTablePhase(Phase): def __init__(self, parser, tree): @@ -2360,64 +2342,64 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inSelect"].processEndTag(token) - class InForeignContentPhase(Phase): - breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", + breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", - "em", "embed", "h1", "h2", "h3", + "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", - "li", "listing", "menu", "meta", "nobr", - "ol", "p", "pre", "ruby", "s", "small", - "span", "strong", "strike", "sub", "sup", + "li", "listing", "menu", "meta", "nobr", + "ol", "p", "pre", "ruby", "s", "small", + "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var"]) + def __init__(self, parser, tree): Phase.__init__(self, parser, tree) def adjustSVGTagNames(self, token): - replacements = {u"altglyph":u"altGlyph", - u"altglyphdef":u"altGlyphDef", - u"altglyphitem":u"altGlyphItem", - u"animatecolor":u"animateColor", - u"animatemotion":u"animateMotion", - u"animatetransform":u"animateTransform", - u"clippath":u"clipPath", - u"feblend":u"feBlend", - u"fecolormatrix":u"feColorMatrix", - u"fecomponenttransfer":u"feComponentTransfer", - u"fecomposite":u"feComposite", - u"feconvolvematrix":u"feConvolveMatrix", - u"fediffuselighting":u"feDiffuseLighting", - u"fedisplacementmap":u"feDisplacementMap", - u"fedistantlight":u"feDistantLight", - u"feflood":u"feFlood", - u"fefunca":u"feFuncA", - u"fefuncb":u"feFuncB", - u"fefuncg":u"feFuncG", - u"fefuncr":u"feFuncR", - u"fegaussianblur":u"feGaussianBlur", - u"feimage":u"feImage", - u"femerge":u"feMerge", - u"femergenode":u"feMergeNode", - u"femorphology":u"feMorphology", - u"feoffset":u"feOffset", - u"fepointlight":u"fePointLight", - u"fespecularlighting":u"feSpecularLighting", - u"fespotlight":u"feSpotLight", - u"fetile":u"feTile", - u"feturbulence":u"feTurbulence", - u"foreignobject":u"foreignObject", - u"glyphref":u"glyphRef", - u"lineargradient":u"linearGradient", - u"radialgradient":u"radialGradient", - u"textpath":u"textPath"} + replacements = {"altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath"} if token["name"] in replacements: token["name"] = replacements[token["name"]] def processCharacters(self, token): - if token["data"] == u"\u0000": - token["data"] = u"\uFFFD" - elif (self.parser.framesetOK and + if token["data"] == "\u0000": + token["data"] = "\uFFFD" + elif (self.parser.framesetOK and any(char not in spaceCharacters for char in token["data"])): self.parser.framesetOK = False Phase.processCharacters(self, token) @@ -2428,9 +2410,9 @@ def getPhases(debug): (token["name"] == "font" and set(token["data"].keys()) & set(["color", "face", "size"]))): self.parser.parseError("unexpected-html-element-in-foreign-content", - token["name"]) + {"name": token["name"]}) while (self.tree.openElements[-1].namespace != - self.tree.defaultNamespace and + self.tree.defaultNamespace and not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])): self.tree.openElements.pop() @@ -2453,11 +2435,11 @@ def getPhases(debug): nodeIndex = len(self.tree.openElements) - 1 node = self.tree.openElements[-1] if node.name != token["name"]: - self.parser.parseError("unexpected-end-tag", token["name"]) + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) while True: if node.name.translate(asciiUpper2Lower) == token["name"]: - #XXX this isn't in the spec but it seems necessary + # XXX this isn't in the spec but it seems necessary if self.parser.phase == self.parser.phases["inTableText"]: self.parser.phase.flushCharacters() self.parser.phase = self.parser.phase.originalPhase @@ -2475,21 +2457,20 @@ def getPhases(debug): break return new_token - class AfterBodyPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) self.startTagHandler = utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) + ("html", self.startTagHtml) + ]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([("html", self.endTagHtml)]) self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processComment(self, token): @@ -2507,11 +2488,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token - def endTagHtml(self,name): + def endTagHtml(self, name): if self.parser.innerHTML: self.parser.parseError("unexpected-end-tag-after-body-innerhtml") else: @@ -2519,7 +2500,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2562,7 +2543,7 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagFrameset(self, token): if self.tree.openElements[-1].name == "html": @@ -2571,15 +2552,14 @@ def getPhases(debug): else: self.tree.openElements.pop() if (not self.parser.innerHTML and - self.tree.openElements[-1].name != "frameset"): + self.tree.openElements[-1].name != "frameset"): # If we're not in innerHTML mode and the the current node is not a # "frameset" element (anymore) then switch. self.parser.phase = self.parser.phases["afterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterFramesetPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#after3 @@ -2598,7 +2578,7 @@ def getPhases(debug): self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processCharacters(self, token): @@ -2609,15 +2589,14 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagHtml(self, token): self.parser.phase = self.parser.phases["afterAfterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterAfterBodyPhase(Phase): def __init__(self, parser, tree): @@ -2647,13 +2626,13 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2687,12 +2666,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) - + {"name": token["name"]}) return { "initial": InitialPhase, @@ -2719,14 +2697,16 @@ def getPhases(debug): "afterAfterBody": AfterAfterBodyPhase, "afterAfterFrameset": AfterAfterFramesetPhase, # XXX after after frameset - } + } -def impliedTagToken(name, type="EndTag", attributes = None, - selfClosing = False): + +def impliedTagToken(name, type="EndTag", attributes=None, + selfClosing=False): if attributes is None: attributes = {} - return {"type":tokenTypes[type], "name":unicode(name), "data":attributes, - "selfClosing":selfClosing} + return {"type": tokenTypes[type], "name": name, "data": attributes, + "selfClosing": selfClosing} + class ParseError(Exception): """Error in parsed document""" diff --git a/libs/html5lib/ihatexml.py b/libs/html5lib/ihatexml.py index dd785639..0fc79308 100644 --- a/libs/html5lib/ihatexml.py +++ b/libs/html5lib/ihatexml.py @@ -1,25 +1,105 @@ -import re +from __future__ import absolute_import, division, unicode_literals -baseChar = """[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | [#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | [#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | [#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | [#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | [#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | [#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | [#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | [#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | [#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | [#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | [#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | [#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | [#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | [#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | [#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | [#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | [#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | [#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | [#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | [#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | [#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | [#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | [#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | [#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | [#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | [#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | [#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | [#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | #x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | #x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | #x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | [#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | [#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | #x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | [#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | [#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | [#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | [#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | [#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | #x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | [#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | [#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | [#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | [#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" +import re +import warnings + +from .constants import DataLossWarning + +baseChar = """ +[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | +[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | +[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | +[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | +[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | +[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | +[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | +[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | +[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | +[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | +[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | +[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | +[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | +[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | +[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | +[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | +[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | +[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | +[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | +[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | +[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | +[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | +[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | +[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | +[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | +[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | +[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | +[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | +[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | +[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | +#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | +#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | +#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | +[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | +[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | +#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | +[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | +[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | +[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | +[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | +[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | +#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | +[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | +[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | +[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | +[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]""" -combiningCharacter = """[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | [#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | [#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | [#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | #x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | [#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | [#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | #x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | [#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | [#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | #x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | [#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | [#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | [#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | [#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | [#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | #x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | [#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | #x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | [#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | [#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | #x3099 | #x309A""" +combiningCharacter = """ +[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | +[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | +[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | +[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | +#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | +[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | +[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | +#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | +[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | +[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | +#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | +[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | +[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | +[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | +[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | +[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | +#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | +[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | +#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | +[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | +[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | +#x3099 | #x309A""" -digit = """[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | [#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | [#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | [#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" +digit = """ +[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | +[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | +[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | +[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" -extender = """#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | [#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" +extender = """ +#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | +#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" letter = " | ".join([baseChar, ideographic]) -#Without the -name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, - extender]) +# Without the +name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, + extender]) nameFirst = " | ".join([letter, "_"]) reChar = re.compile(r"#x([\d|A-F]{4,4})") reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]") + def charStringToList(chars): charRanges = [item.strip() for item in chars.split(" | ")] rv = [] @@ -30,16 +110,17 @@ def charStringToList(chars): if match is not None: rv.append([hexToInt(item) for item in match.groups()]) if len(rv[-1]) == 1: - rv[-1] = rv[-1]*2 + rv[-1] = rv[-1] * 2 foundMatch = True break if not foundMatch: assert len(item) == 1 - + rv.append([ord(item)] * 2) rv = normaliseCharList(rv) return rv + def normaliseCharList(charList): charList = sorted(charList) for item in charList: @@ -49,61 +130,69 @@ def normaliseCharList(charList): while i < len(charList): j = 1 rv.append(charList[i]) - while i + j < len(charList) and charList[i+j][0] <= rv[-1][1] + 1: - rv[-1][1] = charList[i+j][1] + while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1: + rv[-1][1] = charList[i + j][1] j += 1 i += j return rv -#We don't really support characters above the BMP :( +# We don't really support characters above the BMP :( max_unicode = int("FFFF", 16) - + + def missingRanges(charList): rv = [] if charList[0] != 0: rv.append([0, charList[0][0] - 1]) for i, item in enumerate(charList[:-1]): - rv.append([item[1]+1, charList[i+1][0] - 1]) + rv.append([item[1] + 1, charList[i + 1][0] - 1]) if charList[-1][1] != max_unicode: rv.append([charList[-1][1] + 1, max_unicode]) return rv + def listToRegexpStr(charList): rv = [] for item in charList: if item[0] == item[1]: - rv.append(escapeRegexp(unichr(item[0]))) + rv.append(escapeRegexp(chr(item[0]))) else: - rv.append(escapeRegexp(unichr(item[0])) + "-" + - escapeRegexp(unichr(item[1]))) - return "[%s]"%"".join(rv) + rv.append(escapeRegexp(chr(item[0])) + "-" + + escapeRegexp(chr(item[1]))) + return "[%s]" % "".join(rv) + def hexToInt(hex_str): return int(hex_str, 16) + def escapeRegexp(string): specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}", - "[", "]", "|", "(", ")", "-") + "[", "]", "|", "(", ")", "-") for char in specialCharacters: string = string.replace(char, "\\" + char) - if char in string: - print string return string -#output from the above -nonXmlNameBMPRegexp = re.compile(u'[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') +# output from the above +nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +# Simpler things +nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\-\'()+,./:=?;!*#@$_%]") -nonXmlNameFirstBMPRegexp = re.compile(u'[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') class InfosetFilter(object): replacementRegexp = re.compile(r"U[\dA-F]{5,5}") - def __init__(self, replaceChars = None, - dropXmlnsLocalName = False, - dropXmlnsAttrNs = False, - preventDoubleDashComments = False, - preventDashAtCommentEnd = False, - replaceFormFeedCharacters = True): + + def __init__(self, replaceChars=None, + dropXmlnsLocalName=False, + dropXmlnsAttrNs=False, + preventDoubleDashComments=False, + preventDashAtCommentEnd=False, + replaceFormFeedCharacters=True, + preventSingleQuotePubid=False): self.dropXmlnsLocalName = dropXmlnsLocalName self.dropXmlnsAttrNs = dropXmlnsAttrNs @@ -113,14 +202,17 @@ class InfosetFilter(object): self.replaceFormFeedCharacters = replaceFormFeedCharacters + self.preventSingleQuotePubid = preventSingleQuotePubid + self.replaceCache = {} def coerceAttribute(self, name, namespace=None): if self.dropXmlnsLocalName and name.startswith("xmlns:"): - #Need a datalosswarning here + warnings.warn("Attributes cannot begin with xmlns", DataLossWarning) return None - elif (self.dropXmlnsAttrNs and + elif (self.dropXmlnsAttrNs and namespace == "http://www.w3.org/2000/xmlns/"): + warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning) return None else: return self.toXmlName(name) @@ -131,20 +223,35 @@ class InfosetFilter(object): def coerceComment(self, data): if self.preventDoubleDashComments: while "--" in data: + warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning) data = data.replace("--", "- -") return data - + def coerceCharacters(self, data): if self.replaceFormFeedCharacters: + for i in range(data.count("\x0C")): + warnings.warn("Text cannot contain U+000C", DataLossWarning) data = data.replace("\x0C", " ") - #Other non-xml characters + # Other non-xml characters return data + def coercePubid(self, data): + dataOutput = data + for char in nonPubidCharRegexp.findall(data): + warnings.warn("Coercing non-XML pubid", DataLossWarning) + replacement = self.getReplacementCharacter(char) + dataOutput = dataOutput.replace(char, replacement) + if self.preventSingleQuotePubid and dataOutput.find("'") >= 0: + warnings.warn("Pubid cannot contain single quote", DataLossWarning) + dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'")) + return dataOutput + def toXmlName(self, name): nameFirst = name[0] nameRest = name[1:] m = nonXmlNameFirstBMPRegexp.match(nameFirst) if m: + warnings.warn("Coercing non-XML name", DataLossWarning) nameFirstOutput = self.getReplacementCharacter(nameFirst) else: nameFirstOutput = nameFirst @@ -152,10 +259,11 @@ class InfosetFilter(object): nameRestOutput = nameRest replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest)) for char in replaceChars: + warnings.warn("Coercing non-XML name", DataLossWarning) replacement = self.getReplacementCharacter(char) nameRestOutput = nameRestOutput.replace(char, replacement) return nameFirstOutput + nameRestOutput - + def getReplacementCharacter(self, char): if char in self.replaceCache: replacement = self.replaceCache[char] @@ -169,9 +277,9 @@ class InfosetFilter(object): return name def escapeChar(self, char): - replacement = "U" + hex(ord(char))[2:].upper().rjust(5, "0") + replacement = "U%05X" % ord(char) self.replaceCache[char] = replacement return replacement def unescapeChar(self, charcode): - return unichr(int(charcode[1:], 16)) + return chr(int(charcode[1:], 16)) diff --git a/libs/html5lib/inputstream.py b/libs/html5lib/inputstream.py index edec1329..004bdd4a 100644 --- a/libs/html5lib/inputstream.py +++ b/libs/html5lib/inputstream.py @@ -1,19 +1,33 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + import codecs import re -import types -import sys -from constants import EOF, spaceCharacters, asciiLetters, asciiUppercase -from constants import encodings, ReparseException -import utils +from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase +from .constants import encodings, ReparseException +from . import utils -#Non-unicode versions of constants for use in the pre-parser -spaceCharactersBytes = frozenset([str(item) for item in spaceCharacters]) -asciiLettersBytes = frozenset([str(item) for item in asciiLetters]) -asciiUppercaseBytes = frozenset([str(item) for item in asciiUppercase]) -spacesAngleBrackets = spaceCharactersBytes | frozenset([">", "<"]) +from io import StringIO -invalid_unicode_re = re.compile(u"[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") +try: + from io import BytesIO +except ImportError: + BytesIO = StringIO + +try: + from io import BufferedIOBase +except ImportError: + class BufferedIOBase(object): + pass + +# Non-unicode versions of constants for use in the pre-parser +spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters]) +asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters]) +asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase]) +spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"]) + +invalid_unicode_re = re.compile("[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, @@ -23,22 +37,23 @@ non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF]) -ascii_punctuation_re = re.compile(ur"[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") +ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") # Cache for charsUntil() charsUntilRegEx = {} - -class BufferedStream: + + +class BufferedStream(object): """Buffering for streams that do not have buffering of their own - The buffer is implemented as a list of chunks on the assumption that + The buffer is implemented as a list of chunks on the assumption that joining many strings will be slow since it is O(n**2) """ - + def __init__(self, stream): self.stream = stream self.buffer = [] - self.position = [-1,0] #chunk number, offset + self.position = [-1, 0] # chunk number, offset def tell(self): pos = 0 @@ -48,11 +63,11 @@ class BufferedStream: return pos def seek(self, pos): - assert pos < self._bufferedBytes() + assert pos <= self._bufferedBytes() offset = pos i = 0 while len(self.buffer[i]) < offset: - offset -= pos + offset -= len(self.buffer[i]) i += 1 self.position = [i, offset] @@ -64,7 +79,7 @@ class BufferedStream: return self._readStream(bytes) else: return self._readFromBuffer(bytes) - + def _bufferedBytes(self): return sum([len(item) for item in self.buffer]) @@ -83,7 +98,7 @@ class BufferedStream: while bufferIndex < len(self.buffer) and remainingBytes != 0: assert remainingBytes > 0 bufferedData = self.buffer[bufferIndex] - + if remainingBytes <= len(bufferedData) - bufferOffset: bytesToRead = remainingBytes self.position = [bufferIndex, bufferOffset + bytesToRead] @@ -91,20 +106,33 @@ class BufferedStream: bytesToRead = len(bufferedData) - bufferOffset self.position = [bufferIndex, len(bufferedData)] bufferIndex += 1 - data = rv.append(bufferedData[bufferOffset: - bufferOffset + bytesToRead]) + rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead]) remainingBytes -= bytesToRead bufferOffset = 0 if remainingBytes: rv.append(self._readStream(remainingBytes)) - - return "".join(rv) - + + return b"".join(rv) -class HTMLInputStream: +def HTMLInputStream(source, encoding=None, parseMeta=True, chardet=True): + if hasattr(source, "read"): + isUnicode = isinstance(source.read(0), text_type) + else: + isUnicode = isinstance(source, text_type) + + if isUnicode: + if encoding is not None: + raise TypeError("Cannot explicitly set an encoding with a unicode string") + + return HTMLUnicodeInputStream(source) + else: + return HTMLBinaryInputStream(source, encoding, parseMeta, chardet) + + +class HTMLUnicodeInputStream(object): """Provides a unicode stream of characters to the HTMLTokenizer. This class takes care of character encoding and removing or replacing @@ -114,7 +142,7 @@ class HTMLInputStream: _defaultChunkSize = 10240 - def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + def __init__(self, source): """Initialises the HTMLInputStream. HTMLInputStream(source, [encoding]) -> Normalized stream from source @@ -126,49 +154,29 @@ class HTMLInputStream: the encoding. If specified, that encoding will be used, regardless of any BOM or later declaration (such as in a meta element) - + parseMeta - Look for a element containing encoding information """ - #Craziness - if len(u"\U0010FFFF") == 1: + # Craziness + if len("\U0010FFFF") == 1: self.reportCharacterErrors = self.characterErrorsUCS4 - self.replaceCharactersRegexp = re.compile(u"[\uD800-\uDFFF]") + self.replaceCharactersRegexp = re.compile("[\uD800-\uDFFF]") else: self.reportCharacterErrors = self.characterErrorsUCS2 - self.replaceCharactersRegexp = re.compile(u"([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 1: lastv = ord(data[-1]) if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF: self._bufferedCharacter = data[-1] data = data[:-1] - + self.reportCharacterErrors(data) - + # Replace invalid characters # Note U+0000 is dealt with in the tokenizer - data = self.replaceCharactersRegexp.sub(u"\ufffd", data) - - data = data.replace(u"\r\n", u"\n") - data = data.replace(u"\r", u"\n") + data = self.replaceCharactersRegexp.sub("\ufffd", data) + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") self.chunk = data self.chunkSize = len(data) @@ -378,23 +275,22 @@ class HTMLInputStream: return True def characterErrorsUCS4(self, data): - for i in xrange(len(invalid_unicode_re.findall(data))): + for i in range(len(invalid_unicode_re.findall(data))): self.errors.append("invalid-codepoint") def characterErrorsUCS2(self, data): - #Someone picked the wrong compile option - #You lose + # Someone picked the wrong compile option + # You lose skip = False - import sys for match in invalid_unicode_re.finditer(data): if skip: continue codepoint = ord(match.group()) pos = match.start() - #Pretty sure there should be endianness issues here - if utils.isSurrogatePair(data[pos:pos+2]): - #We have a surrogate pair! - char_val = utils.surrogatePairToCodepoint(data[pos:pos+2]) + # Pretty sure there should be endianness issues here + if utils.isSurrogatePair(data[pos:pos + 2]): + # We have a surrogate pair! + char_val = utils.surrogatePairToCodepoint(data[pos:pos + 2]) if char_val in non_bmp_invalid_codepoints: self.errors.append("invalid-codepoint") skip = True @@ -405,7 +301,7 @@ class HTMLInputStream: skip = False self.errors.append("invalid-codepoint") - def charsUntil(self, characters, opposite = False): + def charsUntil(self, characters, opposite=False): """ Returns a string of characters from the stream up to but not including any character in 'characters' or EOF. 'characters' must be a container that supports the 'in' method and iteration over its @@ -417,12 +313,12 @@ class HTMLInputStream: chars = charsUntilRegEx[(characters, opposite)] except KeyError: if __debug__: - for c in characters: + for c in characters: assert(ord(c) < 128) - regex = u"".join([u"\\x%02x" % ord(c) for c in characters]) + regex = "".join(["\\x%02x" % ord(c) for c in characters]) if not opposite: - regex = u"^%s" % regex - chars = charsUntilRegEx[(characters, opposite)] = re.compile(u"[%s]+" % regex) + regex = "^%s" % regex + chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex) rv = [] @@ -449,7 +345,7 @@ class HTMLInputStream: # Reached EOF break - r = u"".join(rv) + r = "".join(rv) return r def unget(self, char): @@ -468,26 +364,210 @@ class HTMLInputStream: self.chunkOffset -= 1 assert self.chunk[self.chunkOffset] == char -class EncodingBytes(str): + +class HTMLBinaryInputStream(HTMLUnicodeInputStream): + """Provides a unicode stream of characters to the HTMLTokenizer. + + This class takes care of character encoding and removing or replacing + incorrect byte-sequences and also provides column and line tracking. + + """ + + def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + """Initialises the HTMLInputStream. + + HTMLInputStream(source, [encoding]) -> Normalized stream from source + for use by html5lib. + + source can be either a file-object, local filename or a string. + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + + parseMeta - Look for a element containing encoding information + + """ + # Raw Stream - for unicode objects this will encode to utf-8 and set + # self.charEncoding as appropriate + self.rawStream = self.openStream(source) + + HTMLUnicodeInputStream.__init__(self, self.rawStream) + + self.charEncoding = (codecName(encoding), "certain") + + # Encoding Information + # Number of bytes to use when looking for a meta element with + # encoding information + self.numBytesMeta = 512 + # Number of bytes to use when using detecting encoding using chardet + self.numBytesChardet = 100 + # Encoding to use if no other information can be found + self.defaultEncoding = "windows-1252" + + # Detect encoding iff no explicit "transport level" encoding is supplied + if (self.charEncoding[0] is None): + self.charEncoding = self.detectEncoding(parseMeta, chardet) + + # Call superclass + self.reset() + + def reset(self): + self.dataStream = codecs.getreader(self.charEncoding[0])(self.rawStream, + 'replace') + HTMLUnicodeInputStream.reset(self) + + def openStream(self, source): + """Produces a file object from source. + + source can be either a file object, local filename or a string. + + """ + # Already a file object + if hasattr(source, 'read'): + stream = source + else: + stream = BytesIO(source) + + try: + stream.seek(stream.tell()) + except: + stream = BufferedStream(stream) + + return stream + + def detectEncoding(self, parseMeta=True, chardet=True): + # First look for a BOM + # This will also read past the BOM if present + encoding = self.detectBOM() + confidence = "certain" + # If there is no BOM need to look for meta elements with encoding + # information + if encoding is None and parseMeta: + encoding = self.detectEncodingMeta() + confidence = "tentative" + # Guess with chardet, if avaliable + if encoding is None and chardet: + confidence = "tentative" + try: + try: + from charade.universaldetector import UniversalDetector + except ImportError: + from chardet.universaldetector import UniversalDetector + buffers = [] + detector = UniversalDetector() + while not detector.done: + buffer = self.rawStream.read(self.numBytesChardet) + assert isinstance(buffer, bytes) + if not buffer: + break + buffers.append(buffer) + detector.feed(buffer) + detector.close() + encoding = detector.result['encoding'] + self.rawStream.seek(0) + except ImportError: + pass + # If all else fails use the default encoding + if encoding is None: + confidence = "tentative" + encoding = self.defaultEncoding + + # Substitute for equivalent encodings: + encodingSub = {"iso-8859-1": "windows-1252"} + + if encoding.lower() in encodingSub: + encoding = encodingSub[encoding.lower()] + + return encoding, confidence + + def changeEncoding(self, newEncoding): + assert self.charEncoding[1] != "certain" + newEncoding = codecName(newEncoding) + if newEncoding in ("utf-16", "utf-16-be", "utf-16-le"): + newEncoding = "utf-8" + if newEncoding is None: + return + elif newEncoding == self.charEncoding[0]: + self.charEncoding = (self.charEncoding[0], "certain") + else: + self.rawStream.seek(0) + self.reset() + self.charEncoding = (newEncoding, "certain") + raise ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding)) + + def detectBOM(self): + """Attempts to detect at BOM at the start of the stream. If + an encoding can be determined from the BOM return the name of the + encoding otherwise return None""" + bomDict = { + codecs.BOM_UTF8: 'utf-8', + codecs.BOM_UTF16_LE: 'utf-16-le', codecs.BOM_UTF16_BE: 'utf-16-be', + codecs.BOM_UTF32_LE: 'utf-32-le', codecs.BOM_UTF32_BE: 'utf-32-be' + } + + # Go to beginning of file and read in 4 bytes + string = self.rawStream.read(4) + assert isinstance(string, bytes) + + # Try detecting the BOM using bytes from the string + encoding = bomDict.get(string[:3]) # UTF-8 + seek = 3 + if not encoding: + # Need to detect UTF-32 before UTF-16 + encoding = bomDict.get(string) # UTF-32 + seek = 4 + if not encoding: + encoding = bomDict.get(string[:2]) # UTF-16 + seek = 2 + + # Set the read position past the BOM if one was found, otherwise + # set it to the start of the stream + self.rawStream.seek(encoding and seek or 0) + + return encoding + + def detectEncodingMeta(self): + """Report the encoding declared by the meta element + """ + buffer = self.rawStream.read(self.numBytesMeta) + assert isinstance(buffer, bytes) + parser = EncodingParser(buffer) + self.rawStream.seek(0) + encoding = parser.getEncoding() + + if encoding in ("utf-16", "utf-16-be", "utf-16-le"): + encoding = "utf-8" + + return encoding + + +class EncodingBytes(bytes): """String-like object with an associated position and various extra methods If the position is ever greater than the string length then an exception is raised""" def __new__(self, value): - return str.__new__(self, value.lower()) + assert isinstance(value, bytes) + return bytes.__new__(self, value.lower()) def __init__(self, value): - self._position=-1 - + self._position = -1 + def __iter__(self): return self - - def next(self): + + def __next__(self): p = self._position = self._position + 1 if p >= len(self): raise StopIteration elif p < 0: raise TypeError - return self[p] + return self[p:p + 1] + + def next(self): + # Py2 compat + return self.__next__() def previous(self): p = self._position @@ -496,13 +576,13 @@ class EncodingBytes(str): elif p < 0: raise TypeError self._position = p = p - 1 - return self[p] - + return self[p:p + 1] + def setPosition(self, position): if self._position >= len(self): raise StopIteration self._position = position - + def getPosition(self): if self._position >= len(self): raise StopIteration @@ -510,19 +590,19 @@ class EncodingBytes(str): return self._position else: return None - + position = property(getPosition, setPosition) def getCurrentByte(self): - return self[self.position] - + return self[self.position:self.position + 1] + currentByte = property(getCurrentByte) def skip(self, chars=spaceCharactersBytes): """Skip past a list of characters""" p = self.position # use property for the error-checking while p < len(self): - c = self[p] + c = self[p:p + 1] if c not in chars: self._position = p return c @@ -533,7 +613,7 @@ class EncodingBytes(str): def skipUntil(self, chars): p = self.position while p < len(self): - c = self[p] + c = self[p:p + 1] if c in chars: self._position = p return c @@ -542,16 +622,16 @@ class EncodingBytes(str): return None def matchBytes(self, bytes): - """Look for a sequence of bytes at the start of a string. If the bytes - are found return True and advance the position to the byte after the + """Look for a sequence of bytes at the start of a string. If the bytes + are found return True and advance the position to the byte after the match. Otherwise return False and leave the position alone""" p = self.position - data = self[p:p+len(bytes)] + data = self[p:p + len(bytes)] rv = data.startswith(bytes) if rv: self.position += len(bytes) return rv - + def jumpTo(self, bytes): """Look for the next sequence of bytes matching a given sequence. If a match is found advance the position to the last byte of the match""" @@ -560,11 +640,12 @@ class EncodingBytes(str): # XXX: This is ugly, but I can't see a nicer way to fix this. if self._position == -1: self._position = 0 - self._position += (newPosition + len(bytes)-1) + self._position += (newPosition + len(bytes) - 1) return True else: raise StopIteration + class EncodingParser(object): """Mini parser for detecting character encoding from meta elements""" @@ -575,147 +656,158 @@ class EncodingParser(object): def getEncoding(self): methodDispatch = ( - ("") + return self.data.jumpTo(b"-->") def handleMeta(self): if self.data.currentByte not in spaceCharactersBytes: - #if we have ") + return self.data.jumpTo(b">") def getAttribute(self): - """Return a name,value pair for the next attribute in the stream, + """Return a name,value pair for the next attribute in the stream, if one is found, or None""" data = self.data # Step 1 (skip chars) - c = data.skip(spaceCharactersBytes | frozenset("/")) + c = data.skip(spaceCharactersBytes | frozenset([b"/"])) + assert c is None or len(c) == 1 # Step 2 - if c in (">", None): + if c in (b">", None): return None # Step 3 attrName = [] attrValue = [] - #Step 4 attribute name + # Step 4 attribute name while True: - if c == "=" and attrName: + if c == b"=" and attrName: break elif c in spaceCharactersBytes: - #Step 6! + # Step 6! c = data.skip() - c = data.next() break - elif c in ("/", ">"): - return "".join(attrName), "" + elif c in (b"/", b">"): + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrName.append(c.lower()) - elif c == None: + elif c is None: return None else: attrName.append(c) - #Step 5 - c = data.next() - #Step 7 - if c != "=": + # Step 5 + c = next(data) + # Step 7 + if c != b"=": data.previous() - return "".join(attrName), "" - #Step 8 - data.next() - #Step 9 + return b"".join(attrName), b"" + # Step 8 + next(data) + # Step 9 c = data.skip() - #Step 10 - if c in ("'", '"'): - #10.1 + # Step 10 + if c in (b"'", b'"'): + # 10.1 quoteChar = c while True: - #10.2 - c = data.next() - #10.3 + # 10.2 + c = next(data) + # 10.3 if c == quoteChar: - data.next() - return "".join(attrName), "".join(attrValue) - #10.4 + next(data) + return b"".join(attrName), b"".join(attrValue) + # 10.4 elif c in asciiUppercaseBytes: attrValue.append(c.lower()) - #10.5 + # 10.5 else: attrValue.append(c) - elif c == ">": - return "".join(attrName), "" + elif c == b">": + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -724,9 +816,9 @@ class EncodingParser(object): attrValue.append(c) # Step 11 while True: - c = data.next() + c = next(data) if c in spacesAngleBrackets: - return "".join(attrName), "".join(attrValue) + return b"".join(attrName), b"".join(attrValue) elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -737,21 +829,23 @@ class EncodingParser(object): class ContentAttrParser(object): def __init__(self, data): + assert isinstance(data, bytes) self.data = data + def parse(self): try: - #Check if the attr name is charset - #otherwise return - self.data.jumpTo("charset") + # Check if the attr name is charset + # otherwise return + self.data.jumpTo(b"charset") self.data.position += 1 self.data.skip() - if not self.data.currentByte == "=": - #If there is no = sign keep looking for attrs + if not self.data.currentByte == b"=": + # If there is no = sign keep looking for attrs return None self.data.position += 1 self.data.skip() - #Look for an encoding between matching quote marks - if self.data.currentByte in ('"', "'"): + # Look for an encoding between matching quote marks + if self.data.currentByte in (b'"', b"'"): quoteMark = self.data.currentByte self.data.position += 1 oldPosition = self.data.position @@ -760,13 +854,13 @@ class ContentAttrParser(object): else: return None else: - #Unquoted value + # Unquoted value oldPosition = self.data.position try: self.data.skipUntil(spaceCharactersBytes) return self.data[oldPosition:self.data.position] except StopIteration: - #Return the whole remaining value + # Return the whole remaining value return self.data[oldPosition:] except StopIteration: return None @@ -775,7 +869,12 @@ class ContentAttrParser(object): def codecName(encoding): """Return the python codec name corresponding to an encoding or None if the string doesn't correspond to a valid encoding.""" - if (encoding is not None and type(encoding) in types.StringTypes): + if isinstance(encoding, bytes): + try: + encoding = encoding.decode("ascii") + except UnicodeDecodeError: + return None + if encoding: canonicalName = ascii_punctuation_re.sub("", encoding).lower() return encodings.get(canonicalName, None) else: diff --git a/libs/html5lib/sanitizer.py b/libs/html5lib/sanitizer.py index ae4c7d83..71dc5212 100644 --- a/libs/html5lib/sanitizer.py +++ b/libs/html5lib/sanitizer.py @@ -1,142 +1,145 @@ +from __future__ import absolute_import, division, unicode_literals + import re from xml.sax.saxutils import escape, unescape -from tokenizer import HTMLTokenizer -from constants import tokenTypes +from .tokenizer import HTMLTokenizer +from .constants import tokenTypes + class HTMLSanitizerMixin(object): """ sanitization of XHTML+MathML+SVG and of inline style attributes.""" acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] - + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi', - 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', - 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', - 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', - 'munderover', 'none'] - + 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', + 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', + 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none'] + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', - 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', - 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', - 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', - 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', - 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] - + 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', + 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', + 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', - 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', - 'background', 'balance', 'bgcolor', 'bgproperties', 'border', - 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', - 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', - 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', - 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', - 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', - 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', - 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', - 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', - 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', - 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', - 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', - 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', - 'optimum', 'pattern', 'ping', 'point-size', 'prompt', 'pqg', - 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', - 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', - 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', - 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', - 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', - 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', - 'width', 'wrap', 'xml:lang'] + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', + 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', + 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', + 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', + 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', + 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', + 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', + 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', + 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', + 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', + 'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload', + 'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', + 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', + 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', + 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', + 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', + 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', + 'width', 'wrap', 'xml:lang'] mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', - 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', - 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', - 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', - 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', - 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', - 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', - 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', - 'xlink:type', 'xmlns', 'xmlns:xlink'] - - svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', - 'arabic-form', 'ascent', 'attributeName', 'attributeType', - 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', - 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', - 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', - 'fill-opacity', 'fill-rule', 'font-family', 'font-size', - 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', - 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', - 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', - 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', - 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', - 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', - 'opacity', 'orient', 'origin', 'overline-position', - 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', - 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', - 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', - 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', - 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', - 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', - 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', - 'transform', 'type', 'u1', 'u2', 'underline-position', - 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', - 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', - 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', - 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', - 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', - 'y1', 'y2', 'zoomAndPan'] + 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', + 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', + 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', + 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', + 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', + 'xlink:type', 'xmlns', 'xmlns:xlink'] - attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', - 'xlink:href', 'xml:base'] + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', + 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', + 'fill-opacity', 'fill-rule', 'font-family', 'font-size', + 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', + 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', + 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', + 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', + 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', + 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', + 'opacity', 'orient', 'origin', 'overline-position', + 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', + 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', + 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', + 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', + 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', + 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', + 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', + 'transform', 'type', 'u1', 'u2', 'underline-position', + 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', + 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', + 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', + 'y1', 'y2', 'zoomAndPan'] + + attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster', + 'xlink:href', 'xml:base'] svg_attr_val_allows_ref = ['clip-path', 'color-profile', 'cursor', 'fill', - 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', - 'mask', 'stroke'] + 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', + 'mask', 'stroke'] svg_allow_local_href = ['altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', - 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', - 'set', 'use'] - + 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', + 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', + 'set', 'use'] + acceptable_css_properties = ['azimuth', 'background-color', - 'border-bottom-color', 'border-collapse', 'border-color', - 'border-left-color', 'border-right-color', 'border-top-color', 'clear', - 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', - 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', - 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', - 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', - 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', - 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', - 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', - 'white-space', 'width'] - + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', - 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', - 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', - 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', - 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', - 'transparent', 'underline', 'white', 'yellow'] - - acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', - 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', - 'stroke-opacity'] - - acceptable_protocols = [ 'ed2k', 'ftp', 'http', 'https', 'irc', - 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', - 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', - 'ssh', 'sftp', 'rtsp', 'afs' ] - + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + acceptable_svg_properties = ['fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] + + acceptable_protocols = ['ed2k', 'ftp', 'http', 'https', 'irc', + 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', + 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', + 'ssh', 'sftp', 'rtsp', 'afs'] + # subclasses may define their own versions of these constants allowed_elements = acceptable_elements + mathml_elements + svg_elements allowed_attributes = acceptable_attributes + mathml_attributes + svg_attributes @@ -160,94 +163,104 @@ class HTMLSanitizerMixin(object): # accommodate filters which use token_type differently token_type = token["type"] - if token_type in tokenTypes.keys(): - token_type = tokenTypes[token_type] + if token_type in list(tokenTypes.keys()): + token_type = tokenTypes[token_type] - if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], - tokenTypes["EmptyTag"]): + if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"]): if token["name"] in self.allowed_elements: - if token.has_key("data"): - attrs = dict([(name,val) for name,val in - token["data"][::-1] - if name in self.allowed_attributes]) - for attr in self.attr_val_is_uri: - if not attrs.has_key(attr): - continue - val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', - unescape(attrs[attr])).lower() - #remove replacement characters from unescaped characters - val_unescaped = val_unescaped.replace(u"\ufffd", "") - if (re.match("^[a-z0-9][-+.a-z0-9]*:",val_unescaped) and - (val_unescaped.split(':')[0] not in - self.allowed_protocols)): - del attrs[attr] - for attr in self.svg_attr_val_allows_ref: - if attr in attrs: - attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', - ' ', - unescape(attrs[attr])) - if (token["name"] in self.svg_allow_local_href and - 'xlink:href' in attrs and re.search('^\s*[^#\s].*', - attrs['xlink:href'])): - del attrs['xlink:href'] - if attrs.has_key('style'): - attrs['style'] = self.sanitize_css(attrs['style']) - token["data"] = [[name,val] for name,val in attrs.items()] - return token + return self.allowed_token(token, token_type) else: - if token_type == tokenTypes["EndTag"]: - token["data"] = "%s>" % token["name"] - elif token["data"]: - attrs = ''.join([' %s="%s"' % (k,escape(v)) for k,v in token["data"]]) - token["data"] = "<%s%s>" % (token["name"],attrs) - else: - token["data"] = "<%s>" % token["name"] - if token.get("selfClosing"): - token["data"]=token["data"][:-1] + "/>" - - if token["type"] in tokenTypes.keys(): - token["type"] = "Characters" - else: - token["type"] = tokenTypes["Characters"] - - del token["name"] - return token + return self.disallowed_token(token, token_type) elif token_type == tokenTypes["Comment"]: pass else: return token + def allowed_token(self, token, token_type): + if "data" in token: + attrs = dict([(name, val) for name, val in + token["data"][::-1] + if name in self.allowed_attributes]) + for attr in self.attr_val_is_uri: + if attr not in attrs: + continue + val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', + unescape(attrs[attr])).lower() + # remove replacement characters from unescaped characters + val_unescaped = val_unescaped.replace("\ufffd", "") + if (re.match("^[a-z0-9][-+.a-z0-9]*:", val_unescaped) and + (val_unescaped.split(':')[0] not in + self.allowed_protocols)): + del attrs[attr] + for attr in self.svg_attr_val_allows_ref: + if attr in attrs: + attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', + ' ', + unescape(attrs[attr])) + if (token["name"] in self.svg_allow_local_href and + 'xlink:href' in attrs and re.search('^\s*[^#\s].*', + attrs['xlink:href'])): + del attrs['xlink:href'] + if 'style' in attrs: + attrs['style'] = self.sanitize_css(attrs['style']) + token["data"] = [[name, val] for name, val in list(attrs.items())] + return token + + def disallowed_token(self, token, token_type): + if token_type == tokenTypes["EndTag"]: + token["data"] = "%s>" % token["name"] + elif token["data"]: + attrs = ''.join([' %s="%s"' % (k, escape(v)) for k, v in token["data"]]) + token["data"] = "<%s%s>" % (token["name"], attrs) + else: + token["data"] = "<%s>" % token["name"] + if token.get("selfClosing"): + token["data"] = token["data"][:-1] + "/>" + + if token["type"] in list(tokenTypes.keys()): + token["type"] = "Characters" + else: + token["type"] = tokenTypes["Characters"] + + del token["name"] + return token + def sanitize_css(self, style): # disallow urls - style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) + style = re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) # gauntlet - if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' - if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): return '' + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): + return '' clean = [] - for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): - if not value: continue - if prop.lower() in self.allowed_css_properties: - clean.append(prop + ': ' + value + ';') - elif prop.split('-')[0].lower() in ['background','border','margin', - 'padding']: - for keyword in value.split(): - if not keyword in self.acceptable_css_keywords and \ - not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$",keyword): - break - else: - clean.append(prop + ': ' + value + ';') - elif prop.lower() in self.allowed_svg_properties: - clean.append(prop + ': ' + value + ';') + for prop, value in re.findall("([-\w]+)\s*:\s*([^:;]*)", style): + if not value: + continue + if prop.lower() in self.allowed_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background', 'border', 'margin', + 'padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif prop.lower() in self.allowed_svg_properties: + clean.append(prop + ': ' + value + ';') return ' '.join(clean) + class HTMLSanitizer(HTMLTokenizer, HTMLSanitizerMixin): def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True, lowercaseElementName=False, lowercaseAttrName=False, parser=None): - #Change case matching defaults as we only output lowercase html anyway - #This solution doesn't seem ideal... + # Change case matching defaults as we only output lowercase html anyway + # This solution doesn't seem ideal... HTMLTokenizer.__init__(self, stream, encoding, parseMeta, useChardet, lowercaseElementName, lowercaseAttrName, parser=parser) diff --git a/libs/html5lib/serializer/__init__.py b/libs/html5lib/serializer/__init__.py index 1b746655..8380839a 100644 --- a/libs/html5lib/serializer/__init__.py +++ b/libs/html5lib/serializer/__init__.py @@ -1,17 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals -from html5lib import treewalkers +from .. import treewalkers -from htmlserializer import HTMLSerializer -from xhtmlserializer import XHTMLSerializer +from .htmlserializer import HTMLSerializer -def serialize(input, tree="simpletree", format="html", encoding=None, + +def serialize(input, tree="etree", format="html", encoding=None, **serializer_opts): # XXX: Should we cache this? - walker = treewalkers.getTreeWalker(tree) + walker = treewalkers.getTreeWalker(tree) if format == "html": s = HTMLSerializer(**serializer_opts) - elif format == "xhtml": - s = XHTMLSerializer(**serializer_opts) else: - raise ValueError, "type must be either html or xhtml" + raise ValueError("type must be html") return s.render(walker(input), encoding) diff --git a/libs/html5lib/serializer/htmlserializer.py b/libs/html5lib/serializer/htmlserializer.py index 8dd0a815..412a5a22 100644 --- a/libs/html5lib/serializer/htmlserializer.py +++ b/libs/html5lib/serializer/htmlserializer.py @@ -1,18 +1,20 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type import gettext _ = gettext.gettext -from html5lib.constants import voidElements, booleanAttributes, spaceCharacters -from html5lib.constants import rcdataElements, entities, xmlEntities -from html5lib import utils +try: + from functools import reduce +except ImportError: + pass + +from ..constants import voidElements, booleanAttributes, spaceCharacters +from ..constants import rcdataElements, entities, xmlEntities +from .. import utils from xml.sax.saxutils import escape -spaceCharacters = u"".join(spaceCharacters) +spaceCharacters = "".join(spaceCharacters) try: from codecs import register_error, xmlcharrefreplace_errors @@ -21,24 +23,18 @@ except ImportError: else: unicode_encode_errors = "htmlentityreplace" - from html5lib.constants import entities - encode_entity_map = {} - is_ucs4 = len(u"\U0010FFFF") == 1 - for k, v in entities.items(): - #skip multi-character entities + is_ucs4 = len("\U0010FFFF") == 1 + for k, v in list(entities.items()): + # skip multi-character entities if ((is_ucs4 and len(v) > 1) or - (not is_ucs4 and len(v) > 2)): + (not is_ucs4 and len(v) > 2)): continue if v != "&": if len(v) == 2: v = utils.surrogatePairToCodepoint(v) else: - try: - v = ord(v) - except: - print v - raise + v = ord(v) if not v in encode_entity_map or k.islower(): # prefer < over < and similarly for &, >, etc. encode_entity_map[v] = k @@ -53,8 +49,8 @@ else: skip = False continue index = i + exc.start - if utils.isSurrogatePair(exc.object[index:min([exc.end, index+2])]): - codepoint = utils.surrogatePairToCodepoint(exc.object[index:index+2]) + if utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]): + codepoint = utils.surrogatePairToCodepoint(exc.object[index:index + 2]) skip = True else: codepoint = ord(c) @@ -67,8 +63,8 @@ else: if not e.endswith(";"): res.append(";") else: - res.append("%s;"%(hex(cp)[2:])) - return (u"".join(res), exc.end) + res.append("%s;" % (hex(cp)[2:])) + return ("".join(res), exc.end) else: return xmlcharrefreplace_errors(exc) @@ -81,7 +77,7 @@ class HTMLSerializer(object): # attribute quoting options quote_attr_values = False - quote_char = u'"' + quote_char = '"' use_best_quote_char = True # tag syntax options @@ -96,15 +92,17 @@ class HTMLSerializer(object): resolve_entities = True # miscellaneous options + alphabetical_attributes = False inject_meta_charset = True strip_whitespace = False sanitize = False options = ("quote_attr_values", "quote_char", "use_best_quote_char", - "minimize_boolean_attributes", "use_trailing_solidus", - "space_before_trailing_solidus", "omit_optional_tags", - "strip_whitespace", "inject_meta_charset", "escape_lt_in_attrs", - "escape_rcdata", "resolve_entities", "sanitize") + "omit_optional_tags", "minimize_boolean_attributes", + "use_trailing_solidus", "space_before_trailing_solidus", + "escape_lt_in_attrs", "escape_rcdata", "resolve_entities", + "alphabetical_attributes", "inject_meta_charset", + "strip_whitespace", "sanitize") def __init__(self, **kwargs): """Initialize HTMLSerializer. @@ -147,10 +145,12 @@ class HTMLSerializer(object): See `html5lib user documentation`_ omit_optional_tags=True|False Omit start/end tags that are optional. + alphabetical_attributes=False|True + Reorder attributes to be in alphabetical order. .. _html5lib user documentation: http://code.google.com/p/html5lib/wiki/UserDocumentation """ - if kwargs.has_key('quote_char'): + if 'quote_char' in kwargs: self.use_best_quote_char = False for attr in self.options: setattr(self, attr, kwargs.get(attr, getattr(self, attr))) @@ -158,14 +158,14 @@ class HTMLSerializer(object): self.strict = False def encode(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, unicode_encode_errors) else: return string def encodeStrict(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, "strict") else: @@ -175,39 +175,46 @@ class HTMLSerializer(object): self.encoding = encoding in_cdata = False self.errors = [] + if encoding and self.inject_meta_charset: - from html5lib.filters.inject_meta_charset import Filter + from ..filters.inject_meta_charset import Filter treewalker = Filter(treewalker, encoding) - # XXX: WhitespaceFilter should be used before OptionalTagFilter + # WhitespaceFilter should be used before OptionalTagFilter # for maximum efficiently of this latter filter if self.strip_whitespace: - from html5lib.filters.whitespace import Filter + from ..filters.whitespace import Filter treewalker = Filter(treewalker) if self.sanitize: - from html5lib.filters.sanitizer import Filter + from ..filters.sanitizer import Filter treewalker = Filter(treewalker) if self.omit_optional_tags: - from html5lib.filters.optionaltags import Filter + from ..filters.optionaltags import Filter treewalker = Filter(treewalker) + # Alphabetical attributes must be last, as other filters + # could add attributes and alter the order + if self.alphabetical_attributes: + from ..filters.alphabeticalattributes import Filter + treewalker = Filter(treewalker) + for token in treewalker: type = token["type"] if type == "Doctype": - doctype = u"= 0: - if token["systemId"].find(u"'") >= 0: + doctype += " SYSTEM" + if token["systemId"]: + if token["systemId"].find('"') >= 0: + if token["systemId"].find("'") >= 0: self.serializeError(_("System identifer contains both single and double quote characters")) - quote_char = u"'" + quote_char = "'" else: - quote_char = u'"' - doctype += u" %s%s%s" % (quote_char, token["systemId"], quote_char) - - doctype += u">" + quote_char = '"' + doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char) + + doctype += ">" yield self.encodeStrict(doctype) elif type in ("Characters", "SpaceCharacters"): @@ -220,41 +227,41 @@ class HTMLSerializer(object): elif type in ("StartTag", "EmptyTag"): name = token["name"] - yield self.encodeStrict(u"<%s" % name) + yield self.encodeStrict("<%s" % name) if name in rcdataElements and not self.escape_rcdata: in_cdata = True elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - attributes = [] - for (attr_namespace,attr_name),attr_value in sorted(token["data"].items()): - #TODO: Add namespace support here + for (attr_namespace, attr_name), attr_value in token["data"].items(): + # TODO: Add namespace support here k = attr_name v = attr_value - yield self.encodeStrict(u' ') + yield self.encodeStrict(' ') yield self.encodeStrict(k) if not self.minimize_boolean_attributes or \ - (k not in booleanAttributes.get(name, tuple()) \ - and k not in booleanAttributes.get("", tuple())): - yield self.encodeStrict(u"=") + (k not in booleanAttributes.get(name, tuple()) + and k not in booleanAttributes.get("", tuple())): + yield self.encodeStrict("=") if self.quote_attr_values or not v: quote_attr = True else: - quote_attr = reduce(lambda x,y: x or (y in v), - spaceCharacters + u">\"'=", False) - v = v.replace(u"&", u"&") - if self.escape_lt_in_attrs: v = v.replace(u"<", u"<") + quote_attr = reduce(lambda x, y: x or (y in v), + spaceCharacters + ">\"'=", False) + v = v.replace("&", "&") + if self.escape_lt_in_attrs: + v = v.replace("<", "<") if quote_attr: quote_char = self.quote_char if self.use_best_quote_char: - if u"'" in v and u'"' not in v: - quote_char = u'"' - elif u'"' in v and u"'" not in v: - quote_char = u"'" - if quote_char == u"'": - v = v.replace(u"'", u"'") + if "'" in v and '"' not in v: + quote_char = '"' + elif '"' in v and "'" not in v: + quote_char = "'" + if quote_char == "'": + v = v.replace("'", "'") else: - v = v.replace(u'"', u""") + v = v.replace('"', """) yield self.encodeStrict(quote_char) yield self.encode(v) yield self.encodeStrict(quote_char) @@ -262,10 +269,10 @@ class HTMLSerializer(object): yield self.encode(v) if name in voidElements and self.use_trailing_solidus: if self.space_before_trailing_solidus: - yield self.encodeStrict(u" /") + yield self.encodeStrict(" /") else: - yield self.encodeStrict(u"/") - yield self.encode(u">") + yield self.encodeStrict("/") + yield self.encode(">") elif type == "EndTag": name = token["name"] @@ -273,13 +280,13 @@ class HTMLSerializer(object): in_cdata = False elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - yield self.encodeStrict(u"%s>" % name) + yield self.encodeStrict("%s>" % name) elif type == "Comment": data = token["data"] if data.find("--") >= 0: self.serializeError(_("Comment contains --")) - yield self.encodeStrict(u"" % token["data"]) + yield self.encodeStrict("" % token["data"]) elif type == "Entity": name = token["name"] @@ -289,7 +296,7 @@ class HTMLSerializer(object): if self.resolve_entities and key not in xmlEntities: data = entities[key] else: - data = u"&%s;" % name + data = "&%s;" % name yield self.encodeStrict(data) else: @@ -297,9 +304,9 @@ class HTMLSerializer(object): def render(self, treewalker, encoding=None): if encoding: - return "".join(list(self.serialize(treewalker, encoding))) + return b"".join(list(self.serialize(treewalker, encoding))) else: - return u"".join(list(self.serialize(treewalker))) + return "".join(list(self.serialize(treewalker))) def serializeError(self, data="XXX ERROR MESSAGE NEEDED"): # XXX The idea is to make data mandatory. @@ -307,6 +314,7 @@ class HTMLSerializer(object): if self.strict: raise SerializeError + def SerializeError(Exception): """Error in serialized tree""" pass diff --git a/libs/html5lib/serializer/xhtmlserializer.py b/libs/html5lib/serializer/xhtmlserializer.py deleted file mode 100644 index 7fdce47b..00000000 --- a/libs/html5lib/serializer/xhtmlserializer.py +++ /dev/null @@ -1,9 +0,0 @@ -from htmlserializer import HTMLSerializer - -class XHTMLSerializer(HTMLSerializer): - quote_attr_values = True - minimize_boolean_attributes = False - use_trailing_solidus = True - escape_lt_in_attrs = True - omit_optional_tags = False - escape_rcdata = True diff --git a/libs/html5lib/tokenizer.py b/libs/html5lib/tokenizer.py index 7e9eca88..79774578 100644 --- a/libs/html5lib/tokenizer.py +++ b/libs/html5lib/tokenizer.py @@ -1,27 +1,25 @@ +from __future__ import absolute_import, division, unicode_literals + try: - frozenset + chr = unichr # flake8: noqa except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset -try: - from collections import deque -except ImportError: - from utils import deque - -from constants import spaceCharacters -from constants import entitiesWindows1252, entities -from constants import asciiLowercase, asciiLetters, asciiUpper2Lower -from constants import digits, hexDigits, EOF -from constants import tokenTypes, tagTokenTypes -from constants import replacementCharacters + pass -from inputstream import HTMLInputStream +from collections import deque + +from .constants import spaceCharacters +from .constants import entities +from .constants import asciiLetters, asciiUpper2Lower +from .constants import digits, hexDigits, EOF +from .constants import tokenTypes, tagTokenTypes +from .constants import replacementCharacters + +from .inputstream import HTMLInputStream + +from .trie import Trie + +entitiesTrie = Trie(entities) -# Group entities by their first character, for faster lookups -entitiesByFirstChar = {} -for e in entities: - entitiesByFirstChar.setdefault(e[0], []).append(e) class HTMLTokenizer(object): """ This class takes care of tokenizing HTML. @@ -42,10 +40,10 @@ class HTMLTokenizer(object): self.stream = HTMLInputStream(stream, encoding, parseMeta, useChardet) self.parser = parser - #Perform case conversions? + # Perform case conversions? self.lowercaseElementName = lowercaseElementName self.lowercaseAttrName = lowercaseAttrName - + # Setup the initial tokenizer state self.escapeFlag = False self.lastFourChars = [] @@ -100,78 +98,79 @@ class HTMLTokenizer(object): if charAsInt in replacementCharacters: char = replacementCharacters[charAsInt] self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) - elif ((0xD800 <= charAsInt <= 0xDFFF) or + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) + elif ((0xD800 <= charAsInt <= 0xDFFF) or (charAsInt > 0x10FFFF)): - char = u"\uFFFD" + char = "\uFFFD" self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) else: - #Should speed up this check somehow (e.g. move the set to a constant) - if ((0x0001 <= charAsInt <= 0x0008) or - (0x000E <= charAsInt <= 0x001F) or - (0x007F <= charAsInt <= 0x009F) or - (0xFDD0 <= charAsInt <= 0xFDEF) or - charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, + # Should speed up this check somehow (e.g. move the set to a constant) + if ((0x0001 <= charAsInt <= 0x0008) or + (0x000E <= charAsInt <= 0x001F) or + (0x007F <= charAsInt <= 0x009F) or + (0xFDD0 <= charAsInt <= 0xFDEF) or + charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, - 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, + 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, - 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, - 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, - 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, + 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, + 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, + 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF])): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", + "illegal-codepoint-for-numeric-entity", "datavars": {"charAsInt": charAsInt}}) try: # Try/except needed as UCS-2 Python builds' unichar only works # within the BMP. - char = unichr(charAsInt) + char = chr(charAsInt) except ValueError: - char = eval("u'\\U%08x'" % charAsInt) + v = charAsInt - 0x10000 + char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF)) # Discard the ; if present. Otherwise, put it back on the queue and # invoke parseError on parser. - if c != u";": + if c != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "numeric-entity-without-semicolon"}) + "numeric-entity-without-semicolon"}) self.stream.unget(c) return char def consumeEntity(self, allowedChar=None, fromAttribute=False): # Initialise to the default output for when no entity is matched - output = u"&" + output = "&" charStack = [self.stream.char()] - if (charStack[0] in spaceCharacters or charStack[0] in (EOF, u"<", u"&") - or (allowedChar is not None and allowedChar == charStack[0])): + if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") + or (allowedChar is not None and allowedChar == charStack[0])): self.stream.unget(charStack[0]) - elif charStack[0] == u"#": + elif charStack[0] == "#": # Read the next character to see if it's hex or decimal hex = False charStack.append(self.stream.char()) - if charStack[-1] in (u"x", u"X"): + if charStack[-1] in ("x", "X"): hex = True charStack.append(self.stream.char()) # charStack[-1] should be the first digit if (hex and charStack[-1] in hexDigits) \ - or (not hex and charStack[-1] in digits): + or (not hex and charStack[-1] in digits): # At least one digit found, so consume the whole number self.stream.unget(charStack[-1]) output = self.consumeNumberEntity(hex) else: # No digits found self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data": "expected-numeric-entity"}) + "data": "expected-numeric-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: # At this point in the process might have named entity. Entities @@ -179,46 +178,40 @@ class HTMLTokenizer(object): # # Consume characters and compare to these to a substring of the # entity names in the list until the substring no longer matches. - filteredEntityList = entitiesByFirstChar.get(charStack[0], []) - - def entitiesStartingWith(name): - return [e for e in filteredEntityList if e.startswith(name)] - - while (charStack[-1] is not EOF and - entitiesStartingWith("".join(charStack))): + while (charStack[-1] is not EOF): + if not entitiesTrie.has_keys_with_prefix("".join(charStack)): + break charStack.append(self.stream.char()) # At this point we have a string that starts with some characters # that may match an entity - entityName = None - # Try to find the longest entity the string will match to take care # of ¬i for instance. - for entityLength in xrange(len(charStack)-1, 1, -1): - possibleEntityName = "".join(charStack[:entityLength]) - if possibleEntityName in entities: - entityName = possibleEntityName - break + try: + entityName = entitiesTrie.longest_prefix("".join(charStack[:-1])) + entityLength = len(entityName) + except KeyError: + entityName = None if entityName is not None: if entityName[-1] != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "named-entity-without-semicolon"}) + "named-entity-without-semicolon"}) if (entityName[-1] != ";" and fromAttribute and (charStack[entityLength] in asciiLetters or charStack[entityLength] in digits or - charStack[entityLength] == "=")): + charStack[entityLength] == "=")): self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: output = entities[entityName] self.stream.unget(charStack.pop()) - output += u"".join(charStack[entityLength:]) + output += "".join(charStack[entityLength:]) else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-named-entity"}) + "expected-named-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) if fromAttribute: self.currentToken["data"][-1][1] += output @@ -246,28 +239,26 @@ class HTMLTokenizer(object): token["name"] = token["name"].translate(asciiUpper2Lower) if token["type"] == tokenTypes["EndTag"]: if token["data"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"attributes-in-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "attributes-in-end-tag"}) if token["selfClosing"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"self-closing-flag-on-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "self-closing-flag-on-end-tag"}) self.tokenQueue.append(token) self.state = self.dataState - # Below are the various tokenizer states worked out. - def dataState(self): data = self.stream.char() if data == "&": self.state = self.entityDataState elif data == "<": self.state = self.tagOpenState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data":"invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\u0000"}) + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "invalid-codepoint"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\u0000"}) elif data is EOF: # Tokenization ends. return False @@ -276,21 +267,21 @@ class HTMLTokenizer(object): # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def entityDataState(self): self.consumeEntity() self.state = self.dataState return True - + def rcdataState(self): data = self.stream.char() if data == "&": @@ -300,113 +291,113 @@ class HTMLTokenizer(object): elif data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data in spaceCharacters: # Directly after emitting a token you switch back to the "data # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def characterReferenceInRcdata(self): self.consumeEntity() self.state = self.rcdataState return True - + def rawtextState(self): data = self.stream.char() if data == "<": self.state = self.rawtextLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataState(self): data = self.stream.char() if data == "<": self.state = self.scriptDataLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def plaintextState(self): data = self.stream.char() if data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + self.stream.charsUntil(u"\u0000")}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + self.stream.charsUntil("\u0000")}) return True def tagOpenState(self): data = self.stream.char() - if data == u"!": + if data == "!": self.state = self.markupDeclarationOpenState - elif data == u"/": + elif data == "/": self.state = self.closeTagOpenState elif data in asciiLetters: - self.currentToken = {"type": tokenTypes["StartTag"], + self.currentToken = {"type": tokenTypes["StartTag"], "name": data, "data": [], "selfClosing": False, "selfClosingAcknowledged": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-right-bracket"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<>"}) + "expected-tag-name-but-got-right-bracket"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"}) self.state = self.dataState - elif data == u"?": + elif data == "?": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-question-mark"}) + "expected-tag-name-but-got-question-mark"}) self.stream.unget(data) self.state = self.bogusCommentState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + "expected-tag-name"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.dataState return True @@ -415,22 +406,22 @@ class HTMLTokenizer(object): data = self.stream.char() if data in asciiLetters: self.currentToken = {"type": tokenTypes["EndTag"], "name": data, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-right-bracket"}) + "expected-closing-tag-but-got-right-bracket"}) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-eof"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + "expected-closing-tag-but-got-eof"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.state = self.dataState else: # XXX data can be _'_... self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-char", - "datavars": {"data": data}}) + "expected-closing-tag-but-got-char", + "datavars": {"data": data}}) self.stream.unget(data) self.state = self.bogusCommentState return True @@ -439,229 +430,229 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-tag-name"}) + "eof-in-tag-name"}) self.state = self.dataState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" else: self.currentToken["name"] += data # (Don't use charsUntil here, because tag names are # very short and it's faster to not do anything fancy) return True - + def rcdataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rcdataEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rcdataEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rcdataState return True - + def rawtextLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rawtextEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rawtextEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rawtextState return True - + def scriptDataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEndTagOpenState elif data == "!": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapeStartDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.state = self.dataState else: - chars = self.stream.charsUntil((u"<", u"-", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "-", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -669,21 +660,21 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": self.state = self.scriptDataEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -691,61 +682,61 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEscapedEndTagOpenState elif data in asciiLetters: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<" + data}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data}) self.temporaryBuffer = data self.state = self.scriptDataDoubleEscapeStartState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer = data self.state = self.scriptDataEscapedEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapeStartState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -761,87 +752,87 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) return True - + def scriptDataDoubleEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - - def scriptDataDoubleEscapedDashState(self): + + def scriptDataDoubleEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapedLessThanSignState(self): data = self.stream.char() if data == "/": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"/"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"}) self.temporaryBuffer = "" self.state = self.scriptDataDoubleEscapeEndState else: self.stream.unget(data) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapeEndState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -865,23 +856,23 @@ class HTMLTokenizer(object): elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data in (u"'", u'"', u"=", u"<"): + elif data in ("'", '"', "=", "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-name-but-got-eof"}) + "expected-attribute-name-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -892,34 +883,34 @@ class HTMLTokenizer(object): data = self.stream.char() leavingThisState = True emitToken = False - if data == u"=": + if data == "=": self.state = self.beforeAttributeValueState elif data in asciiLetters: self.currentToken["data"][-1][0] += data +\ - self.stream.charsUntil(asciiLetters, True) + self.stream.charsUntil(asciiLetters, True) leavingThisState = False - elif data == u">": + elif data == ">": # XXX If we emit here the attributes are converted to a dict # without being checked and when the code below runs we error # because data is a dict not a list emitToken = True elif data in spaceCharacters: self.state = self.afterAttributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][0] += u"\uFFFD" + self.currentToken["data"][-1][0] += "\uFFFD" leavingThisState = False - elif data in (u"'", u'"', u"<"): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data in ("'", '"', "<"): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"][-1][0] += data leavingThisState = False elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-attribute-name"}) self.state = self.dataState else: @@ -936,7 +927,7 @@ class HTMLTokenizer(object): for name, value in self.currentToken["data"][:-1]: if self.currentToken["data"][-1][0] == name: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "duplicate-attribute"}) + "duplicate-attribute"}) break # XXX Fix for above XXX if emitToken: @@ -947,28 +938,28 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"=": + elif data == "=": self.state = self.beforeAttributeValueState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState - elif data in (u"'", u'"', u"<"): + elif data in ("'", '"', "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-after-attribute-name"}) + "invalid-character-after-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-end-of-tag-but-got-eof"}) + "expected-end-of-tag-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -979,30 +970,30 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"\"": + elif data == "\"": self.state = self.attributeValueDoubleQuotedState - elif data == u"&": + elif data == "&": self.state = self.attributeValueUnQuotedState - self.stream.unget(data); - elif data == u"'": + self.stream.unget(data) + elif data == "'": self.state = self.attributeValueSingleQuotedState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-right-bracket"}) + "expected-attribute-value-but-got-right-bracket"}) self.emitCurrentToken() - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" self.state = self.attributeValueUnQuotedState - elif data in (u"=", u"<", u"`"): + elif data in ("=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "equals-in-unquoted-attribute-value"}) + "equals-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data self.state = self.attributeValueUnQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-eof"}) + "expected-attribute-value-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data @@ -1013,81 +1004,81 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u'"') - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute('"') + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-double-quote"}) + "eof-in-attribute-value-double-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("\"", u"&")) + self.stream.charsUntil(("\"", "&", "\u0000")) return True def attributeValueSingleQuotedState(self): data = self.stream.char() if data == "'": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u"'") - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute("'") + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-single-quote"}) + "eof-in-attribute-value-single-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("'", u"&")) + self.stream.charsUntil(("'", "&", "\u0000")) return True def attributeValueUnQuotedState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u"&": + elif data == "&": self.processEntityInAttribute(">") - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data in (u'"', u"'", u"=", u"<", u"`"): + elif data in ('"', "'", "=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-in-unquoted-attribute-value"}) + "unexpected-character-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-no-quotes"}) + "eof-in-attribute-value-no-quotes"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data + self.stream.charsUntil( - frozenset((u"&", u">", u'"', u"'", u"=", u"<", u"`")) | spaceCharacters) + frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters) return True def afterAttributeValueState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-attribute-value"}) + "unexpected-EOF-after-attribute-value"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-attribute-value"}) + "unexpected-character-after-attribute-value"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1098,14 +1089,14 @@ class HTMLTokenizer(object): self.currentToken["selfClosing"] = True self.emitCurrentToken() elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-solidus-in-tag"}) + "unexpected-EOF-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-soldius-in-tag"}) + "unexpected-character-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1114,10 +1105,10 @@ class HTMLTokenizer(object): # Make a new comment token and give it as value all the characters # until the first > or EOF (charsUntil checks for EOF automatically) # and emit it. - data = self.stream.charsUntil(u">") - data = data.replace(u"\u0000", u"\uFFFD") + data = self.stream.charsUntil(">") + data = data.replace("\u0000", "\uFFFD") self.tokenQueue.append( - {"type": tokenTypes["Comment"], "data": data}) + {"type": tokenTypes["Comment"], "data": data}) # Eat the character directly after the bogus comment which is either a # ">" or an EOF. @@ -1127,28 +1118,28 @@ class HTMLTokenizer(object): def markupDeclarationOpenState(self): charStack = [self.stream.char()] - if charStack[-1] == u"-": + if charStack[-1] == "-": charStack.append(self.stream.char()) - if charStack[-1] == u"-": - self.currentToken = {"type": tokenTypes["Comment"], "data": u""} + if charStack[-1] == "-": + self.currentToken = {"type": tokenTypes["Comment"], "data": ""} self.state = self.commentStartState return True - elif charStack[-1] in (u'd', u'D'): + elif charStack[-1] in ('d', 'D'): matched = True - for expected in ((u'o', u'O'), (u'c', u'C'), (u't', u'T'), - (u'y', u'Y'), (u'p', u'P'), (u'e', u'E')): + for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'), + ('y', 'Y'), ('p', 'P'), ('e', 'E')): charStack.append(self.stream.char()) if charStack[-1] not in expected: matched = False break if matched: self.currentToken = {"type": tokenTypes["Doctype"], - "name": u"", - "publicId": None, "systemId": None, + "name": "", + "publicId": None, "systemId": None, "correct": True} self.state = self.doctypeState return True - elif (charStack[-1] == "[" and + elif (charStack[-1] == "[" and self.parser is not None and self.parser.tree.openElements and self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace): @@ -1163,7 +1154,7 @@ class HTMLTokenizer(object): return True self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-dashes-or-doctype"}) + "expected-dashes-or-doctype"}) while charStack: self.stream.unget(charStack.pop()) @@ -1174,41 +1165,41 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "-": self.state = self.commentStartDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data self.state = self.commentState return True - + def commentStartDashState(self): data = self.stream.char() if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: @@ -1216,95 +1207,94 @@ class HTMLTokenizer(object): self.state = self.commentState return True - def commentState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data + \ - self.stream.charsUntil((u"-", u"\u0000")) + self.stream.charsUntil(("-", "\u0000")) return True def commentEndDashState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-dash"}) + "eof-in-comment-end-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"-" + data + self.currentToken["data"] += "-" + data self.state = self.commentState return True def commentEndState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--\uFFFD" + self.currentToken["data"] += "--\uFFFD" self.state = self.commentState elif data == "!": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-bang-after-double-dash-in-comment"}) + "unexpected-bang-after-double-dash-in-comment"}) self.state = self.commentEndBangState - elif data == u"-": + elif data == "-": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-dash-after-double-dash-in-comment"}) + "unexpected-dash-after-double-dash-in-comment"}) self.currentToken["data"] += data elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-double-dash"}) + "eof-in-comment-double-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-comment"}) - self.currentToken["data"] += u"--" + data + "unexpected-char-in-comment"}) + self.currentToken["data"] += "--" + data self.state = self.commentState return True def commentEndBangState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"-": + elif data == "-": self.currentToken["data"] += "--!" self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--!\uFFFD" + self.currentToken["data"] += "--!\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-bang-state"}) + "eof-in-comment-end-bang-state"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"--!" + data + self.currentToken["data"] += "--!" + data self.state = self.commentState return True @@ -1314,13 +1304,13 @@ class HTMLTokenizer(object): self.state = self.beforeDoctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "need-space-after-doctype"}) + "need-space-after-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeNameState return True @@ -1329,20 +1319,20 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-right-bracket"}) + "expected-doctype-name-but-got-right-bracket"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] = u"\uFFFD" + self.currentToken["name"] = "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1356,18 +1346,18 @@ class HTMLTokenizer(object): if data in spaceCharacters: self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.state = self.afterDoctypeNameState - elif data == u">": + elif data == ">": self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype-name"}) + "eof-in-doctype-name"}) self.currentToken["correct"] = False self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) @@ -1380,21 +1370,21 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.currentToken["correct"] = False self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - if data in (u"p", u"P"): + if data in ("p", "P"): matched = True - for expected in ((u"u", u"U"), (u"b", u"B"), (u"l", u"L"), - (u"i", u"I"), (u"c", u"C")): + for expected in (("u", "U"), ("b", "B"), ("l", "L"), + ("i", "I"), ("c", "C")): data = self.stream.char() if data not in expected: matched = False @@ -1402,10 +1392,10 @@ class HTMLTokenizer(object): if matched: self.state = self.afterDoctypePublicKeywordState return True - elif data in (u"s", u"S"): + elif data in ("s", "S"): matched = True - for expected in ((u"y", u"Y"), (u"s", u"S"), (u"t", u"T"), - (u"e", u"E"), (u"m", u"M")): + for expected in (("y", "Y"), ("s", "S"), ("t", "T"), + ("e", "E"), ("m", "M")): data = self.stream.char() if data not in expected: matched = False @@ -1420,25 +1410,25 @@ class HTMLTokenizer(object): # and needs to be ungetted self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-space-or-right-bracket-in-doctype", "datavars": - {"data": data}}) + "expected-space-or-right-bracket-in-doctype", "datavars": + {"data": data}}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypePublicKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypePublicIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypePublicIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1452,26 +1442,26 @@ class HTMLTokenizer(object): if data in spaceCharacters: pass elif data == "\"": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierDoubleQuotedState elif data == "'": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1480,19 +1470,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1504,19 +1494,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1533,27 +1523,27 @@ class HTMLTokenizer(object): self.state = self.dataState elif data == '"': self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def betweenDoctypePublicAndSystemIdentifiersState(self): data = self.stream.char() if data in spaceCharacters: @@ -1562,36 +1552,36 @@ class HTMLTokenizer(object): self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data == '"': - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypeSystemKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypeSystemIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1599,32 +1589,32 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState return True - + def beforeDoctypeSystemIdentifierState(self): data = self.stream.char() if data in spaceCharacters: pass elif data == "\"": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1633,19 +1623,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1657,19 +1647,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1686,19 +1676,19 @@ class HTMLTokenizer(object): self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.state = self.bogusDoctypeState return True def bogusDoctypeState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: @@ -1713,32 +1703,29 @@ class HTMLTokenizer(object): def cdataSectionState(self): data = [] while True: - data.append(self.stream.charsUntil(u"]")) - charStack = [] - - for expected in ["]", "]", ">"]: - charStack.append(self.stream.char()) - matched = True - if charStack[-1] == EOF: - data.extend(charStack[:-1]) - break - elif charStack[-1] != expected: - matched = False - data.extend(charStack) - break - - if matched: + data.append(self.stream.charsUntil("]")) + data.append(self.stream.charsUntil(">")) + char = self.stream.char() + if char == EOF: break + else: + assert char == ">" + if data[-1][-2:] == "]]": + data[-1] = data[-1][:-2] + break + else: + data.append(char) + data = "".join(data) - #Deal with null here rather than in the parser - nullCount = data.count(u"\u0000") + # Deal with null here rather than in the parser + nullCount = data.count("\u0000") if nullCount > 0: - for i in xrange(nullCount): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + for i in range(nullCount): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - data = data.replace(u"\u0000", u"\uFFFD") + data = data.replace("\u0000", "\uFFFD") if data: - self.tokenQueue.append({"type": tokenTypes["Characters"], + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.dataState return True diff --git a/libs/html5lib/treeadapters/__init__.py b/libs/html5lib/treeadapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/html5lib/treeadapters/sax.py b/libs/html5lib/treeadapters/sax.py new file mode 100644 index 00000000..ad47df95 --- /dev/null +++ b/libs/html5lib/treeadapters/sax.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from xml.sax.xmlreader import AttributesNSImpl + +from ..constants import adjustForeignAttributes, unadjustForeignAttributes + +prefix_mapping = {} +for prefix, localName, namespace in adjustForeignAttributes.values(): + if prefix is not None: + prefix_mapping[prefix] = namespace + + +def to_sax(walker, handler): + """Call SAX-like content handler based on treewalker walker""" + handler.startDocument() + for prefix, namespace in prefix_mapping.items(): + handler.startPrefixMapping(prefix, namespace) + + for token in walker: + type = token["type"] + if type == "Doctype": + continue + elif type in ("StartTag", "EmptyTag"): + attrs = AttributesNSImpl(token["data"], + unadjustForeignAttributes) + handler.startElementNS((token["namespace"], token["name"]), + token["name"], + attrs) + if type == "EmptyTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type == "EndTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type in ("Characters", "SpaceCharacters"): + handler.characters(token["data"]) + elif type == "Comment": + pass + else: + assert False, "Unknown token type" + + for prefix, namespace in prefix_mapping.items(): + handler.endPrefixMapping(prefix) + handler.endDocument() diff --git a/libs/html5lib/treebuilders/__init__.py b/libs/html5lib/treebuilders/__init__.py index 14f66d40..6a6b2a4c 100755 --- a/libs/html5lib/treebuilders/__init__.py +++ b/libs/html5lib/treebuilders/__init__.py @@ -7,7 +7,7 @@ implement several things: 1) A set of classes for various types of elements: Document, Doctype, Comment, Element. These must implement the interface of _base.treebuilders.Node (although comment nodes have a different -signature for their constructor, see treebuilders.simpletree.Comment) +signature for their constructor, see treebuilders.etree.Comment) Textual content may also be implemented as another node type, or not, as your tree implementation requires. @@ -24,73 +24,53 @@ getDocument - Returns the root node of the complete document tree testSerializer method on your treebuilder which accepts a node and returns a string containing Node and its children serialized according to the format used in the unittests - -The supplied simpletree module provides a python-only implementation -of a full treebuilder and is a useful reference for the semantics of -the various methods. """ +from __future__ import absolute_import, division, unicode_literals + +from ..utils import default_etree + treeBuilderCache = {} -import sys def getTreeBuilder(treeType, implementation=None, **kwargs): """Get a TreeBuilder class for various types of tree with built-in support - + treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" - - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. - "dom" - A generic builder for DOM implementations, defaulting to - a xml.dom.minidom based implementation for the sake of - backwards compatibility (as releases up until 0.10 had a - builder called "dom" that was a minidom implemenation). - "etree" - A generic builder for tree implementations exposing an - elementtree-like interface (known to work with - ElementTree, cElementTree and lxml.etree). - "beautifulsoup" - Beautiful soup (if installed) - + values are: + + "dom" - A generic builder for DOM implementations, defaulting to + a xml.dom.minidom based implementation. + "etree" - A generic builder for tree implementations exposing an + ElementTree-like interface, defaulting to + xml.etree.cElementTree if available and + xml.etree.ElementTree if not. + "lxml" - A etree-based builder for lxml.etree, handling + limitations of lxml's implementation. + implementation - (Currently applies to the "etree" and "dom" tree types). A module implementing the tree type e.g. - xml.etree.ElementTree or lxml.etree.""" - + xml.etree.ElementTree or xml.etree.cElementTree.""" + treeType = treeType.lower() if treeType not in treeBuilderCache: if treeType == "dom": - import dom - # XXX: Keep backwards compatibility by using minidom if no implementation is given - if implementation == None: + from . import dom + # Come up with a sane default (pref. from the stdlib) + if implementation is None: from xml.dom import minidom implementation = minidom - # XXX: NEVER cache here, caching is done in the dom submodule + # NEVER cache here, caching is done in the dom submodule return dom.getDomModule(implementation, **kwargs).TreeBuilder - elif treeType == "simpletree": - import simpletree - treeBuilderCache[treeType] = simpletree.TreeBuilder - elif treeType == "beautifulsoup": - import soup - treeBuilderCache[treeType] = soup.TreeBuilder elif treeType == "lxml": - import etree_lxml + from . import etree_lxml treeBuilderCache[treeType] = etree_lxml.TreeBuilder elif treeType == "etree": - # Come up with a sane default - if implementation == None: - try: - import xml.etree.cElementTree as ET - except ImportError: - try: - import xml.etree.ElementTree as ET - except ImportError: - try: - import cElementTree as ET - except ImportError: - import elementtree.ElementTree as ET - implementation = ET - import etree + from . import etree + if implementation is None: + implementation = default_etree # NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeBuilder else: - raise ValueError("""Unrecognised treebuilder "%s" """%treeType) + raise ValueError("""Unrecognised treebuilder "%s" """ % treeType) return treeBuilderCache.get(treeType) diff --git a/libs/html5lib/treebuilders/_base.py b/libs/html5lib/treebuilders/_base.py index f3782d28..8b97cc11 100755 --- a/libs/html5lib/treebuilders/_base.py +++ b/libs/html5lib/treebuilders/_base.py @@ -1,25 +1,34 @@ -from html5lib.constants import scopingElements, tableInsertModeElements, namespaces -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from ..constants import scopingElements, tableInsertModeElements, namespaces # The scope markers are inserted when entering object elements, # marquees, table cells, and table captions, and are used to prevent formatting # from "leaking" into tables, object elements, and marquees. Marker = None +listElementsMap = { + None: (frozenset(scopingElements), False), + "button": (frozenset(scopingElements | set([(namespaces["html"], "button")])), False), + "list": (frozenset(scopingElements | set([(namespaces["html"], "ol"), + (namespaces["html"], "ul")])), False), + "table": (frozenset([(namespaces["html"], "html"), + (namespaces["html"], "table")]), False), + "select": (frozenset([(namespaces["html"], "optgroup"), + (namespaces["html"], "option")]), True) +} + + class Node(object): def __init__(self, name): """Node representing an item in the tree. name - The tag name associated with the node parent - The parent of the current node (or None for the document node) - value - The value of the current node (applies to text nodes and + value - The value of the current node (applies to text nodes and comments attributes - a dict holding name, value pairs for attributes of the node - childNodes - a list of child nodes of the current node. This must + childNodes - a list of child nodes of the current node. This must include all elements but not necessarily other node types _flags - A list of miscellaneous flags that can be set on the node """ @@ -30,14 +39,14 @@ class Node(object): self.childNodes = [] self._flags = [] - def __unicode__(self): - attributesStr = " ".join(["%s=\"%s\""%(name, value) - for name, value in - self.attributes.iteritems()]) + def __str__(self): + attributesStr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in + self.attributes.items()]) if attributesStr: - return "<%s %s>"%(self.name,attributesStr) + return "<%s %s>" % (self.name, attributesStr) else: - return "<%s>"%(self.name) + return "<%s>" % (self.name) def __repr__(self): return "<%s>" % (self.name) @@ -48,14 +57,14 @@ class Node(object): raise NotImplementedError def insertText(self, data, insertBefore=None): - """Insert data as text in the current node, positioned before the + """Insert data as text in the current node, positioned before the start of node insertBefore or to the end of the node's text. """ raise NotImplementedError def insertBefore(self, node, refNode): - """Insert node as a child of the current node, before refNode in the - list of child nodes. Raises ValueError if refNode is not a child of + """Insert node as a child of the current node, before refNode in the + list of child nodes. Raises ValueError if refNode is not a child of the current node""" raise NotImplementedError @@ -65,11 +74,11 @@ class Node(object): raise NotImplementedError def reparentChildren(self, newParent): - """Move all the children of the current node to newParent. - This is needed so that trees that don't store text as nodes move the + """Move all the children of the current node to newParent. + This is needed so that trees that don't store text as nodes move the text in the correct way """ - #XXX - should this method be made more general? + # XXX - should this method be made more general? for child in self.childNodes: newParent.appendChild(child) self.childNodes = [] @@ -80,12 +89,12 @@ class Node(object): """ raise NotImplementedError - def hasContent(self): """Return true if the node has children or text, false otherwise """ raise NotImplementedError + class ActiveFormattingElements(list): def append(self, node): equalCount = 0 @@ -103,12 +112,13 @@ class ActiveFormattingElements(list): def nodesEqual(self, node1, node2): if not node1.nameTuple == node2.nameTuple: return False - + if not node1.attributes == node2.attributes: return False - + return True + class TreeBuilder(object): """Base treebuilder implementation documentClass - the class to use for the bottommost node of a document @@ -117,19 +127,19 @@ class TreeBuilder(object): doctypeClass - the class to use for doctypes """ - #Document class + # Document class documentClass = None - #The class to use for creating a node + # The class to use for creating a node elementClass = None - #The class to use for creating comments + # The class to use for creating comments commentClass = None - #The class to use for creating doctypes + # The class to use for creating doctypes doctypeClass = None - - #Fragment class + + # Fragment class fragmentClass = None def __init__(self, namespaceHTMLElements): @@ -138,12 +148,12 @@ class TreeBuilder(object): else: self.defaultNamespace = None self.reset() - + def reset(self): self.openElements = [] self.activeFormattingElements = ActiveFormattingElements() - #XXX - rename these to headElement, formElement + # XXX - rename these to headElement, formElement self.headPointer = None self.formPointer = None @@ -153,30 +163,20 @@ class TreeBuilder(object): def elementInScope(self, target, variant=None): - #If we pass a node in we match that. if we pass a string - #match any node with that name + # If we pass a node in we match that. if we pass a string + # match any node with that name exactNode = hasattr(target, "nameTuple") - listElementsMap = { - None:(scopingElements, False), - "button":(scopingElements | set([(namespaces["html"], "button")]), False), - "list":(scopingElements | set([(namespaces["html"], "ol"), - (namespaces["html"], "ul")]), False), - "table":(set([(namespaces["html"], "html"), - (namespaces["html"], "table")]), False), - "select":(set([(namespaces["html"], "optgroup"), - (namespaces["html"], "option")]), True) - } listElements, invert = listElementsMap[variant] for node in reversed(self.openElements): if (node.name == target and not exactNode or - node == target and exactNode): + node == target and exactNode): return True - elif (invert ^ (node.nameTuple in listElements)): + elif (invert ^ (node.nameTuple in listElements)): return False - assert False # We should never reach this point + assert False # We should never reach this point def reconstructActiveFormattingElements(self): # Within this algorithm the order of steps described in the @@ -196,7 +196,7 @@ class TreeBuilder(object): # Step 6 while entry != Marker and entry not in self.openElements: if i == 0: - #This will be reset to 0 below + # This will be reset to 0 below i = -1 break i -= 1 @@ -209,13 +209,13 @@ class TreeBuilder(object): # Step 8 entry = self.activeFormattingElements[i] - clone = entry.cloneNode() #Mainly to get a new copy of the attributes + clone = entry.cloneNode() # Mainly to get a new copy of the attributes # Step 9 - element = self.insertElement({"type":"StartTag", - "name":clone.name, - "namespace":clone.namespace, - "data":clone.attributes}) + element = self.insertElement({"type": "StartTag", + "name": clone.name, + "namespace": clone.namespace, + "data": clone.attributes}) # Step 10 self.activeFormattingElements[i] = element @@ -260,7 +260,7 @@ class TreeBuilder(object): if parent is None: parent = self.openElements[-1] parent.appendChild(self.commentClass(token["data"])) - + def createElement(self, token): """Create an element but don't insert it anywhere""" name = token["name"] @@ -282,10 +282,10 @@ class TreeBuilder(object): self.insertElement = self.insertElementNormal insertFromTable = property(_getInsertFromTable, _setInsertFromTable) - + def insertElementNormal(self, token): name = token["name"] - assert type(name) == unicode, "Element %s not unicode"%name + assert isinstance(name, text_type), "Element %s not unicode" % name namespace = token.get("namespace", self.defaultNamespace) element = self.elementClass(name, namespace) element.attributes = token["data"] @@ -294,13 +294,13 @@ class TreeBuilder(object): return element def insertElementTable(self, token): - """Create an element and insert it into the tree""" + """Create an element and insert it into the tree""" element = self.createElement(token) if self.openElements[-1].name not in tableInsertModeElements: return self.insertElementNormal(token) else: - #We should be in the InTable mode. This means we want to do - #special magic element rearranging + # We should be in the InTable mode. This means we want to do + # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() if insertBefore is None: parent.appendChild(element) @@ -315,7 +315,7 @@ class TreeBuilder(object): parent = self.openElements[-1] if (not self.insertFromTable or (self.insertFromTable and - self.openElements[-1].name + self.openElements[-1].name not in tableInsertModeElements)): parent.insertText(data) else: @@ -323,14 +323,14 @@ class TreeBuilder(object): # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() parent.insertText(data, insertBefore) - + def getTableMisnestedNodePosition(self): """Get the foster parent element, and sibling to insert before (or None) when inserting a misnested table node""" # The foster parent element is the one which comes before the most # recently opened table element # XXX - this is really inelegant - lastTable=None + lastTable = None fosterParent = None insertBefore = None for elm in self.openElements[::-1]: @@ -354,7 +354,7 @@ class TreeBuilder(object): name = self.openElements[-1].name # XXX td, th and tr are not actually needed if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) - and name != exclude): + and name != exclude): self.openElements.pop() # XXX This is not entirely what the specification says. We should # investigate it more closely. @@ -363,10 +363,10 @@ class TreeBuilder(object): def getDocument(self): "Return the final tree" return self.document - + def getFragment(self): "Return the final fragment" - #assert self.innerHTML + # assert self.innerHTML fragment = self.fragmentClass() self.openElements[0].reparentChildren(fragment) return fragment diff --git a/libs/html5lib/treebuilders/dom.py b/libs/html5lib/treebuilders/dom.py index 9578da2b..61e5ed79 100644 --- a/libs/html5lib/treebuilders/dom.py +++ b/libs/html5lib/treebuilders/dom.py @@ -1,45 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals -from xml.dom import minidom, Node, XML_NAMESPACE, XMLNS_NAMESPACE -try: - from types import ModuleType -except: - from new import module as ModuleType -import re + +from xml.dom import minidom, Node import weakref -import _base -from html5lib import constants, ihatexml -from html5lib.constants import namespaces +from . import _base +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory -moduleCache = {} - -def getDomModule(DomImplementation): - name = "_" + DomImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType(name) - objs = getDomBuilder(DomImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getDomBuilder(DomImplementation): Dom = DomImplementation + class AttrList(object): def __init__(self, element): self.element = element + def __iter__(self): - return self.element.attributes.items().__iter__() + return list(self.element.attributes.items()).__iter__() + def __setitem__(self, name, value): self.element.setAttribute(name, value) + def __len__(self): - return len(self.element.attributes.items()) + return len(list(self.element.attributes.items())) + def items(self): return [(item[0], item[1]) for item in - self.element.attributes.items()] + list(self.element.attributes.items())] + def keys(self): - return self.element.attributes.keys() + return list(self.element.attributes.keys()) + def __getitem__(self, name): return self.element.getAttribute(name) @@ -48,68 +41,68 @@ def getDomBuilder(DomImplementation): raise NotImplementedError else: return self.element.hasAttribute(name) - + class NodeBuilder(_base.Node): def __init__(self, element): _base.Node.__init__(self, element.nodeName) self.element = element - namespace = property(lambda self:hasattr(self.element, "namespaceURI") + namespace = property(lambda self: hasattr(self.element, "namespaceURI") and self.element.namespaceURI or None) def appendChild(self, node): node.parent = self self.element.appendChild(node.element) - + def insertText(self, data, insertBefore=None): text = self.element.ownerDocument.createTextNode(data) if insertBefore: self.element.insertBefore(text, insertBefore.element) else: self.element.appendChild(text) - + def insertBefore(self, node, refNode): self.element.insertBefore(node.element, refNode.element) node.parent = self - + def removeChild(self, node): if node.element.parentNode == self.element: self.element.removeChild(node.element) node.parent = None - + def reparentChildren(self, newParent): while self.element.hasChildNodes(): child = self.element.firstChild self.element.removeChild(child) newParent.element.appendChild(child) self.childNodes = [] - + def getAttributes(self): return AttrList(self.element) - + def setAttributes(self, attributes): if attributes: - for name, value in attributes.items(): + for name, value in list(attributes.items()): if isinstance(name, tuple): if name[0] is not None: qualifiedName = (name[0] + ":" + name[1]) else: qualifiedName = name[1] - self.element.setAttributeNS(name[2], qualifiedName, + self.element.setAttributeNS(name[2], qualifiedName, value) else: self.element.setAttribute( name, value) attributes = property(getAttributes, setAttributes) - + def cloneNode(self): return NodeBuilder(self.element.cloneNode(False)) - + def hasContent(self): return self.element.hasChildNodes() def getNameTuple(self): - if self.namespace == None: + if self.namespace is None: return namespaces["html"], self.name else: return self.namespace, self.name @@ -118,9 +111,9 @@ def getDomBuilder(DomImplementation): class TreeBuilder(_base.TreeBuilder): def documentClass(self): - self.dom = Dom.getDOMImplementation().createDocument(None,None,None) + self.dom = Dom.getDOMImplementation().createDocument(None, None, None) return weakref.proxy(self) - + def insertDoctype(self, token): name = token["name"] publicId = token["publicId"] @@ -131,7 +124,7 @@ def getDomBuilder(DomImplementation): self.document.appendChild(NodeBuilder(doctype)) if Dom == minidom: doctype.ownerDocument = self.dom - + def elementClass(self, name, namespace=None): if namespace is None and self.defaultNamespace is None: node = self.dom.createElement(name) @@ -139,70 +132,72 @@ def getDomBuilder(DomImplementation): node = self.dom.createElementNS(namespace, name) return NodeBuilder(node) - + def commentClass(self, data): return NodeBuilder(self.dom.createComment(data)) - + def fragmentClass(self): return NodeBuilder(self.dom.createDocumentFragment()) - + def appendChild(self, node): self.dom.appendChild(node.element) - + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): return self.dom - + def getFragment(self): return _base.TreeBuilder.getFragment(self).element - + def insertText(self, data, parent=None): - data=data - if parent <> self: + data = data + if parent != self: _base.TreeBuilder.insertText(self, data, parent) else: # HACK: allow text nodes as children of the document node if hasattr(self.dom, '_child_node_types'): if not Node.TEXT_NODE in self.dom._child_node_types: - self.dom._child_node_types=list(self.dom._child_node_types) + self.dom._child_node_types = list(self.dom._child_node_types) self.dom._child_node_types.append(Node.TEXT_NODE) self.dom.appendChild(self.dom.createTextNode(data)) - + + implementation = DomImplementation name = None - + def testSerializer(element): element.normalize() rv = [] + def serializeElement(element, indent=0): if element.nodeType == Node.DOCUMENT_TYPE_NODE: if element.name: if element.publicId or element.systemId: publicId = element.publicId or "" systemId = element.systemId or "" - rv.append( """|%s"""%( - ' '*indent, element.name, publicId, systemId)) + rv.append("""|%s""" % + (' ' * indent, element.name, publicId, systemId)) else: - rv.append("|%s"%(' '*indent, element.name)) + rv.append("|%s" % (' ' * indent, element.name)) else: - rv.append("|%s"%(' '*indent,)) + rv.append("|%s" % (' ' * indent,)) elif element.nodeType == Node.DOCUMENT_NODE: rv.append("#document") elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE: rv.append("#document-fragment") elif element.nodeType == Node.COMMENT_NODE: - rv.append("|%s"%(' '*indent, element.nodeValue)) + rv.append("|%s" % (' ' * indent, element.nodeValue)) elif element.nodeType == Node.TEXT_NODE: - rv.append("|%s\"%s\"" %(' '*indent, element.nodeValue)) + rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue)) else: if (hasattr(element, "namespaceURI") and - element.namespaceURI != None): - name = "%s %s"%(constants.prefixes[element.namespaceURI], - element.nodeName) + element.namespaceURI is not None): + name = "%s %s" % (constants.prefixes[element.namespaceURI], + element.nodeName) else: name = element.nodeName - rv.append("|%s<%s>"%(' '*indent, name)) + rv.append("|%s<%s>" % (' ' * indent, name)) if element.hasAttributes(): attributes = [] for i in range(len(element.attributes)): @@ -211,81 +206,22 @@ def getDomBuilder(DomImplementation): value = attr.value ns = attr.namespaceURI if ns: - name = "%s %s"%(constants.prefixes[ns], attr.localName) + name = "%s %s" % (constants.prefixes[ns], attr.localName) else: name = attr.nodeName attributes.append((name, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) indent += 2 for child in element.childNodes: serializeElement(child, indent) serializeElement(element, 0) - + return "\n".join(rv) - - def dom2sax(node, handler, nsmap={'xml':XML_NAMESPACE}): - if node.nodeType == Node.ELEMENT_NODE: - if not nsmap: - handler.startElement(node.nodeName, node.attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElement(node.nodeName) - else: - attributes = dict(node.attributes.itemsNS()) - - # gather namespace declarations - prefixes = [] - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if (attr.namespaceURI == XMLNS_NAMESPACE or - (attr.namespaceURI == None and attr.nodeName.startswith('xmlns'))): - prefix = (attr.nodeName != 'xmlns' and attr.nodeName or None) - handler.startPrefixMapping(prefix, attr.nodeValue) - prefixes.append(prefix) - nsmap = nsmap.copy() - nsmap[prefix] = attr.nodeValue - del attributes[(attr.namespaceURI, attr.nodeName)] - - # apply namespace declarations - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if attr.namespaceURI == None and ':' in attr.nodeName: - prefix = attr.nodeName.split(':')[0] - if nsmap.has_key(prefix): - del attributes[(attr.namespaceURI, attr.nodeName)] - attributes[(nsmap[prefix],attr.nodeName)]=attr.nodeValue - - # SAX events - ns = node.namespaceURI or nsmap.get(None,None) - handler.startElementNS((ns,node.nodeName), node.nodeName, attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElementNS((ns, node.nodeName), node.nodeName) - for prefix in prefixes: handler.endPrefixMapping(prefix) - - elif node.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: - handler.characters(node.nodeValue) - - elif node.nodeType == Node.DOCUMENT_NODE: - handler.startDocument() - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endDocument() - - elif node.nodeType == Node.DOCUMENT_FRAGMENT_NODE: - for child in node.childNodes: dom2sax(child, handler, nsmap) - - else: - # ATTRIBUTE_NODE - # ENTITY_NODE - # PROCESSING_INSTRUCTION_NODE - # COMMENT_NODE - # DOCUMENT_TYPE_NODE - # NOTATION_NODE - pass - + return locals() -# Keep backwards compatibility with things that directly load -# classes/functions from this module -for key, value in getDomModule(minidom).__dict__.items(): - globals()[key] = value + +# The actual means to get a module! +getDomModule = moduleFactoryFactory(getDomBuilder) diff --git a/libs/html5lib/treebuilders/etree.py b/libs/html5lib/treebuilders/etree.py index 95be4755..2c8ed19f 100755 --- a/libs/html5lib/treebuilders/etree.py +++ b/libs/html5lib/treebuilders/etree.py @@ -1,32 +1,21 @@ -try: - from types import ModuleType -except: - from new import module as ModuleType -import re -import types +from __future__ import absolute_import, division, unicode_literals +from six import text_type -import _base -from html5lib import ihatexml -from html5lib import constants -from html5lib.constants import namespaces +import re + +from . import _base +from .. import ihatexml +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation, fullTree=False): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation, fullTree) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation, fullTree=False): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag + class Element(_base.Node): def __init__(self, name, namespace=None): self._name = name @@ -45,16 +34,16 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) return etree_tag - + def _setName(self, name): self._name = name self._element.tag = self._getETreeTag(self._name, self._namespace) - + def _getName(self): return self._name - + name = property(_getName, _setName) def _setNamespace(self, namespace): @@ -65,81 +54,82 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): return self._namespace namespace = property(_getNamespace, _setNamespace) - + def _getAttributes(self): return self._element.attrib - + def _setAttributes(self, attributes): - #Delete existing attributes first - #XXX - there may be a better way to do this... - for key in self._element.attrib.keys(): + # Delete existing attributes first + # XXX - there may be a better way to do this... + for key in list(self._element.attrib.keys()): del self._element.attrib[key] - for key, value in attributes.iteritems(): + for key, value in attributes.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], key[1]) + name = "{%s}%s" % (key[2], key[1]) else: name = key self._element.set(name, value) - + attributes = property(_getAttributes, _setAttributes) - + def _getChildNodes(self): - return self._childNodes + return self._childNodes + def _setChildNodes(self, value): del self._element[:] self._childNodes = [] for element in value: self.insertChild(element) - + childNodes = property(_getChildNodes, _setChildNodes) - + def hasContent(self): """Return true if the node has children or text""" return bool(self._element.text or len(self._element)) - + def appendChild(self, node): self._childNodes.append(node) self._element.append(node._element) node.parent = self - + def insertBefore(self, node, refNode): index = list(self._element).index(refNode._element) self._element.insert(index, node._element) node.parent = self - + def removeChild(self, node): self._element.remove(node._element) - node.parent=None - + node.parent = None + def insertText(self, data, insertBefore=None): if not(len(self._element)): if not self._element.text: self._element.text = "" self._element.text += data elif insertBefore is None: - #Insert the text as the tail of the last child element + # Insert the text as the tail of the last child element if not self._element[-1].tail: self._element[-1].tail = "" self._element[-1].tail += data else: - #Insert the text before the specified node + # Insert the text before the specified node children = list(self._element) index = children.index(insertBefore._element) if index > 0: - if not self._element[index-1].tail: - self._element[index-1].tail = "" - self._element[index-1].tail += data + if not self._element[index - 1].tail: + self._element[index - 1].tail = "" + self._element[index - 1].tail += data else: if not self._element.text: self._element.text = "" self._element.text += data - + def cloneNode(self): element = type(self)(self.name, self.namespace) - for name, value in self.attributes.iteritems(): + for name, value in self.attributes.items(): element.attributes[name] = value return element - + def reparentChildren(self, newParent): if newParent.childNodes: newParent.childNodes[-1]._element.tail += self._element.text @@ -150,60 +140,60 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): newParent._element.text += self._element.text self._element.text = "" _base.Node.reparentChildren(self, newParent) - + class Comment(Element): def __init__(self, data): - #Use the superclass constructor to set all properties on the - #wrapper element + # Use the superclass constructor to set all properties on the + # wrapper element self._element = ElementTree.Comment(data) self.parent = None self._childNodes = [] self._flags = [] - + def _getData(self): return self._element.text - + def _setData(self, value): self._element.text = value - + data = property(_getData, _setData) - + class DocumentType(Element): def __init__(self, name, publicId, systemId): - Element.__init__(self, "") + Element.__init__(self, "") self._element.text = name self.publicId = publicId self.systemId = systemId def _getPublicId(self): - return self._element.get(u"publicId", "") + return self._element.get("publicId", "") def _setPublicId(self, value): if value is not None: - self._element.set(u"publicId", value) + self._element.set("publicId", value) publicId = property(_getPublicId, _setPublicId) - + def _getSystemId(self): - return self._element.get(u"systemId", "") + return self._element.get("systemId", "") def _setSystemId(self, value): if value is not None: - self._element.set(u"systemId", value) + self._element.set("systemId", value) systemId = property(_getSystemId, _setSystemId) - + class Document(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_ROOT") + class DocumentFragment(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_FRAGMENT") + def testSerializer(element): rv = [] - finalText = None + def serializeElement(element, indent=0): if not(hasattr(element, "tag")): element = element.getroot() @@ -211,20 +201,23 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": + rv.append("""""" % + (element.text, publicId, systemId)) + else: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": rv.append("#document") - if element.text: - rv.append("|%s\"%s\""%(' '*(indent+2), element.text)) - if element.tail: - finalText = element.tail - elif element.tag == ElementTree.Comment: - rv.append("|%s"%(' '*indent, element.text)) + if element.text is not None: + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + elif element.tag == ElementTreeCommentType: + rv.append("|%s" % (' ' * indent, element.text)) else: - assert type(element.tag) in types.StringTypes, "Expected unicode, got %s"%type(element.tag) + assert isinstance(element.tag, text_type), \ + "Expected unicode, got %s, %s" % (type(element.tag), element.tag) nsmatch = tag_regexp.match(element.tag) if nsmatch is None: @@ -232,113 +225,113 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): else: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - name = "%s %s"%(prefix, name) - rv.append("|%s<%s>"%(' '*indent, name)) + name = "%s %s" % (prefix, name) + rv.append("|%s<%s>" % (' ' * indent, name)) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: attr_string = name attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 for child in element: serializeElement(child, indent) if element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) - - if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) - + return "\n".join(rv) - + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] - finalText = None filter = ihatexml.InfosetFilter() + def serializeElement(element): - if type(element) == type(ElementTree.ElementTree): + if isinstance(element, ElementTree.ElementTree): element = element.getroot() - + if element.tag == "": if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": - if element.text: - rv.append(element.text) - if element.tail: - finalText = element.tail - - for child in element: - serializeElement(child) - - elif type(element.tag) == type(ElementTree.Comment): - rv.append(""%(element.text,)) - else: - #This is assumed to be an ordinary element - if not element.attrib: - rv.append("<%s>"%(filter.fromXmlName(element.tag),)) + rv.append("""""" % + (element.text, publicId, systemId)) else: - attr = " ".join(["%s=\"%s\""%( - filter.fromXmlName(name), value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) - if element.text: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": + if element.text is not None: rv.append(element.text) - + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + for child in element: serializeElement(child) - - rv.append("%s>"%(element.tag,)) - + + elif element.tag == ElementTreeCommentType: + rv.append("" % (element.text,)) + else: + # This is assumed to be an ordinary element + if not element.attrib: + rv.append("<%s>" % (filter.fromXmlName(element.tag),)) + else: + attr = " ".join(["%s=\"%s\"" % ( + filter.fromXmlName(name), value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) + if element.text: + rv.append(element.text) + + for child in element: + serializeElement(child) + + rv.append("%s>" % (element.tag,)) + if element.tail: rv.append(element.tail) - + serializeElement(element) - - if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) - + return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = Element commentClass = Comment fragmentClass = DocumentFragment - + implementation = ElementTreeImplementation + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): if fullTree: return self.document._element else: if self.defaultNamespace is not None: return self.document._element.find( - "{%s}html"%self.defaultNamespace) + "{%s}html" % self.defaultNamespace) else: return self.document._element.find("html") - + def getFragment(self): return _base.TreeBuilder.getFragment(self)._element - + return locals() + + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treebuilders/etree_lxml.py b/libs/html5lib/treebuilders/etree_lxml.py index eee1e3b2..35d08efa 100644 --- a/libs/html5lib/treebuilders/etree_lxml.py +++ b/libs/html5lib/treebuilders/etree_lxml.py @@ -1,20 +1,3 @@ -import warnings -import re - -import _base -from html5lib.constants import DataLossWarning -import html5lib.constants as constants -import etree as etree_builders -from html5lib import ihatexml - -try: - import lxml.etree as etree -except ImportError: - pass - -fullTree = True -tag_regexp = re.compile("{([^}]*)}(.*)") - """Module for supporting the lxml.etree library. The idea here is to use as much of the native library as possible, without using fragile hacks like custom element names that break between releases. The downside of this is that we cannot represent @@ -26,12 +9,34 @@ Docypes with no name When any of these things occur, we emit a DataLossWarning """ +from __future__ import absolute_import, division, unicode_literals + +import warnings +import re +import sys + +from . import _base +from ..constants import DataLossWarning +from .. import constants +from . import etree as etree_builders +from .. import ihatexml + +import lxml.etree as etree + + +fullTree = True +tag_regexp = re.compile("{([^}]*)}(.*)") + +comment_type = etree.Comment("asd").tag + + class DocumentType(object): def __init__(self, name, publicId, systemId): - self.name = name + self.name = name self.publicId = publicId self.systemId = systemId + class Document(object): def __init__(self): self._elementTree = None @@ -42,118 +47,126 @@ class Document(object): def _getChildNodes(self): return self._childNodes - + childNodes = property(_getChildNodes) + def testSerializer(element): rv = [] finalText = None - filter = ihatexml.InfosetFilter() + infosetFilter = ihatexml.InfosetFilter() + def serializeElement(element, indent=0): if not hasattr(element, "tag"): - if hasattr(element, "getroot"): - #Full tree case + if hasattr(element, "getroot"): + # Full tree case rv.append("#document") if element.docinfo.internalDTD: - if not (element.docinfo.public_id or + if not (element.docinfo.public_id or element.docinfo.system_url): - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name else: - dtd_str = """"""%( - element.docinfo.root_name, + dtd_str = """""" % ( + element.docinfo.root_name, element.docinfo.public_id, element.docinfo.system_url) - rv.append("|%s%s"%(' '*(indent+2), dtd_str)) + rv.append("|%s%s" % (' ' * (indent + 2), dtd_str)) next_element = element.getroot() while next_element.getprevious() is not None: next_element = next_element.getprevious() while next_element is not None: - serializeElement(next_element, indent+2) + serializeElement(next_element, indent + 2) next_element = next_element.getnext() - elif isinstance(element, basestring): - #Text in a fragment - rv.append("|%s\"%s\""%(' '*indent, element)) + elif isinstance(element, str) or isinstance(element, bytes): + # Text in a fragment + assert isinstance(element, str) or sys.version_info.major == 2 + rv.append("|%s\"%s\"" % (' ' * indent, element)) else: - #Fragment case + # Fragment case rv.append("#document-fragment") for next_element in element: - serializeElement(next_element, indent+2) - elif type(element.tag) == type(etree.Comment): - rv.append("|%s"%(' '*indent, element.text)) + serializeElement(next_element, indent + 2) + elif element.tag == comment_type: + rv.append("|%s" % (' ' * indent, element.text)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * indent, element.tail)) else: + assert isinstance(element, etree._Element) nsmatch = etree_builders.tag_regexp.match(element.tag) if nsmatch is not None: ns = nsmatch.group(1) tag = nsmatch.group(2) prefix = constants.prefixes[ns] - rv.append("|%s<%s %s>"%(' '*indent, prefix, - filter.fromXmlName(tag))) + rv.append("|%s<%s %s>" % (' ' * indent, prefix, + infosetFilter.fromXmlName(tag))) else: - rv.append("|%s<%s>"%(' '*indent, - filter.fromXmlName(element.tag))) + rv.append("|%s<%s>" % (' ' * indent, + infosetFilter.fromXmlName(element.tag))) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() - name = filter.fromXmlName(name) + name = infosetFilter.fromXmlName(name) prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: - attr_string = filter.fromXmlName(name) + attr_string = infosetFilter.fromXmlName(name) attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 - for child in element.getchildren(): + for child in element: serializeElement(child, indent) - if hasattr(element, "tail") and element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) + rv.append("|%s\"%s\"" % (' ' * 2, finalText)) return "\n".join(rv) + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] finalText = None + def serializeElement(element): if not hasattr(element, "tag"): if element.docinfo.internalDTD: if element.docinfo.doctype: dtd_str = element.docinfo.doctype else: - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name rv.append(dtd_str) serializeElement(element.getroot()) - - elif type(element.tag) == type(etree.Comment): - rv.append(""%(element.text,)) - + + elif element.tag == comment_type: + rv.append("" % (element.text,)) + else: - #This is assumed to be an ordinary element + # This is assumed to be an ordinary element if not element.attrib: - rv.append("<%s>"%(element.tag,)) + rv.append("<%s>" % (element.tag,)) else: - attr = " ".join(["%s=\"%s\""%(name, value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) + attr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) if element.text: rv.append(element.text) - for child in element.getchildren(): + for child in element: serializeElement(child) - rv.append("%s>"%(element.tag,)) + rv.append("%s>" % (element.tag,)) if hasattr(element, "tail") and element.tail: rv.append(element.tail) @@ -161,56 +174,57 @@ def tostring(element): serializeElement(element) if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) + rv.append("%s\"" % (' ' * 2, finalText)) return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = None commentClass = None - fragmentClass = Document + fragmentClass = Document + implementation = etree - def __init__(self, namespaceHTMLElements, fullTree = False): + def __init__(self, namespaceHTMLElements, fullTree=False): builder = etree_builders.getETreeModule(etree, fullTree=fullTree) - filter = self.filter = ihatexml.InfosetFilter() + infosetFilter = self.infosetFilter = ihatexml.InfosetFilter() self.namespaceHTMLElements = namespaceHTMLElements class Attributes(dict): def __init__(self, element, value={}): self._element = element dict.__init__(self, value) - for key, value in self.iteritems(): + for key, value in self.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value def __setitem__(self, key, value): dict.__setitem__(self, key, value) if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value class Element(builder.Element): def __init__(self, name, namespace): - name = filter.coerceElement(name) + name = infosetFilter.coerceElement(name) builder.Element.__init__(self, name, namespace=namespace) self._attributes = Attributes(self) def _setName(self, name): - self._name = filter.coerceElement(name) + self._name = infosetFilter.coerceElement(name) self._element.tag = self._getETreeTag( self._name, self._namespace) - + def _getName(self): - return filter.fromXmlName(self._name) - + return infosetFilter.fromXmlName(self._name) + name = property(_getName, _setName) def _getAttributes(self): @@ -218,24 +232,23 @@ class TreeBuilder(_base.TreeBuilder): def _setAttributes(self, attributes): self._attributes = Attributes(self, attributes) - + attributes = property(_getAttributes, _setAttributes) def insertText(self, data, insertBefore=None): - data = filter.coerceCharacters(data) + data = infosetFilter.coerceCharacters(data) builder.Element.insertText(self, data, insertBefore) def appendChild(self, child): builder.Element.appendChild(self, child) - class Comment(builder.Comment): def __init__(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) builder.Comment.__init__(self, data) def _setData(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) self._element.text = data def _getData(self): @@ -245,9 +258,9 @@ class TreeBuilder(_base.TreeBuilder): self.elementClass = Element self.commentClass = builder.Comment - #self.fragmentClass = builder.DocumentFragment + # self.fragmentClass = builder.DocumentFragment _base.TreeBuilder.__init__(self, namespaceHTMLElements) - + def reset(self): _base.TreeBuilder.reset(self) self.insertComment = self.insertCommentInitial @@ -262,13 +275,13 @@ class TreeBuilder(_base.TreeBuilder): return self.document._elementTree else: return self.document._elementTree.getroot() - + def getFragment(self): fragment = [] element = self.openElements[0]._element if element.text: fragment.append(element.text) - fragment.extend(element.getchildren()) + fragment.extend(list(element)) if element.tail: fragment.append(element.tail) return fragment @@ -278,59 +291,79 @@ class TreeBuilder(_base.TreeBuilder): publicId = token["publicId"] systemId = token["systemId"] - if not name or ihatexml.nonXmlNameBMPRegexp.search(name) or name[0] == '"': - warnings.warn("lxml cannot represent null or non-xml doctype", DataLossWarning) + if not name: + warnings.warn("lxml cannot represent empty doctype", DataLossWarning) + self.doctype = None + else: + coercedName = self.infosetFilter.coerceElement(name) + if coercedName != name: + warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning) + + doctype = self.doctypeClass(coercedName, publicId, systemId) + self.doctype = doctype - doctype = self.doctypeClass(name, publicId, systemId) - self.doctype = doctype - def insertCommentInitial(self, data, parent=None): self.initial_comments.append(data) - + + def insertCommentMain(self, data, parent=None): + if (parent == self.document and + self.document._elementTree.getroot()[-1].tag == comment_type): + warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning) + super(TreeBuilder, self).insertComment(data, parent) + def insertRoot(self, token): """Create the document root""" - #Because of the way libxml2 works, it doesn't seem to be possible to - #alter information like the doctype after the tree has been parsed. - #Therefore we need to use the built-in parser to create our iniial - #tree, after which we can add elements like normal + # Because of the way libxml2 works, it doesn't seem to be possible to + # alter information like the doctype after the tree has been parsed. + # Therefore we need to use the built-in parser to create our iniial + # tree, after which we can add elements like normal docStr = "" - if self.doctype and self.doctype.name and not self.doctype.name.startswith('"'): - docStr += "= 0 and sysid.find('"') >= 0: + warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning) + sysid = sysid.replace("'", 'U00027') + if sysid.find("'") >= 0: + docStr += '"%s"' % sysid + else: + docStr += "'%s'" % sysid + else: + docStr += "''" docStr += ">" + if self.doctype.name != token["name"]: + warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning) docStr += "" - - try: - root = etree.fromstring(docStr) - except etree.XMLSyntaxError: - print docStr - raise - - #Append the initial comments: + root = etree.fromstring(docStr) + + # Append the initial comments: for comment_token in self.initial_comments: root.addprevious(etree.Comment(comment_token["data"])) - - #Create the root document and add the ElementTree to it + + # Create the root document and add the ElementTree to it self.document = self.documentClass() self.document._elementTree = root.getroottree() - + # Give the root element the right name name = token["name"] namespace = token.get("namespace", self.defaultNamespace) if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) root.tag = etree_tag - - #Add the root element to the internal child/open data structures + + # Add the root element to the internal child/open data structures root_element = self.elementClass(name, namespace) root_element._element = root self.document._childNodes.append(root_element) self.openElements.append(root_element) - - #Reset to the default insert comment function - self.insertComment = super(TreeBuilder, self).insertComment + + # Reset to the default insert comment function + self.insertComment = self.insertCommentMain diff --git a/libs/html5lib/treebuilders/simpletree.py b/libs/html5lib/treebuilders/simpletree.py deleted file mode 100755 index 67fe7583..00000000 --- a/libs/html5lib/treebuilders/simpletree.py +++ /dev/null @@ -1,256 +0,0 @@ -import _base -from html5lib.constants import voidElements, namespaces, prefixes -from xml.sax.saxutils import escape - -# Really crappy basic implementation of a DOM-core like thing -class Node(_base.Node): - type = -1 - def __init__(self, name): - self.name = name - self.parent = None - self.value = None - self.childNodes = [] - self._flags = [] - - def __iter__(self): - for node in self.childNodes: - yield node - for item in node: - yield item - - def __unicode__(self): - return self.name - - def toxml(self): - raise NotImplementedError - - def printTree(self, indent=0): - tree = '\n|%s%s' % (' '* indent, unicode(self)) - for child in self.childNodes: - tree += child.printTree(indent + 2) - return tree - - def appendChild(self, node): - assert isinstance(node, Node) - if (isinstance(node, TextNode) and self.childNodes and - isinstance(self.childNodes[-1], TextNode)): - self.childNodes[-1].value += node.value - else: - self.childNodes.append(node) - node.parent = self - - def insertText(self, data, insertBefore=None): - assert isinstance(data, unicode), "data %s is of type %s expected unicode"%(repr(data), type(data)) - if insertBefore is None: - self.appendChild(TextNode(data)) - else: - self.insertBefore(TextNode(data), insertBefore) - - def insertBefore(self, node, refNode): - index = self.childNodes.index(refNode) - if (isinstance(node, TextNode) and index > 0 and - isinstance(self.childNodes[index - 1], TextNode)): - self.childNodes[index - 1].value += node.value - else: - self.childNodes.insert(index, node) - node.parent = self - - def removeChild(self, node): - try: - self.childNodes.remove(node) - except: - # XXX - raise - node.parent = None - - def cloneNode(self): - raise NotImplementedError - - def hasContent(self): - """Return true if the node has children or text""" - return bool(self.childNodes) - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class Document(Node): - type = 1 - def __init__(self): - Node.__init__(self, None) - - def __str__(self): - return "#document" - - def __unicode__(self): - return str(self) - - def appendChild(self, child): - Node.appendChild(self, child) - - def toxml(self, encoding="utf=8"): - result = "" - for child in self.childNodes: - result += child.toxml() - return result.encode(encoding) - - def hilite(self, encoding="utf-8"): - result = "" - for child in self.childNodes: - result += child.hilite() - return result.encode(encoding) + "" - - def printTree(self): - tree = unicode(self) - for child in self.childNodes: - tree += child.printTree(2) - return tree - - def cloneNode(self): - return Document() - -class DocumentFragment(Document): - type = 2 - def __str__(self): - return "#document-fragment" - - def __unicode__(self): - return str(self) - - def cloneNode(self): - return DocumentFragment() - -class DocumentType(Node): - type = 3 - def __init__(self, name, publicId, systemId): - Node.__init__(self, name) - self.publicId = publicId - self.systemId = systemId - - def __unicode__(self): - if self.publicId or self.systemId: - publicId = self.publicId or "" - systemId = self.systemId or "" - return """"""%( - self.name, publicId, systemId) - - else: - return u"" % self.name - - - toxml = __unicode__ - - def hilite(self): - return '<!DOCTYPE %s>' % self.name - - def cloneNode(self): - return DocumentType(self.name, self.publicId, self.systemId) - -class TextNode(Node): - type = 4 - def __init__(self, value): - Node.__init__(self, None) - self.value = value - - def __unicode__(self): - return u"\"%s\"" % self.value - - def toxml(self): - return escape(self.value) - - hilite = toxml - - def cloneNode(self): - return TextNode(self.value) - -class Element(Node): - type = 5 - def __init__(self, name, namespace=None): - Node.__init__(self, name) - self.namespace = namespace - self.attributes = {} - - def __unicode__(self): - if self.namespace == None: - return u"<%s>" % self.name - else: - return u"<%s %s>"%(prefixes[self.namespace], self.name) - - def toxml(self): - result = '<' + self.name - if self.attributes: - for name,value in self.attributes.iteritems(): - result += u' %s="%s"' % (name, escape(value,{'"':'"'})) - if self.childNodes: - result += '>' - for child in self.childNodes: - result += child.toxml() - result += u'%s>' % self.name - else: - result += u'/>' - return result - - def hilite(self): - result = '<%s' % self.name - if self.attributes: - for name, value in self.attributes.iteritems(): - result += ' %s="%s"' % (name, escape(value, {'"':'"'})) - if self.childNodes: - result += ">" - for child in self.childNodes: - result += child.hilite() - elif self.name in voidElements: - return result + ">" - return result + '</%s>' % self.name - - def printTree(self, indent): - tree = '\n|%s%s' % (' '*indent, unicode(self)) - indent += 2 - if self.attributes: - for name, value in sorted(self.attributes.iteritems()): - if isinstance(name, tuple): - name = "%s %s"%(name[0], name[1]) - tree += '\n|%s%s="%s"' % (' ' * indent, name, value) - for child in self.childNodes: - tree += child.printTree(indent) - return tree - - def cloneNode(self): - newNode = Element(self.name) - if hasattr(self, 'namespace'): - newNode.namespace = self.namespace - for attr, value in self.attributes.iteritems(): - newNode.attributes[attr] = value - return newNode - -class CommentNode(Node): - type = 6 - def __init__(self, data): - Node.__init__(self, None) - self.data = data - - def __unicode__(self): - return "" % self.data - - def toxml(self): - return "" % self.data - - def hilite(self): - return '<!--%s-->' % escape(self.data) - - def cloneNode(self): - return CommentNode(self.data) - -class TreeBuilder(_base.TreeBuilder): - documentClass = Document - doctypeClass = DocumentType - elementClass = Element - commentClass = CommentNode - fragmentClass = DocumentFragment - - def testSerializer(self, node): - return node.printTree() diff --git a/libs/html5lib/treebuilders/soup.py b/libs/html5lib/treebuilders/soup.py deleted file mode 100644 index 9bc5ff0e..00000000 --- a/libs/html5lib/treebuilders/soup.py +++ /dev/null @@ -1,236 +0,0 @@ -import warnings - -warnings.warn("BeautifulSoup 3.x (as of 3.1) is not fully compatible with html5lib and support will be removed in the future", DeprecationWarning) - -from BeautifulSoup import BeautifulSoup, Tag, NavigableString, Comment, Declaration - -import _base -from html5lib.constants import namespaces, DataLossWarning - -class AttrList(object): - def __init__(self, element): - self.element = element - self.attrs = dict(self.element.attrs) - def __iter__(self): - return self.attrs.items().__iter__() - def __setitem__(self, name, value): - "set attr", name, value - self.element[name] = value - def items(self): - return self.attrs.items() - def keys(self): - return self.attrs.keys() - def __getitem__(self, name): - return self.attrs[name] - def __contains__(self, name): - return name in self.attrs.keys() - def __eq__(self, other): - if len(self.keys()) != len(other.keys()): - return False - for item in self.keys(): - if item not in other: - return False - if self[item] != other[item]: - return False - return True - -class Element(_base.Node): - def __init__(self, element, soup, namespace): - _base.Node.__init__(self, element.name) - self.element = element - self.soup = soup - self.namespace = namespace - - def _nodeIndex(self, node, refNode): - # Finds a node by identity rather than equality - for index in range(len(self.element.contents)): - if id(self.element.contents[index]) == id(refNode.element): - return index - return None - - def appendChild(self, node): - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[-1].__class__ == NavigableString): - # Concatenate new text onto old text node - # (TODO: This has O(n^2) performance, for input like "aaa...") - newStr = NavigableString(self.element.contents[-1]+node.element) - - # Remove the old text node - # (Can't simply use .extract() by itself, because it fails if - # an equal text node exists within the parent node) - oldElement = self.element.contents[-1] - del self.element.contents[-1] - oldElement.parent = None - oldElement.extract() - - self.element.insert(len(self.element.contents), newStr) - else: - self.element.insert(len(self.element.contents), node.element) - node.parent = self - - def getAttributes(self): - return AttrList(self.element) - - def setAttributes(self, attributes): - if attributes: - for name, value in attributes.items(): - self.element[name] = value - - attributes = property(getAttributes, setAttributes) - - def insertText(self, data, insertBefore=None): - text = TextNode(NavigableString(data), self.soup) - if insertBefore: - self.insertBefore(text, insertBefore) - else: - self.appendChild(text) - - def insertBefore(self, node, refNode): - index = self._nodeIndex(node, refNode) - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[index-1].__class__ == NavigableString): - # (See comments in appendChild) - newStr = NavigableString(self.element.contents[index-1]+node.element) - oldNode = self.element.contents[index-1] - del self.element.contents[index-1] - oldNode.parent = None - oldNode.extract() - - self.element.insert(index-1, newStr) - else: - self.element.insert(index, node.element) - node.parent = self - - def removeChild(self, node): - index = self._nodeIndex(node.parent, node) - del node.parent.element.contents[index] - node.element.parent = None - node.element.extract() - node.parent = None - - def reparentChildren(self, newParent): - while self.element.contents: - child = self.element.contents[0] - child.extract() - if isinstance(child, Tag): - newParent.appendChild(Element(child, self.soup, namespaces["html"])) - else: - newParent.appendChild(TextNode(child, self.soup)) - - def cloneNode(self): - node = Element(Tag(self.soup, self.element.name), self.soup, self.namespace) - for key,value in self.attributes: - node.attributes[key] = value - return node - - def hasContent(self): - return self.element.contents - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class TextNode(Element): - def __init__(self, element, soup): - _base.Node.__init__(self, None) - self.element = element - self.soup = soup - - def cloneNode(self): - raise NotImplementedError - -class TreeBuilder(_base.TreeBuilder): - def __init__(self, namespaceHTMLElements): - if namespaceHTMLElements: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - _base.TreeBuilder.__init__(self, namespaceHTMLElements) - - def documentClass(self): - self.soup = BeautifulSoup("") - return Element(self.soup, self.soup, None) - - def insertDoctype(self, token): - name = token["name"] - publicId = token["publicId"] - systemId = token["systemId"] - - if publicId: - self.soup.insert(0, Declaration("DOCTYPE %s PUBLIC \"%s\" \"%s\""%(name, publicId, systemId or ""))) - elif systemId: - self.soup.insert(0, Declaration("DOCTYPE %s SYSTEM \"%s\""% - (name, systemId))) - else: - self.soup.insert(0, Declaration("DOCTYPE %s"%name)) - - def elementClass(self, name, namespace): - if namespace is not None: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - return Element(Tag(self.soup, name), self.soup, namespace) - - def commentClass(self, data): - return TextNode(Comment(data), self.soup) - - def fragmentClass(self): - self.soup = BeautifulSoup("") - self.soup.name = "[document_fragment]" - return Element(self.soup, self.soup, None) - - def appendChild(self, node): - self.soup.insert(len(self.soup.contents), node.element) - - def testSerializer(self, element): - return testSerializer(element) - - def getDocument(self): - return self.soup - - def getFragment(self): - return _base.TreeBuilder.getFragment(self).element - -def testSerializer(element): - import re - rv = [] - def serializeElement(element, indent=0): - if isinstance(element, Declaration): - doctype_regexp = r'DOCTYPE\s+(?P[^\s]*)( PUBLIC "(?P.*)" "(?P.*)"| SYSTEM "(?P.*)")?' - m = re.compile(doctype_regexp).match(element.string) - assert m is not None, "DOCTYPE did not match expected format" - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') or "" - else: - systemId = m.group('systemId2') - - if publicId is not None or systemId is not None: - rv.append("""|%s"""% - (' '*indent, name, publicId or "", systemId or "")) - else: - rv.append("|%s"%(' '*indent, name)) - - elif isinstance(element, BeautifulSoup): - if element.name == "[document_fragment]": - rv.append("#document-fragment") - else: - rv.append("#document") - - elif isinstance(element, Comment): - rv.append("|%s"%(' '*indent, element.string)) - elif isinstance(element, unicode): - rv.append("|%s\"%s\"" %(' '*indent, element)) - else: - rv.append("|%s<%s>"%(' '*indent, element.name)) - if element.attrs: - for name, value in sorted(element.attrs): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) - indent += 2 - if hasattr(element, "contents"): - for child in element.contents: - serializeElement(child, indent) - serializeElement(element, 0) - - return "\n".join(rv) diff --git a/libs/html5lib/treewalkers/__init__.py b/libs/html5lib/treewalkers/__init__.py index 3a606a8b..18124e75 100644 --- a/libs/html5lib/treewalkers/__init__.py +++ b/libs/html5lib/treewalkers/__init__.py @@ -8,23 +8,27 @@ implements a 'serialize' method taking a tree as sole argument and returning an iterator generating tokens. """ +from __future__ import absolute_import, division, unicode_literals + +import sys + +from ..utils import default_etree + treeWalkerCache = {} + def getTreeWalker(treeType, implementation=None, **kwargs): """Get a TreeWalker class for various types of tree with built-in support treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" + values are: - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. "dom" - The xml.dom.minidom DOM implementation "pulldom" - The xml.dom.pulldom event stream "etree" - A generic walker for tree implementations exposing an elementtree-like interface (known to work with ElementTree, cElementTree and lxml.etree). "lxml" - Optimized walker for lxml.etree - "beautifulsoup" - Beautiful soup (if installed) "genshi" - a Genshi stream implementation - (Currently applies to the "etree" tree type only). A module @@ -33,20 +37,21 @@ def getTreeWalker(treeType, implementation=None, **kwargs): treeType = treeType.lower() if treeType not in treeWalkerCache: - if treeType in ("dom", "pulldom", "simpletree"): - mod = __import__(treeType, globals()) + if treeType in ("dom", "pulldom"): + name = "%s.%s" % (__name__, treeType) + __import__(name) + mod = sys.modules[name] treeWalkerCache[treeType] = mod.TreeWalker elif treeType == "genshi": - import genshistream + from . import genshistream treeWalkerCache[treeType] = genshistream.TreeWalker - elif treeType == "beautifulsoup": - import soup - treeWalkerCache[treeType] = soup.TreeWalker elif treeType == "lxml": - import lxmletree + from . import lxmletree treeWalkerCache[treeType] = lxmletree.TreeWalker elif treeType == "etree": - import etree + from . import etree + if implementation is None: + implementation = default_etree # XXX: NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeWalker return treeWalkerCache.get(treeType) diff --git a/libs/html5lib/treewalkers/_base.py b/libs/html5lib/treewalkers/_base.py index 5929ba05..34252e50 100644 --- a/libs/html5lib/treewalkers/_base.py +++ b/libs/html5lib/treewalkers/_base.py @@ -1,94 +1,9 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type, string_types + import gettext _ = gettext.gettext -from html5lib.constants import voidElements, spaceCharacters -spaceCharacters = u"".join(spaceCharacters) - -class TreeWalker(object): - def __init__(self, tree): - self.tree = tree - - def __iter__(self): - raise NotImplementedError - - def error(self, msg): - return {"type": "SerializeError", "data": msg} - - def normalizeAttrs(self, attrs): - newattrs = {} - if attrs: - #TODO: treewalkers should always have attrs - for (namespace,name),value in attrs.iteritems(): - namespace = unicode(namespace) if namespace else None - name = unicode(name) - value = unicode(value) - newattrs[(namespace,name)] = value - return newattrs - - def emptyTag(self, namespace, name, attrs, hasChildren=False): - yield {"type": "EmptyTag", "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - if hasChildren: - yield self.error(_("Void element has children")) - - def startTag(self, namespace, name, attrs): - return {"type": "StartTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - - def endTag(self, namespace, name): - return {"type": "EndTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": {}} - - def text(self, data): - data = unicode(data) - middle = data.lstrip(spaceCharacters) - left = data[:len(data)-len(middle)] - if left: - yield {"type": "SpaceCharacters", "data": left} - data = middle - middle = data.rstrip(spaceCharacters) - right = data[len(middle):] - if middle: - yield {"type": "Characters", "data": middle} - if right: - yield {"type": "SpaceCharacters", "data": right} - - def comment(self, data): - return {"type": "Comment", "data": unicode(data)} - - def doctype(self, name, publicId=None, systemId=None, correct=True): - return {"type": "Doctype", - "name": name is not None and unicode(name) or u"", - "publicId": publicId, - "systemId": systemId, - "correct": correct} - - def entity(self, name): - return {"type": "Entity", "name": unicode(name)} - - def unknown(self, nodeType): - return self.error(_("Unknown node type: ") + nodeType) - -class RecursiveTreeWalker(TreeWalker): - def walkChildren(self, node): - raise NodeImplementedError - - def element(self, node, namespace, name, attrs, hasChildren): - if name in voidElements: - for token in self.emptyTag(namespace, name, attrs, hasChildren): - yield token - else: - yield self.startTag(name, attrs) - if hasChildren: - for token in self.walkChildren(node): - yield token - yield self.endTag(name) - from xml.dom import Node DOCUMENT = Node.DOCUMENT_NODE @@ -99,16 +14,127 @@ COMMENT = Node.COMMENT_NODE ENTITY = Node.ENTITY_NODE UNKNOWN = "<#UNKNOWN#>" +from ..constants import voidElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +def to_text(s, blank_if_none=True): + """Wrapper around six.text_type to convert None to empty string""" + if s is None: + if blank_if_none: + return "" + else: + return None + elif isinstance(s, text_type): + return s + else: + return text_type(s) + + +def is_text_or_none(string): + """Wrapper around isinstance(string_types) or is None""" + return string is None or isinstance(string, string_types) + + +class TreeWalker(object): + def __init__(self, tree): + self.tree = tree + + def __iter__(self): + raise NotImplementedError + + def error(self, msg): + return {"type": "SerializeError", "data": msg} + + def emptyTag(self, namespace, name, attrs, hasChildren=False): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + yield {"type": "EmptyTag", "name": to_text(name, False), + "namespace": to_text(namespace), + "data": attrs} + if hasChildren: + yield self.error(_("Void element has children")) + + def startTag(self, namespace, name, attrs): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + return {"type": "StartTag", + "name": text_type(name), + "namespace": to_text(namespace), + "data": dict(((to_text(namespace, False), to_text(name)), + to_text(value, False)) + for (namespace, name), value in attrs.items())} + + def endTag(self, namespace, name): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(namespace) + + return {"type": "EndTag", + "name": to_text(name, False), + "namespace": to_text(namespace), + "data": {}} + + def text(self, data): + assert isinstance(data, string_types), type(data) + + data = to_text(data) + middle = data.lstrip(spaceCharacters) + left = data[:len(data) - len(middle)] + if left: + yield {"type": "SpaceCharacters", "data": left} + data = middle + middle = data.rstrip(spaceCharacters) + right = data[len(middle):] + if middle: + yield {"type": "Characters", "data": middle} + if right: + yield {"type": "SpaceCharacters", "data": right} + + def comment(self, data): + assert isinstance(data, string_types), type(data) + + return {"type": "Comment", "data": text_type(data)} + + def doctype(self, name, publicId=None, systemId=None, correct=True): + assert is_text_or_none(name), type(name) + assert is_text_or_none(publicId), type(publicId) + assert is_text_or_none(systemId), type(systemId) + + return {"type": "Doctype", + "name": to_text(name), + "publicId": to_text(publicId), + "systemId": to_text(systemId), + "correct": to_text(correct)} + + def entity(self, name): + assert isinstance(name, string_types), type(name) + + return {"type": "Entity", "name": text_type(name)} + + def unknown(self, nodeType): + return self.error(_("Unknown node type: ") + nodeType) + + class NonRecursiveTreeWalker(TreeWalker): def getNodeDetails(self, node): raise NotImplementedError - + def getFirstChild(self, node): raise NotImplementedError - + def getNextSibling(self, node): raise NotImplementedError - + def getParentNode(self, node): raise NotImplementedError @@ -118,7 +144,6 @@ class NonRecursiveTreeWalker(TreeWalker): details = self.getNodeDetails(currentNode) type, details = details[0], details[1:] hasChildren = False - endTag = None if type == DOCTYPE: yield self.doctype(*details) @@ -130,12 +155,11 @@ class NonRecursiveTreeWalker(TreeWalker): elif type == ELEMENT: namespace, name, attributes, hasChildren = details if name in voidElements: - for token in self.emptyTag(namespace, name, attributes, + for token in self.emptyTag(namespace, name, attributes, hasChildren): yield token hasChildren = False else: - endTag = name yield self.startTag(namespace, name, attributes) elif type == COMMENT: @@ -149,12 +173,12 @@ class NonRecursiveTreeWalker(TreeWalker): else: yield self.unknown(details[0]) - + if hasChildren: firstChild = self.getFirstChild(currentNode) else: firstChild = None - + if firstChild is not None: currentNode = firstChild else: diff --git a/libs/html5lib/treewalkers/dom.py b/libs/html5lib/treewalkers/dom.py index 383b46cb..a01287a9 100644 --- a/libs/html5lib/treewalkers/dom.py +++ b/libs/html5lib/treewalkers/dom.py @@ -1,10 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom import Node import gettext _ = gettext.gettext -import _base -from html5lib.constants import voidElements +from . import _base + class TreeWalker(_base.NonRecursiveTreeWalker): def getNodeDetails(self, node): @@ -16,10 +18,13 @@ class TreeWalker(_base.NonRecursiveTreeWalker): elif node.nodeType == Node.ELEMENT_NODE: attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value - return (_base.ELEMENT, node.namespaceURI, node.nodeName, + if attr.namespaceURI: + attrs[(attr.namespaceURI, attr.localName)] = attr.value + else: + attrs[(None, attr.name)] = attr.value + return (_base.ELEMENT, node.namespaceURI, node.nodeName, attrs, node.hasChildNodes()) elif node.nodeType == Node.COMMENT_NODE: diff --git a/libs/html5lib/treewalkers/etree.py b/libs/html5lib/treewalkers/etree.py index 13b03194..fd8a9cc9 100644 --- a/libs/html5lib/treewalkers/etree.py +++ b/libs/html5lib/treewalkers/etree.py @@ -1,33 +1,28 @@ +from __future__ import absolute_import, division, unicode_literals + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + OrderedDict = dict import gettext _ = gettext.gettext -try: - from types import ModuleType -except: - from new import module as ModuleType -import copy import re -import _base -from html5lib.constants import voidElements +from six import text_type + +from . import _base +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag class TreeWalker(_base.NonRecursiveTreeWalker): """Given the particular ElementTree representation, this implementation, @@ -35,16 +30,16 @@ def getETreeBuilder(ElementTreeImplementation): content: 1. The current element - + 2. The index of the element relative to its parent - + 3. A stack of ancestor elements - + 4. A flag "text", "tail" or None to indicate if the current node is a text node; either the text or tail of the current element (1) """ def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Element + if isinstance(node, tuple): # It might be the root Element elt, key, parents, flag = node if flag in ("text", "tail"): return _base.TEXT, getattr(elt, flag) @@ -54,41 +49,41 @@ def getETreeBuilder(ElementTreeImplementation): if not(hasattr(node, "tag")): node = node.getroot() - if node.tag in ("", ""): + if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"): return (_base.DOCUMENT,) elif node.tag == "": - return (_base.DOCTYPE, node.text, + return (_base.DOCTYPE, node.text, node.get("publicId"), node.get("systemId")) - elif node.tag == ElementTree.Comment: + elif node.tag == ElementTreeCommentType: return _base.COMMENT, node.text else: - assert type(node.tag) in (str, unicode), type(node.tag) - #This is assumed to be an ordinary element + assert type(node.tag) == text_type, type(node.tag) + # This is assumed to be an ordinary element match = tag_regexp.match(node.tag) if match: namespace, tag = match.groups() else: namespace = None tag = node.tag - attrs = {} - for name, value in node.attrib.items(): + attrs = OrderedDict() + for name, value in list(node.attrib.items()): match = tag_regexp.match(name) if match: - attrs[(match.group(1),match.group(2))] = value + attrs[(match.group(1), match.group(2))] = value else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, tag, + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, tag, attrs, len(node) or node.text) - + def getFirstChild(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: element, key, parents, flag = node, None, [], None - + if flag in ("text", "tail"): return None else: @@ -99,13 +94,13 @@ def getETreeBuilder(ElementTreeImplementation): return element[0], 0, parents, None else: return None - + def getNextSibling(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if len(element): parents.append(element) @@ -116,16 +111,16 @@ def getETreeBuilder(ElementTreeImplementation): if element.tail and flag != "tail": return element, key, parents, "tail" elif key < len(parents[-1]) - 1: - return parents[-1][key+1], key+1, parents, None + return parents[-1][key + 1], key + 1, parents, None else: return None - + def getParentNode(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if not parents: return element @@ -139,3 +134,5 @@ def getETreeBuilder(ElementTreeImplementation): return parent, list(parents[-1]).index(parent), parents, None return locals() + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treewalkers/genshistream.py b/libs/html5lib/treewalkers/genshistream.py index ef71a83e..f559c45d 100644 --- a/libs/html5lib/treewalkers/genshistream.py +++ b/libs/html5lib/treewalkers/genshistream.py @@ -1,50 +1,49 @@ +from __future__ import absolute_import, division, unicode_literals + +from genshi.core import QName from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT -from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -from genshi.output import NamespaceFlattener +from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -import _base +from . import _base + +from ..constants import voidElements, namespaces -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): - depth = 0 - ignore_until = None + # Buffer the events so we can pass in the following one previous = None for event in self.tree: if previous is not None: - if previous[0] == START: - depth += 1 - if ignore_until <= depth: - ignore_until = None - if ignore_until is None: - for token in self.tokens(previous, event): - yield token - if token["type"] == "EmptyTag": - ignore_until = depth - if previous[0] == END: - depth -= 1 - previous = event - if previous is not None: - if ignore_until is None or ignore_until <= depth: - for token in self.tokens(previous, None): + for token in self.tokens(previous, event): yield token - elif ignore_until is not None: - raise ValueError("Illformed DOM event stream: void element without END_ELEMENT") + previous = event + + # Don't forget the final event! + if previous is not None: + for token in self.tokens(previous, None): + yield token def tokens(self, event, next): kind, data, pos = event if kind == START: - tag, attrib = data + tag, attribs = data name = tag.localname namespace = tag.namespace - if tag in voidElements: - for token in self.emptyTag(namespace, name, list(attrib), - not next or next[0] != END + converted_attribs = {} + for k, v in attribs: + if isinstance(k, QName): + converted_attribs[(k.namespace, k.localname)] = v + else: + converted_attribs[(None, k)] = v + + if namespace == namespaces["html"] and name in voidElements: + for token in self.emptyTag(namespace, name, converted_attribs, + not next or next[0] != END or next[1] != tag): yield token else: - yield self.startTag(namespace, name, list(attrib)) + yield self.startTag(namespace, name, converted_attribs) elif kind == END: name = data.localname @@ -62,8 +61,8 @@ class TreeWalker(_base.TreeWalker): elif kind == DOCTYPE: yield self.doctype(*data) - elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, \ - START_CDATA, END_CDATA, PI): + elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, + START_CDATA, END_CDATA, PI): pass else: diff --git a/libs/html5lib/treewalkers/lxmletree.py b/libs/html5lib/treewalkers/lxmletree.py index 46f4908c..375cc2e8 100644 --- a/libs/html5lib/treewalkers/lxmletree.py +++ b/libs/html5lib/treewalkers/lxmletree.py @@ -1,186 +1,208 @@ -from lxml import etree -from html5lib.treebuilders.etree import tag_regexp - -from gettext import gettext -_ = gettext - -import _base - -from html5lib.constants import voidElements -from html5lib import ihatexml - -class Root(object): - def __init__(self, et): - self.elementtree = et - self.children = [] - if et.docinfo.internalDTD: - self.children.append(Doctype(self, et.docinfo.root_name, - et.docinfo.public_id, - et.docinfo.system_url)) - root = et.getroot() - node = root - - while node.getprevious() is not None: - node = node.getprevious() - while node is not None: - self.children.append(node) - node = node.getnext() - - self.text = None - self.tail = None - - def __getitem__(self, key): - return self.children[key] - - def getnext(self): - return None - - def __len__(self): - return 1 - -class Doctype(object): - def __init__(self, root_node, name, public_id, system_id): - self.root_node = root_node - self.name = name - self.public_id = public_id - self.system_id = system_id - - self.text = None - self.tail = None - - def getnext(self): - return self.root_node.children[1] - -class FragmentRoot(Root): - def __init__(self, children): - self.children = [FragmentWrapper(self, child) for child in children] - self.text = self.tail = None - - def getnext(self): - return None - -class FragmentWrapper(object): - def __init__(self, fragment_root, obj): - self.root_node = fragment_root - self.obj = obj - if hasattr(self.obj, 'text'): - self.text = self.obj.text - else: - self.text = None - if hasattr(self.obj, 'tail'): - self.tail = self.obj.tail - else: - self.tail = None - self.isstring = isinstance(obj, basestring) - - def __getattr__(self, name): - return getattr(self.obj, name) - - def getnext(self): - siblings = self.root_node.children - idx = siblings.index(self) - if idx < len(siblings) - 1: - return siblings[idx + 1] - else: - return None - - def __getitem__(self, key): - return self.obj[key] - - def __nonzero__(self): - return bool(self.obj) - - def getparent(self): - return None - - def __str__(self): - return str(self.obj) - - def __unicode__(self): - return unicode(self.obj) - - def __len__(self): - return len(self.obj) - - -class TreeWalker(_base.NonRecursiveTreeWalker): - def __init__(self, tree): - if hasattr(tree, "getroot"): - tree = Root(tree) - elif isinstance(tree, list): - tree = FragmentRoot(tree) - _base.NonRecursiveTreeWalker.__init__(self, tree) - self.filter = ihatexml.InfosetFilter() - def getNodeDetails(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - return _base.TEXT, getattr(node, key) - - elif isinstance(node, Root): - return (_base.DOCUMENT,) - - elif isinstance(node, Doctype): - return _base.DOCTYPE, node.name, node.public_id, node.system_id - - elif isinstance(node, FragmentWrapper) and node.isstring: - return _base.TEXT, node - - elif node.tag == etree.Comment: - return _base.COMMENT, node.text - - elif node.tag == etree.Entity: - return _base.ENTITY, node.text[1:-1] # strip &; - - else: - #This is assumed to be an ordinary element - match = tag_regexp.match(node.tag) - if match: - namespace, tag = match.groups() - else: - namespace = None - tag = node.tag - attrs = {} - for name, value in node.attrib.items(): - match = tag_regexp.match(name) - if match: - attrs[(match.group(1),match.group(2))] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), - attrs, len(node) > 0 or node.text) - - def getFirstChild(self, node): - assert not isinstance(node, tuple), _("Text nodes have no children") - - assert len(node) or node.text, "Node has no children" - if node.text: - return (node, "text") - else: - return node[0] - - def getNextSibling(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - # XXX: we cannot use a "bool(node) and node[0] or None" construct here - # because node[0] might evaluate to False if it has no child element - if len(node): - return node[0] - else: - return None - else: # tail - return node.getnext() - - return node.tail and (node, "tail") or node.getnext() - - def getParentNode(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - return node - # else: fallback to "normal" processing - - return node.getparent() +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from lxml import etree +from ..treebuilders.etree import tag_regexp + +from gettext import gettext +_ = gettext + +from . import _base + +from .. import ihatexml + + +def ensure_str(s): + if s is None: + return None + elif isinstance(s, text_type): + return s + else: + return s.decode("utf-8", "strict") + + +class Root(object): + def __init__(self, et): + self.elementtree = et + self.children = [] + if et.docinfo.internalDTD: + self.children.append(Doctype(self, + ensure_str(et.docinfo.root_name), + ensure_str(et.docinfo.public_id), + ensure_str(et.docinfo.system_url))) + root = et.getroot() + node = root + + while node.getprevious() is not None: + node = node.getprevious() + while node is not None: + self.children.append(node) + node = node.getnext() + + self.text = None + self.tail = None + + def __getitem__(self, key): + return self.children[key] + + def getnext(self): + return None + + def __len__(self): + return 1 + + +class Doctype(object): + def __init__(self, root_node, name, public_id, system_id): + self.root_node = root_node + self.name = name + self.public_id = public_id + self.system_id = system_id + + self.text = None + self.tail = None + + def getnext(self): + return self.root_node.children[1] + + +class FragmentRoot(Root): + def __init__(self, children): + self.children = [FragmentWrapper(self, child) for child in children] + self.text = self.tail = None + + def getnext(self): + return None + + +class FragmentWrapper(object): + def __init__(self, fragment_root, obj): + self.root_node = fragment_root + self.obj = obj + if hasattr(self.obj, 'text'): + self.text = ensure_str(self.obj.text) + else: + self.text = None + if hasattr(self.obj, 'tail'): + self.tail = ensure_str(self.obj.tail) + else: + self.tail = None + self.isstring = isinstance(obj, str) or isinstance(obj, bytes) + # Support for bytes here is Py2 + if self.isstring: + self.obj = ensure_str(self.obj) + + def __getattr__(self, name): + return getattr(self.obj, name) + + def getnext(self): + siblings = self.root_node.children + idx = siblings.index(self) + if idx < len(siblings) - 1: + return siblings[idx + 1] + else: + return None + + def __getitem__(self, key): + return self.obj[key] + + def __bool__(self): + return bool(self.obj) + + def getparent(self): + return None + + def __str__(self): + return str(self.obj) + + def __unicode__(self): + return str(self.obj) + + def __len__(self): + return len(self.obj) + + +class TreeWalker(_base.NonRecursiveTreeWalker): + def __init__(self, tree): + if hasattr(tree, "getroot"): + tree = Root(tree) + elif isinstance(tree, list): + tree = FragmentRoot(tree) + _base.NonRecursiveTreeWalker.__init__(self, tree) + self.filter = ihatexml.InfosetFilter() + + def getNodeDetails(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + return _base.TEXT, ensure_str(getattr(node, key)) + + elif isinstance(node, Root): + return (_base.DOCUMENT,) + + elif isinstance(node, Doctype): + return _base.DOCTYPE, node.name, node.public_id, node.system_id + + elif isinstance(node, FragmentWrapper) and node.isstring: + return _base.TEXT, node.obj + + elif node.tag == etree.Comment: + return _base.COMMENT, ensure_str(node.text) + + elif node.tag == etree.Entity: + return _base.ENTITY, ensure_str(node.text)[1:-1] # strip &; + + else: + # This is assumed to be an ordinary element + match = tag_regexp.match(ensure_str(node.tag)) + if match: + namespace, tag = match.groups() + else: + namespace = None + tag = ensure_str(node.tag) + attrs = {} + for name, value in list(node.attrib.items()): + name = ensure_str(name) + value = ensure_str(value) + match = tag_regexp.match(name) + if match: + attrs[(match.group(1), match.group(2))] = value + else: + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), + attrs, len(node) > 0 or node.text) + + def getFirstChild(self, node): + assert not isinstance(node, tuple), _("Text nodes have no children") + + assert len(node) or node.text, "Node has no children" + if node.text: + return (node, "text") + else: + return node[0] + + def getNextSibling(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + # XXX: we cannot use a "bool(node) and node[0] or None" construct here + # because node[0] might evaluate to False if it has no child element + if len(node): + return node[0] + else: + return None + else: # tail + return node.getnext() + + return (node, "tail") if node.tail else node.getnext() + + def getParentNode(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + return node + # else: fallback to "normal" processing + + return node.getparent() diff --git a/libs/html5lib/treewalkers/pulldom.py b/libs/html5lib/treewalkers/pulldom.py index 1f8b95b8..0b0f515f 100644 --- a/libs/html5lib/treewalkers/pulldom.py +++ b/libs/html5lib/treewalkers/pulldom.py @@ -1,9 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom.pulldom import START_ELEMENT, END_ELEMENT, \ COMMENT, IGNORABLE_WHITESPACE, CHARACTERS -import _base +from . import _base + +from ..constants import voidElements -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): @@ -11,7 +14,7 @@ class TreeWalker(_base.TreeWalker): previous = None for event in self.tree: if previous is not None and \ - (ignore_until is None or previous[1] is ignore_until): + (ignore_until is None or previous[1] is ignore_until): if previous[1] is ignore_until: ignore_until = None for token in self.tokens(previous, event): @@ -31,9 +34,9 @@ class TreeWalker(_base.TreeWalker): name = node.nodeName namespace = node.namespaceURI attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value + attrs[(attr.namespaceURI, attr.localName)] = attr.value if name in voidElements: for token in self.emptyTag(namespace, name, diff --git a/libs/html5lib/treewalkers/simpletree.py b/libs/html5lib/treewalkers/simpletree.py deleted file mode 100644 index 9e6bd4c5..00000000 --- a/libs/html5lib/treewalkers/simpletree.py +++ /dev/null @@ -1,78 +0,0 @@ -import gettext -_ = gettext.gettext - -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - """Given that simpletree has no performant way of getting a node's - next sibling, this implementation returns "nodes" as tuples with the - following content: - - 1. The parent Node (Element, Document or DocumentFragment) - - 2. The child index of the current node in its parent's children list - - 3. A list used as a stack of all ancestors. It is a pair tuple whose - first item is a parent Node and second item is a child index. - """ - - def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - node = parent.childNodes[idx] - - # testing node.type allows us not to import treebuilders.simpletree - if node.type in (1, 2): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif node.type == 3: # DocumentType - return _base.DOCTYPE, node.name, node.publicId, node.systemId - - elif node.type == 4: # TextNode - return _base.TEXT, node.value - - elif node.type == 5: # Element - attrs = {} - for name, value in node.attributes.items(): - if isinstance(name, tuple): - attrs[(name[2],name[1])] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, node.namespace, node.name, - attrs, node.hasContent()) - - elif node.type == 6: # CommentNode - return _base.COMMENT, node.data - - else: - return _node.UNKNOWN, node.type - - def getFirstChild(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - parents.append((parent, idx)) - node = parent.childNodes[idx] - else: - parents = [] - - assert node.hasContent(), "Node has no children" - return (node, 0, parents) - - def getNextSibling(self, node): - assert isinstance(node, tuple), "Node is not a tuple: " + str(node) - parent, idx, parents = node - idx += 1 - if len(parent.childNodes) > idx: - return (parent, idx, parents) - else: - return None - - def getParentNode(self, node): - assert isinstance(node, tuple) - parent, idx, parents = node - if parents: - parent, idx = parents.pop() - return parent, idx, parents - else: - # HACK: We could return ``parent`` but None will stop the algorithm the same way - return None diff --git a/libs/html5lib/treewalkers/soup.py b/libs/html5lib/treewalkers/soup.py deleted file mode 100644 index fca65ecb..00000000 --- a/libs/html5lib/treewalkers/soup.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -import gettext -_ = gettext.gettext - -from BeautifulSoup import BeautifulSoup, Declaration, Comment, Tag -from html5lib.constants import namespaces -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - doctype_regexp = re.compile( - r'DOCTYPE\s+(?P[^\s]*)(\s*PUBLIC\s*"(?P.*)"\s*"(?P.*)"|\s*SYSTEM\s*"(?P.*)")?') - def getNodeDetails(self, node): - if isinstance(node, BeautifulSoup): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif isinstance(node, Declaration): # DocumentType - string = unicode(node.string) - #Slice needed to remove markup added during unicode conversion, - #but only in some versions of BeautifulSoup/Python - if string.startswith(''): - string = string[2:-1] - m = self.doctype_regexp.match(string) - #This regexp approach seems wrong and fragile - #but beautiful soup stores the doctype as a single thing and we want the seperate bits - #It should work as long as the tree is created by html5lib itself but may be wrong if it's - #been modified at all - #We could just feed to it a html5lib tokenizer, I guess... - assert m is not None, "DOCTYPE did not match expected format" - - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') - else: - systemId = m.group('systemId2') - return _base.DOCTYPE, name, publicId or "", systemId or "" - - elif isinstance(node, Comment): - string = unicode(node.string) - if string.startswith(''): - string = string[4:-3] - return _base.COMMENT, string - - elif isinstance(node, unicode): # TextNode - return _base.TEXT, node - - elif isinstance(node, Tag): # Element - return (_base.ELEMENT, namespaces["html"], node.name, - dict(node.attrs).items(), node.contents) - else: - return _base.UNKNOWN, node.__class__.__name__ - - def getFirstChild(self, node): - return node.contents[0] - - def getNextSibling(self, node): - return node.nextSibling - - def getParentNode(self, node): - return node.parent diff --git a/libs/html5lib/trie/__init__.py b/libs/html5lib/trie/__init__.py new file mode 100644 index 00000000..a8cca8a9 --- /dev/null +++ b/libs/html5lib/trie/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + +from .py import Trie as PyTrie + +Trie = PyTrie + +try: + from .datrie import Trie as DATrie +except ImportError: + pass +else: + Trie = DATrie diff --git a/libs/html5lib/trie/_base.py b/libs/html5lib/trie/_base.py new file mode 100644 index 00000000..724486b1 --- /dev/null +++ b/libs/html5lib/trie/_base.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division, unicode_literals + +from collections import Mapping + + +class Trie(Mapping): + """Abstract base class for tries""" + + def keys(self, prefix=None): + keys = super().keys() + + if prefix is None: + return set(keys) + + # Python 2.6: no set comprehensions + return set([x for x in keys if x.startswith(prefix)]) + + def has_keys_with_prefix(self, prefix): + for key in self.keys(): + if key.startswith(prefix): + return True + + return False + + def longest_prefix(self, prefix): + if prefix in self: + return prefix + + for i in range(1, len(prefix) + 1): + if prefix[:-i] in self: + return prefix[:-i] + + raise KeyError(prefix) + + def longest_prefix_item(self, prefix): + lprefix = self.longest_prefix(prefix) + return (lprefix, self[lprefix]) diff --git a/libs/html5lib/trie/datrie.py b/libs/html5lib/trie/datrie.py new file mode 100644 index 00000000..51f3d046 --- /dev/null +++ b/libs/html5lib/trie/datrie.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from datrie import Trie as DATrie +from six import text_type + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + chars = set() + for key in data.keys(): + if not isinstance(key, text_type): + raise TypeError("All keys must be strings") + for char in key: + chars.add(char) + + self._data = DATrie("".join(chars)) + for key, value in data.items(): + self._data[key] = value + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + raise NotImplementedError() + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + return self._data.keys(prefix) + + def has_keys_with_prefix(self, prefix): + return self._data.has_keys_with_prefix(prefix) + + def longest_prefix(self, prefix): + return self._data.longest_prefix(prefix) + + def longest_prefix_item(self, prefix): + return self._data.longest_prefix_item(prefix) diff --git a/libs/html5lib/trie/py.py b/libs/html5lib/trie/py.py new file mode 100644 index 00000000..c2ba3da7 --- /dev/null +++ b/libs/html5lib/trie/py.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from bisect import bisect_left + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + if not all(isinstance(x, text_type) for x in data.keys()): + raise TypeError("All keys must be strings") + + self._data = data + self._keys = sorted(data.keys()) + self._cachestr = "" + self._cachepoints = (0, len(data)) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + if prefix is None or prefix == "" or not self._keys: + return set(self._keys) + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + start = i = bisect_left(self._keys, prefix, lo, hi) + else: + start = i = bisect_left(self._keys, prefix) + + keys = set() + if start == len(self._keys): + return keys + + while self._keys[i].startswith(prefix): + keys.add(self._keys[i]) + i += 1 + + self._cachestr = prefix + self._cachepoints = (start, i) + + return keys + + def has_keys_with_prefix(self, prefix): + if prefix in self._data: + return True + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + i = bisect_left(self._keys, prefix, lo, hi) + else: + i = bisect_left(self._keys, prefix) + + if i == len(self._keys): + return False + + return self._keys[i].startswith(prefix) diff --git a/libs/html5lib/utils.py b/libs/html5lib/utils.py index d53f6788..2f41f4df 100644 --- a/libs/html5lib/utils.py +++ b/libs/html5lib/utils.py @@ -1,9 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals + +from types import ModuleType + try: - frozenset -except NameError: - #Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset + import xml.etree.cElementTree as default_etree +except ImportError: + import xml.etree.ElementTree as default_etree + + +__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", + "surrogatePairToCodepoint", "moduleFactoryFactory"] + class MethodDispatcher(dict): """Dict with 2 special properties: @@ -23,7 +30,7 @@ class MethodDispatcher(dict): # twice as fast. Please do careful performance testing before changing # anything here. _dictEntries = [] - for name,value in items: + for name, value in items: if type(name) in (list, tuple, frozenset, set): for item in name: _dictEntries.append((item, value)) @@ -35,141 +42,41 @@ class MethodDispatcher(dict): def __getitem__(self, key): return dict.get(self, key, self.default) -#Pure python implementation of deque taken from the ASPN Python Cookbook -#Original code by Raymond Hettinger -class deque(object): +# Some utility functions to dal with weirdness around UCS2 vs UCS4 +# python builds - def __init__(self, iterable=(), maxsize=-1): - if not hasattr(self, 'data'): - self.left = self.right = 0 - self.data = {} - self.maxsize = maxsize - self.extend(iterable) - - def append(self, x): - self.data[self.right] = x - self.right += 1 - if self.maxsize != -1 and len(self) > self.maxsize: - self.popleft() - - def appendleft(self, x): - self.left -= 1 - self.data[self.left] = x - if self.maxsize != -1 and len(self) > self.maxsize: - self.pop() - - def pop(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - self.right -= 1 - elem = self.data[self.right] - del self.data[self.right] - return elem - - def popleft(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - elem = self.data[self.left] - del self.data[self.left] - self.left += 1 - return elem - - def clear(self): - self.data.clear() - self.left = self.right = 0 - - def extend(self, iterable): - for elem in iterable: - self.append(elem) - - def extendleft(self, iterable): - for elem in iterable: - self.appendleft(elem) - - def rotate(self, n=1): - if self: - n %= len(self) - for i in xrange(n): - self.appendleft(self.pop()) - - def __getitem__(self, i): - if i < 0: - i += len(self) - try: - return self.data[i + self.left] - except KeyError: - raise IndexError - - def __setitem__(self, i, value): - if i < 0: - i += len(self) - try: - self.data[i + self.left] = value - except KeyError: - raise IndexError - - def __delitem__(self, i): - size = len(self) - if not (-size <= i < size): - raise IndexError - data = self.data - if i < 0: - i += size - for j in xrange(self.left+i, self.right-1): - data[j] = data[j+1] - self.pop() - - def __len__(self): - return self.right - self.left - - def __cmp__(self, other): - if type(self) != type(other): - return cmp(type(self), type(other)) - return cmp(list(self), list(other)) - - def __repr__(self, _track=[]): - if id(self) in _track: - return '...' - _track.append(id(self)) - r = 'deque(%r)' % (list(self),) - _track.remove(id(self)) - return r - - def __getstate__(self): - return (tuple(self),) - - def __setstate__(self, s): - self.__init__(s[0]) - - def __hash__(self): - raise TypeError - - def __copy__(self): - return self.__class__(self) - - def __deepcopy__(self, memo={}): - from copy import deepcopy - result = self.__class__() - memo[id(self)] = result - result.__init__(deepcopy(tuple(self), memo)) - return result - -#Some utility functions to dal with weirdness around UCS2 vs UCS4 -#python builds - -def encodingType(): - if len() == 2: - return "UCS2" - else: - return "UCS4" - -def isSurrogatePair(data): +def isSurrogatePair(data): return (len(data) == 2 and ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF) + def surrogatePairToCodepoint(data): - char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + + char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + (ord(data[1]) - 0xDC00)) return char_val + +# Module Factory Factory (no, this isn't Java, I know) +# Here to stop this being duplicated all over the place. + + +def moduleFactoryFactory(factory): + moduleCache = {} + + def moduleFactory(baseModule, *args, **kwargs): + if isinstance(ModuleType.__name__, type("")): + name = "_%s_factory" % baseModule.__name__ + else: + name = b"_%s_factory" % baseModule.__name__ + + if name in moduleCache: + return moduleCache[name] + else: + mod = ModuleType(name) + objs = factory(baseModule, *args, **kwargs) + mod.__dict__.update(objs) + moduleCache[name] = mod + return mod + + return moduleFactory diff --git a/libs/httplib2/__init__.py b/libs/httplib2/__init__.py index 01151f7f..9780d4e5 100644 --- a/libs/httplib2/__init__.py +++ b/libs/httplib2/__init__.py @@ -3,7 +3,7 @@ from __future__ import generators httplib2 A caching http interface that supports ETags and gzip -to conserve bandwidth. +to conserve bandwidth. Requires Python 2.3 or later @@ -15,17 +15,17 @@ Changelog: __author__ = "Joe Gregorio (joe@bitworking.org)" __copyright__ = "Copyright 2006, Joe Gregorio" __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", - "James Antill", - "Xavier Verges Farrero", - "Jonathan Feinberg", - "Blair Zajac", - "Sam Ruby", - "Louis Nyffenegger"] + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] __license__ = "MIT" -__version__ = "$Rev$" +__version__ = "0.8" -import re -import sys +import re +import sys import email import email.Utils import email.Message @@ -35,6 +35,7 @@ import gzip import zlib import httplib import urlparse +import urllib import base64 import os import copy @@ -42,10 +43,10 @@ import calendar import time import random import errno -# remove depracated warning in python2.6 try: from hashlib import sha1 as _sha, md5 as _md5 except ImportError: + # prior to Python 2.5, these were separate modules import sha import md5 _sha = sha.new @@ -54,21 +55,38 @@ import hmac from gettext import gettext as _ import socket -# Try using local version, followed by system, and none if neither are found try: - import lib.socks as socks + from httplib2 import socks except ImportError: try: - import socks as socks - except ImportError: + import socks + except (ImportError, AttributeError): socks = None # Build the appropriate socket wrapper for ssl try: import ssl # python 2.6 - _ssl_wrap_socket = ssl.wrap_socket -except ImportError: - def _ssl_wrap_socket(sock, key_file, cert_file): + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") ssl_sock = socket.ssl(sock, key_file, cert_file) return httplib.FakeSocket(sock, ssl_sock) @@ -84,15 +102,19 @@ def has_timeout(timeout): # python 2.6 return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) return (timeout is not None) -__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', - 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', - 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', - 'debuglevel'] +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] # The httplib debug level, set to a non-zero value to get debug output debuglevel = 0 +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 # Python 2.3 support if sys.version_info < (2,4): @@ -113,8 +135,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'): # All exceptions raised here derive from HttpLib2Error class HttpLib2Error(Exception): pass -# Some exceptions can be caught and optionally -# be turned back into responses. +# Some exceptions can be caught and optionally +# be turned back into responses. class HttpLib2ErrorWithResponse(HttpLib2Error): def __init__(self, desc, response, content): self.response = response @@ -127,8 +149,18 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class MalformedHeader(HttpLib2Error): pass class RelativeURIError(HttpLib2Error): pass class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert # Open Items: # ----------- @@ -152,6 +184,16 @@ class ServerNotFoundError(HttpLib2Error): pass # requesting that URI again. DEFAULT_MAX_REDIRECTS = 5 +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + # Which headers are hop-by-hop headers by default HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] @@ -176,7 +218,7 @@ def urlnorm(uri): raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) authority = authority.lower() scheme = scheme.lower() - if not path: + if not path: path = "/" # Could do syntax based normalization of the URI before # computing the digest. See Section 6.2.2 of Std 66. @@ -228,7 +270,7 @@ def _parse_cache_control(headers): parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] retval = dict(parts_with_args + parts_wo_args) - return retval + return retval # Whether to use a strict mode to parse WWW-Authenticate headers # Might lead to bad results in case of ill-formed header value, @@ -249,25 +291,30 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): per auth_scheme.""" retval = {} if headers.has_key(headername): - authenticate = headers[headername].strip() - www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED - while authenticate: - # Break off the scheme at the beginning of the line - if headername == 'authentication-info': - (auth_scheme, the_rest) = ('digest', authenticate) - else: - (auth_scheme, the_rest) = authenticate.split(" ", 1) - # Now loop over all the key value pairs that come after the scheme, - # being careful not to roll into the next scheme - match = www_auth.search(the_rest) - auth_params = {} - while match: - if match and len(match.groups()) == 3: - (key, value, the_rest) = match.groups() - auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + try: + + authenticate = headers[headername].strip() + www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED + while authenticate: + # Break off the scheme at the beginning of the line + if headername == 'authentication-info': + (auth_scheme, the_rest) = ('digest', authenticate) + else: + (auth_scheme, the_rest) = authenticate.split(" ", 1) + # Now loop over all the key value pairs that come after the scheme, + # being careful not to roll into the next scheme match = www_auth.search(the_rest) - retval[auth_scheme.lower()] = auth_params - authenticate = the_rest.strip() + auth_params = {} + while match: + if match and len(match.groups()) == 3: + (key, value, the_rest) = match.groups() + auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + match = www_auth.search(the_rest) + retval[auth_scheme.lower()] = auth_params + authenticate = the_rest.strip() + + except ValueError: + raise MalformedHeader("WWW-Authenticate") return retval @@ -279,17 +326,17 @@ def _entry_disposition(response_headers, request_headers): 1. Cache-Control: max-stale 2. Age: headers are not used in the calculations. - Not that this algorithm is simpler than you might think + Not that this algorithm is simpler than you might think because we are operating as a private (non-shared) cache. This lets us ignore 's-maxage'. We can also ignore 'proxy-invalidate' since we aren't a proxy. - We will never return a stale document as - fresh as a design decision, and thus the non-implementation - of 'max-stale'. This also lets us safely ignore 'must-revalidate' + We will never return a stale document as + fresh as a design decision, and thus the non-implementation + of 'max-stale'. This also lets us safely ignore 'must-revalidate' since we operate as if every server has sent 'must-revalidate'. Since we are private we get to ignore both 'public' and 'private' parameters. We also ignore 'no-transform' since - we don't do any transformations. + we don't do any transformations. The 'no-store' parameter is handled at a higher level. So the only Cache-Control parameters we look at are: @@ -298,7 +345,7 @@ def _entry_disposition(response_headers, request_headers): max-age min-fresh """ - + retval = "STALE" cc = _parse_cache_control(request_headers) cc_response = _parse_cache_control(response_headers) @@ -340,10 +387,10 @@ def _entry_disposition(response_headers, request_headers): min_fresh = int(cc['min-fresh']) except ValueError: min_fresh = 0 - current_age += min_fresh + current_age += min_fresh if freshness_lifetime > current_age: retval = "FRESH" - return retval + return retval def _decompressContent(response, new_content): content = new_content @@ -391,7 +438,7 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey): if status == 304: status = 200 - status_header = 'status: %d\r\n' % response_headers.status + status_header = 'status: %d\r\n' % status header_str = info.as_string() @@ -408,10 +455,10 @@ def _wsse_username_token(cnonce, iso_now, password): return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() -# For credentials we need two things, first +# For credentials we need two things, first # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # Then we also need a list of URIs that have already demanded authentication -# That list is tricky since sub-URIs can take the same auth, or the +# That list is tricky since sub-URIs can take the same auth, or the # auth scheme may change as you descend the tree. # So we also need each Auth instance to be able to tell us # how close to the 'top' it is. @@ -435,7 +482,7 @@ class Authentication(object): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate - Authorization header. Over-rise this in sub-classes.""" + Authorization header. Over-ride this in sub-classes.""" pass def response(self, response, content): @@ -443,7 +490,7 @@ class Authentication(object): or such returned from the last authorized response. Over-rise this in sub-classes if necessary. - Return TRUE is the request is to be retried, for + Return TRUE is the request is to be retried, for example Digest may return stale=true. """ return False @@ -461,7 +508,7 @@ class BasicAuthentication(Authentication): class DigestAuthentication(Authentication): - """Only do qop='auth' and MD5, since that + """Only do qop='auth' and MD5, since that is all Apache currently implements""" def __init__(self, credentials, host, request_uri, headers, response, content, http): Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) @@ -474,7 +521,7 @@ class DigestAuthentication(Authentication): self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() if self.challenge['algorithm'] != 'MD5': raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) - self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) + self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.challenge['nc'] = 1 def request(self, method, request_uri, headers, content, cnonce = None): @@ -482,23 +529,24 @@ class DigestAuthentication(Authentication): H = lambda x: _md5(x).hexdigest() KD = lambda s, d: H("%s:%s" % (s, d)) A2 = "".join([method, ":", request_uri]) - self.challenge['cnonce'] = cnonce or _cnonce() - request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], - '%08x' % self.challenge['nc'], - self.challenge['cnonce'], - self.challenge['qop'], H(A2) - )) - headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( - self.credentials[0], + self.challenge['cnonce'] = cnonce or _cnonce() + request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % ( + self.challenge['nonce'], + '%08x' % self.challenge['nc'], + self.challenge['cnonce'], + self.challenge['qop'], H(A2))) + headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['nonce'], - request_uri, + request_uri, self.challenge['algorithm'], request_digest, self.challenge['qop'], self.challenge['nc'], - self.challenge['cnonce'], - ) + self.challenge['cnonce']) + if self.challenge.get('opaque'): + headers['authorization'] += ', opaque="%s"' % self.challenge['opaque'] self.challenge['nc'] += 1 def response(self, response, content): @@ -506,14 +554,14 @@ class DigestAuthentication(Authentication): challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return True else: updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) if updated_challenge.has_key('nextnonce'): self.challenge['nonce'] = updated_challenge['nextnonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return False @@ -547,9 +595,8 @@ class HmacDigestAuthentication(Authentication): else: self.pwhashmod = _sha self.key = "".join([self.credentials[0], ":", - self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), - ":", self.challenge['realm'] - ]) + self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), + ":", self.challenge['realm']]) self.key = self.pwhashmod.new(self.key).hexdigest().lower() def request(self, method, request_uri, headers, content): @@ -561,16 +608,15 @@ class HmacDigestAuthentication(Authentication): cnonce = _cnonce() request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() - headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( - self.credentials[0], + headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['snonce'], cnonce, - request_uri, + request_uri, created, request_digest, - keylist, - ) + keylist) def response(self, response, content): challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {}) @@ -583,7 +629,7 @@ class WsseAuthentication(Authentication): """This is thinly tested and should not be relied upon. At this time there isn't any third party server to test against. Blogger and TypePad implemented this algorithm at one point - but Blogger has since switched to Basic over HTTPS and + but Blogger has since switched to Basic over HTTPS and TypePad has implemented it wrong, by never issuing a 401 challenge but instead requiring your client to telepathically know that their endpoint is expecting WSSE profile="UsernameToken".""" @@ -593,7 +639,7 @@ class WsseAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['Authorization'] = 'WSSE profile="UsernameToken"' + headers['authorization'] = 'WSSE profile="UsernameToken"' iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) cnonce = _cnonce() password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1]) @@ -629,7 +675,7 @@ class GoogleLoginAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth AUTH_SCHEME_CLASSES = { @@ -644,13 +690,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] class FileCache(object): """Uses a local directory as a store for cached files. - Not really safe to use if multiple threads or processes are going to + Not really safe to use if multiple threads or processes are going to be running on the same cache. """ def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior self.cache = cache self.safe = safe - if not os.path.exists(cache): + if not os.path.exists(cache): os.makedirs(self.cache) def get(self, key): @@ -660,7 +706,7 @@ class FileCache(object): f = file(cacheFullPath, "rb") retval = f.read() f.close() - except IOError, e: + except IOError: pass return retval @@ -688,34 +734,127 @@ class Credentials(object): def iter(self, domain): for (cdomain, name, password) in self.credentials: if cdomain == "" or domain == cdomain: - yield (name, password) + yield (name, password) class KeyCerts(Credentials): """Identical to Credentials except that name/password are mapped to key/cert.""" pass +class AllHosts(object): + pass class ProxyInfo(object): - """Collect information required to use a proxy.""" - def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None): - """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX - constants. For example: + """Collect information required to use a proxy.""" + bypass_hosts = () -p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000) - """ - self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: - def astuple(self): - return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, - self.proxy_user, self.proxy_pass) + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass - def isgood(self): - return socks and (self.proxy_host != None) and (self.proxy_port != None) + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) class HTTPConnectionWithTimeout(httplib.HTTPConnection): - """HTTPConnection subclass that supports timeouts""" + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): httplib.HTTPConnection.__init__(self, host, port, strict) @@ -725,27 +864,46 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): def connect(self): """Connect to the host and port specified in __init__.""" # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') msg = "getaddrinfo returns an empty list" - for res in socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM): + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: - if self.proxy_info and self.proxy_info.isgood(): + if use_proxy: self.sock = socks.socksocket(af, socktype, proto) - self.sock.setproxy(*self.proxy_info.astuple()) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) else: self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Different from httplib: support timeouts. if has_timeout(self.timeout): self.sock.settimeout(self.timeout) # End of difference from httplib. if self.debuglevel > 0: - print "connect: (%s, %s)" % (self.host, self.port) + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) - self.sock.connect(sa) + self.sock.connect((self.host, self.port) + sa[2:]) except socket.error, msg: if self.debuglevel > 0: - print 'connect fail:', (self.host, self.port) + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) if self.sock: self.sock.close() self.sock = None @@ -755,56 +913,265 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): raise socket.error, msg class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): - "This class allows communication via SSL." + """ + This class allows communication via SSL. + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=None, proxy_info=None): - httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file, - cert_file=cert_file, strict=strict) + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) self.timeout = timeout self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False def connect(self): "Connect to a host on a given (SSL) port." + msg = "getaddrinfo returns an empty list" if self.proxy_info and self.proxy_info.isgood(): - sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) - sock.setproxy(*self.proxy_info.astuple()) + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() else: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - if has_timeout(self.timeout): - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) - self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file) + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=5): + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass class Http(object): """An HTTP client that handles: -- all methods -- caching -- ETags -- compression, -- HTTPS -- Basic -- Digest -- WSSE -and more. + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. """ - def __init__(self, cache=None, timeout=None, proxy_info=None): - """The value of proxy_info is a ProxyInfo instance. + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. -If 'cache' is a string then it is used as a directory name -for a disk cache. Otherwise it must be an object that supports -the same interface as FileCache.""" + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + # Map domain name to an httplib connection self.connections = {} # The location of the cache, for now a directory # where cached responses are held. - if cache and isinstance(cache, str): + if cache and isinstance(cache, basestring): self.cache = FileCache(cache) else: self.cache = cache @@ -820,10 +1187,10 @@ the same interface as FileCache.""" # If set to False then no redirects are followed, even safe ones. self.follow_redirects = True - + # Which HTTP methods do we apply optimistic concurrency to, i.e. # which methods get an "if-match:" etag header added to them. - self.optimistic_concurrency_methods = ["PUT"] + self.optimistic_concurrency_methods = ["PUT", "PATCH"] # If 'follow_redirects' is True, and this is set to True then # all redirecs are followed, including unsafe ones. @@ -831,10 +1198,27 @@ the same interface as FileCache.""" self.ignore_etag = False - self.force_exception_to_status_code = False + self.force_exception_to_status_code = False self.timeout = timeout + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + def _auth_from_challenge(self, host, request_uri, headers, response, content): """A generator that creates Authorization objects that can be applied to requests. @@ -862,37 +1246,56 @@ the same interface as FileCache.""" self.authorizations = [] def _conn_request(self, conn, request_uri, method, body, headers): - for i in range(2): + for i in range(RETRIES): try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() conn.request(method, request_uri, body, headers) + except socket.timeout: + raise except socket.gaierror: conn.close() raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise except socket.error, e: - if not hasattr(e, 'errno'): # I don't know what this is so lets raise it if it happens + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused raise - elif e.errno == errno.ECONNREFUSED: # Connection refused - raise - # Just because the server closed the connection doesn't apparently mean - # that the server didn't send a response. - pass except httplib.HTTPException: # Just because the server closed the connection doesn't apparently mean # that the server didn't send a response. - pass + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue try: response = conn.getresponse() except (socket.error, httplib.HTTPException): - if i == 0: + if i < RETRIES-1: conn.close() conn.connect() continue else: + conn.close() raise else: content = "" if method == "HEAD": - response.close() + conn.close() else: content = response.read() response = Response(response) @@ -908,12 +1311,12 @@ the same interface as FileCache.""" auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auth = auths and sorted(auths)[0][1] or None - if auth: + if auth: auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers) - if auth: + if auth: if auth.response(response, body): auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers ) @@ -921,7 +1324,7 @@ the same interface as FileCache.""" if response.status == 401: for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): - authorization.request(method, request_uri, headers, body) + authorization.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) if response.status != 401: self.authorizations.append(authorization) @@ -944,26 +1347,31 @@ the same interface as FileCache.""" if response.status == 301 and method in ["GET", "HEAD"]: response['-x-permanent-redirect-url'] = response['location'] if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) if headers.has_key('if-none-match'): del headers['if-none-match'] if headers.has_key('if-modified-since'): del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] if response.has_key('location'): location = response['location'] old_response = copy.deepcopy(response) if not old_response.has_key('content-location'): - old_response['content-location'] = absolute_uri - redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) response.previous = old_response else: - raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) - elif response.status in [200, 203] and method == "GET": + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: # Don't cache 206's since we aren't going to handle byte range requests if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) return (response, content) @@ -978,24 +1386,25 @@ the same interface as FileCache.""" def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): """ Performs a single HTTP request. -The 'uri' is the URI of the HTTP resource and can begin -with either 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. -There is no restriction on the methods allowed. + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'body' is the entity body to be sent with the request. It is a string -object. + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. -Any extra headers that are to be sent with the request should be provided in the -'headers' dictionary. + The 'body' is the entity body to be sent with the request. It is a + string object. -The maximum number of redirect to follow before raising an -exception is 'redirections. The default is 5. + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. -The return value is a tuple of (response, content), the first -being and instance of the 'Response' class, the second being -a string that contains the response entity body. + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. """ try: if headers is None: @@ -1004,7 +1413,7 @@ a string that contains the response entity body. headers = self._normalize_headers(headers) if not headers.has_key('user-agent'): - headers['user-agent'] = "Python-httplib2/%s" % __version__ + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ uri = iri2uri(uri) @@ -1014,21 +1423,38 @@ a string that contains the response entity body. scheme = 'https' authority = domain_port[0] + proxy_info = self._get_proxy_info(scheme, authority) + conn_key = scheme+":"+authority if conn_key in self.connections: conn = self.connections[conn_key] else: if not connection_type: - connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout + connection_type = SCHEME_TO_CONNECTION[scheme] certs = list(self.certificates.iter(authority)) - if scheme == 'https' and certs: - conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], - cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) else: - conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info) + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) conn.set_debuglevel(debuglevel) - if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers: + if 'range' not in headers and 'accept-encoding' not in headers: headers['accept-encoding'] = 'gzip, deflate' info = email.Message.Message() @@ -1048,7 +1474,7 @@ a string that contains the response entity body. feedparser.feed(info) info = feedparser.close() feedparser._parse = None - except IndexError, ValueError: + except (IndexError, ValueError): self.cache.delete(cachekey) cachekey = None cached_value = None @@ -1071,13 +1497,15 @@ a string that contains the response entity body. for header in vary_headers: key = '-varied-%s' % header value = info[key] - if headers.get(header, '') != value: - cached_value = None - break + if headers.get(header, None) != value: + cached_value = None + break if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: if info.has_key('-x-permanent-redirect-url'): # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) response.previous = Response(info) response.previous.fromcache = True @@ -1085,13 +1513,13 @@ a string that contains the response entity body. # Determine our course of action: # Is the cached entry fresh or stale? # Has the client requested a non-cached response? - # - # There seems to be three possible answers: + # + # There seems to be three possible answers: # 1. [FRESH] Return the cache entry w/o doing a GET # 2. [STALE] Do the GET (but add in cache validators if available) # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request - entry_disposition = _entry_disposition(info, headers) - + entry_disposition = _entry_disposition(info, headers) + if entry_disposition == "FRESH": if not cached_value: info['status'] = '504' @@ -1113,7 +1541,7 @@ a string that contains the response entity body. if response.status == 304 and method == "GET": # Rewrite the cache entry with the new end-to-end headers - # Take all headers that are in response + # Take all headers that are in response # and overwrite their values in info. # unless they are hop-by-hop, or are listed in the connection header. @@ -1125,14 +1553,14 @@ a string that contains the response entity body. _updateCache(headers, merged_response, content, self.cache, cachekey) response = merged_response response.status = 200 - response.fromcache = True + response.fromcache = True elif response.status == 200: content = new_content else: self.cache.delete(cachekey) - content = new_content - else: + content = new_content + else: cc = _parse_cache_control(headers) if cc.has_key('only-if-cached'): info['status'] = '504' @@ -1146,34 +1574,47 @@ a string that contains the response entity body. response = e.response content = e.content response.status = 500 - response.reason = str(e) - elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): + response.reason = str(e) + elif isinstance(e, socket.timeout): content = "Request Timeout" - response = Response( { - "content-type": "text/plain", - "status": "408", - "content-length": len(content) - }) + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) response.reason = "Request Timeout" else: - content = str(e) - response = Response( { - "content-type": "text/plain", - "status": "400", - "content-length": len(content) - }) - response.reason = "Bad Request" + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" else: raise - + return (response, content) - + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + class Response(dict): """An object more like email.Message than httplib.HTTPResponse.""" - + """Is this response from our local cache""" fromcache = False @@ -1189,27 +1630,28 @@ class Response(dict): previous = None def __init__(self, info): - # info is either an email.Message or + # info is either an email.Message or # an httplib.HTTPResponse object. if isinstance(info, httplib.HTTPResponse): - for key, value in info.getheaders(): - self[key.lower()] = value + for key, value in info.getheaders(): + self[key.lower()] = value self.status = info.status self['status'] = str(self.status) self.reason = info.reason self.version = info.version elif isinstance(info, email.Message.Message): - for key, value in info.items(): - self[key] = value + for key, value in info.items(): + self[key.lower()] = value self.status = int(self['status']) else: - for key, value in info.iteritems(): - self[key] = value + for key, value in info.iteritems(): + self[key.lower()] = value self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) def __getattr__(self, name): if name == 'dict': - return self - else: - raise AttributeError, name + return self + else: + raise AttributeError, name diff --git a/libs/httplib2/cacerts.txt b/libs/httplib2/cacerts.txt new file mode 100644 index 00000000..d8a0027c --- /dev/null +++ b/libs/httplib2/cacerts.txt @@ -0,0 +1,739 @@ +# Certifcate Authority certificates for validating SSL connections. +# +# This file contains PEM format certificates generated from +# http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Netscape security libraries. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1994-2000 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +Verisign/RSA Secure Server CA +============================= + +-----BEGIN CERTIFICATE----- +MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD +VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0 +MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV +BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy +dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ +ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII +0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI +uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI +hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3 +YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc +1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA== +-----END CERTIFICATE----- + +Thawte Personal Basic CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDITCCAoqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFBlcnNvbmFsIEJhc2lj +IENBMSgwJgYJKoZIhvcNAQkBFhlwZXJzb25hbC1iYXNpY0B0aGF3dGUuY29tMB4X +DTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgcsxCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQZXJzb25hbCBCYXNpYyBD +QTEoMCYGCSqGSIb3DQEJARYZcGVyc29uYWwtYmFzaWNAdGhhd3RlLmNvbTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53 +dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK +wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7 +G1sY0b8jkyECAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQF +AAOBgQAt4plrsD16iddZopQBHyvdEktTwq1/qqcAXJFAVyVKOKqEcLnZgA+le1z7 +c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P +9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ== +-----END CERTIFICATE----- + +Thawte Personal Premium CA +========================== + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBzzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEjMCEGA1UEAxMaVGhhd3RlIFBlcnNvbmFsIFByZW1p +dW0gQ0ExKjAoBgkqhkiG9w0BCQEWG3BlcnNvbmFsLXByZW1pdW1AdGhhd3RlLmNv +bTAeFw05NjAxMDEwMDAwMDBaFw0yMDEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlDYXBlIFRvd24xGjAY +BgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9u +IFNlcnZpY2VzIERpdmlzaW9uMSMwIQYDVQQDExpUaGF3dGUgUGVyc29uYWwgUHJl +bWl1bSBDQTEqMCgGCSqGSIb3DQEJARYbcGVyc29uYWwtcHJlbWl1bUB0aGF3dGUu +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJZtn4B0TPuYwu8KHvE0Vs +Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI +Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD +ZicRFTuqW/KY3TZCstqIdQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH +b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh +KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGJ +-----END CERTIFICATE----- + +Thawte Personal Freemail CA +=========================== + +-----BEGIN CERTIFICATE----- +MIIDLTCCApagAwIBAgIBADANBgkqhkiG9w0BAQQFADCB0TELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEkMCIGA1UEAxMbVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIENBMSswKQYJKoZIhvcNAQkBFhxwZXJzb25hbC1mcmVlbWFpbEB0aGF3dGUu +Y29tMB4XDTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgdExCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEa +MBgGA1UEChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBG +cmVlbWFpbCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhh +d3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1GnX1LCUZFtx6UfY +DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E +rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq +uzgkCGqYx7Hao5iR/Xnb5VrEHLkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zAN +BgkqhkiG9w0BAQQFAAOBgQDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP +MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa +/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei +gQ== +-----END CERTIFICATE----- + +Thawte Server CA +================ + +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +Thawte Premium Server CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +Equifax Secure CA +================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05 +NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD +VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp +bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N +H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR +4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN +BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo +EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5 +FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx +lA== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh +YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7 +FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg +J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc +r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM +HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK +qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj +cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y +cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP +T8qAkbYp +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Equifax Secure Global eBusiness CA +================================== + +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 1 +============================= + +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 2 +============================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +Thawte Time Stamping CA +======================= + +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBizELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzAN +BgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAd +BgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNOTcwMTAxMDAwMDAwWhcN +MjAxMjMxMjM1OTU5WjCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4g +Q2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsG +A1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1l +c3RhbXBpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYrWHhhRYZT +6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa +Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL +8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEEBQADgYEAZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC +9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ +pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnRUEzGLhQOEkbdYATAUOK8oyvyxUBkZ +CayJSdM= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== + +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +Entrust.net Secure Server Certification Authority +================================================= + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +Go Daddy Certification Authority Root Certificate Bundle +======================================================== + +-----BEGIN CERTIFICATE----- +MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx +ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw +MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH +QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j +b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j +b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H +KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm +VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR +SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT +cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ +6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu +MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS +kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB +BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f +BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv +c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH +AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO +BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG +OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU +A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o +0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX +RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH +qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV +U+4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh +bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu +Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g +QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe +BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX +DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE +YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC +ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q +N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO +r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN +f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH +U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU +TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb +VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg +SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv +biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg +MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw +AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv +ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu +Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd +IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv +bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1 +QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O +WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf +SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +GeoTrust Global CA +================== + +-----BEGIN CERTIFICATE----- +MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 +aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw +WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE +AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m +OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu +T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c +JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR +Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz +PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm +aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM +TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g +LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO +BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv +dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB +AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL +NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W +b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S +-----END CERTIFICATE----- + diff --git a/libs/httplib2/iri2uri.py b/libs/httplib2/iri2uri.py index 70667edf..d88c91fd 100644 --- a/libs/httplib2/iri2uri.py +++ b/libs/httplib2/iri2uri.py @@ -16,7 +16,7 @@ import urlparse # Convert an IRI to a URI following the rules in RFC 3987 -# +# # The characters we need to enocde and escape are defined in the spec: # # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD @@ -28,28 +28,28 @@ import urlparse # / %xD0000-DFFFD / %xE1000-EFFFD escape_range = [ - (0xA0, 0xD7FF ), - (0xE000, 0xF8FF ), - (0xF900, 0xFDCF ), - (0xFDF0, 0xFFEF), - (0x10000, 0x1FFFD ), - (0x20000, 0x2FFFD ), - (0x30000, 0x3FFFD), - (0x40000, 0x4FFFD ), - (0x50000, 0x5FFFD ), - (0x60000, 0x6FFFD), - (0x70000, 0x7FFFD ), - (0x80000, 0x8FFFD ), - (0x90000, 0x9FFFD), - (0xA0000, 0xAFFFD ), - (0xB0000, 0xBFFFD ), - (0xC0000, 0xCFFFD), - (0xD0000, 0xDFFFD ), - (0xE1000, 0xEFFFD), - (0xF0000, 0xFFFFD ), - (0x100000, 0x10FFFD) + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), ] - + def encode(c): retval = c i = ord(c) @@ -63,19 +63,19 @@ def encode(c): def iri2uri(uri): - """Convert an IRI to a URI. Note that IRIs must be + """Convert an IRI to a URI. Note that IRIs must be passed in a unicode strings. That is, do not utf-8 encode - the IRI before passing it into the function.""" + the IRI before passing it into the function.""" if isinstance(uri ,unicode): (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) authority = authority.encode('idna') # For each character in 'ucschar' or 'iprivate' # 1. encode as utf-8 - # 2. then %-encode each octet of that utf-8 + # 2. then %-encode each octet of that utf-8 uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = "".join([encode(c) for c in uri]) return uri - + if __name__ == "__main__": import unittest @@ -83,7 +83,7 @@ if __name__ == "__main__": def test_uris(self): """Test that URIs are invariant under the transformation.""" - invariant = [ + invariant = [ u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"http://www.ietf.org/rfc/rfc2396.txt", u"ldap://[2001:db8::7]/c=GB?objectClass?one", @@ -94,7 +94,7 @@ if __name__ == "__main__": u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] for uri in invariant: self.assertEqual(uri, iri2uri(uri)) - + def test_iri(self): """ Test that the right type of escaping is done for each part of the URI.""" self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) @@ -107,4 +107,4 @@ if __name__ == "__main__": unittest.main() - + diff --git a/libs/httplib2/socks.py b/libs/httplib2/socks.py new file mode 100644 index 00000000..0991f4cf --- /dev/null +++ b/libs/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/libs/pyasn1/__init__.py b/libs/pyasn1/__init__.py index 7de39fe5..88aff79c 100644 --- a/libs/pyasn1/__init__.py +++ b/libs/pyasn1/__init__.py @@ -1 +1,8 @@ -majorVersionId = '1' +import sys + +# http://www.python.org/dev/peps/pep-0396/ +__version__ = '0.1.7' + +if sys.version_info[:2] < (2, 4): + raise RuntimeError('PyASN1 requires Python 2.4 or later') + diff --git a/libs/pyasn1/codec/__init__.py b/libs/pyasn1/codec/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/__init__.py +++ b/libs/pyasn1/codec/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/__init__.py b/libs/pyasn1/codec/ber/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/ber/__init__.py +++ b/libs/pyasn1/codec/ber/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/decoder.py b/libs/pyasn1/codec/ber/decoder.py index ae9311cb..be0cf490 100644 --- a/libs/pyasn1/codec/ber/decoder.py +++ b/libs/pyasn1/codec/ber/decoder.py @@ -1,21 +1,24 @@ # BER decoder from pyasn1.type import tag, base, univ, char, useful, tagmap from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import oct2int, octs2ints -from pyasn1 import error +from pyasn1.compat.octets import oct2int, octs2ints, isOctetsType +from pyasn1 import debug, error class AbstractDecoder: protoComponent = None def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Decoder not implemented for %s' % (tagSet,)) def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % (tagSet,)) class AbstractSimpleDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatSimple,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(value, tagSet) elif value is None: @@ -24,7 +27,10 @@ class AbstractSimpleDecoder(AbstractDecoder): return asn1Spec.clone(value) class AbstractConstructedDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatConstructed,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(tagSet) else: @@ -32,19 +38,34 @@ class AbstractConstructedDecoder(AbstractDecoder): class EndOfOctetsDecoder(AbstractSimpleDecoder): def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return eoo.endOfOctets, substrate[:length] + length, state, decodeFun, substrateFun): + return eoo.endOfOctets, substrate[length:] class ExplicitTagDecoder(AbstractSimpleDecoder): + protoComponent = univ.Any('') + tagFormats = (tag.tagFormatConstructed,) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return decodeFun(substrate[:length], asn1Spec, tagSet, length) + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) + head, tail = substrate[:length], substrate[length:] + value, _ = decodeFun(head, asn1Spec, tagSet, length) + return value, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) value, substrate = decodeFun(substrate, asn1Spec, tagSet, length) terminator, substrate = decodeFun(substrate) - if terminator == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(terminator) and \ + terminator == eoo.endOfOctets: return value, substrate else: raise error.PyAsn1Error('Missing end-of-octets terminator') @@ -71,79 +92,71 @@ class IntegerDecoder(AbstractSimpleDecoder): '\xfb': -5 } - def _valueFilter(self, value): - try: - return int(value) - except OverflowError: - return value - def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: - raise error.PyAsn1Error('Empty substrate') - if substrate in self.precomputedValues: - value = self.precomputedValues[substrate] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0), tail + if head in self.precomputedValues: + value = self.precomputedValues[head] else: - firstOctet = oct2int(substrate[0]) + firstOctet = oct2int(head[0]) if firstOctet & 0x80: value = -1 else: value = 0 - for octet in substrate: + for octet in head: value = value << 8 | oct2int(octet) - value = self._valueFilter(value) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class BooleanDecoder(IntegerDecoder): protoComponent = univ.Boolean(0) - def _valueFilter(self, value): - if value: - return 1 - else: - return 0 + def _createComponent(self, asn1Spec, tagSet, value=None): + return IntegerDecoder._createComponent(self, asn1Spec, tagSet, value and 1 or 0) class BitStringDecoder(AbstractSimpleDecoder): protoComponent = univ.BitString(()) + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - if not substrate: - raise error.PyAsn1Error('Missing initial octet') - trailingBits = oct2int(substrate[0]) + if not head: + raise error.PyAsn1Error('Empty substrate') + trailingBits = oct2int(head[0]) if trailingBits > 7: raise error.PyAsn1Error( 'Trailing bits overflow %s' % trailingBits ) - substrate = substrate[1:] - lsb = p = 0; l = len(substrate)-1; b = () + head = head[1:] + lsb = p = 0; l = len(head)-1; b = () while p <= l: if p == l: lsb = trailingBits j = 7 - o = oct2int(substrate[p]) + o = oct2int(head[p]) while j >= lsb: b = b + ((o>>j)&0x01,) j = j - 1 p = p + 1 - return self._createComponent(asn1Spec, tagSet, b), '' + return self._createComponent(asn1Spec, tagSet, b), tail r = self._createComponent(asn1Spec, tagSet, ()) - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -154,27 +167,29 @@ class BitStringDecoder(AbstractSimpleDecoder): class OctetStringDecoder(AbstractSimpleDecoder): protoComponent = univ.OctetString('') + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - return self._createComponent(asn1Spec, tagSet, substrate), '' + return self._createComponent(asn1Spec, tagSet, head), tail r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -186,93 +201,89 @@ class OctetStringDecoder(AbstractSimpleDecoder): class NullDecoder(AbstractSimpleDecoder): protoComponent = univ.Null('') def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if substrate: - raise error.PyAsn1Error('Unexpected substrate for Null') - return r, substrate + if head: + raise error.PyAsn1Error('Unexpected %d-octet substrate for Null' % length) + return r, tail class ObjectIdentifierDecoder(AbstractSimpleDecoder): protoComponent = univ.ObjectIdentifier(()) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - oid = (); index = 0 - # Get the first subid - subId = oct2int(substrate[index]) - oid = oid + divmod(subId, 40) - index = index + 1 - substrateLen = len(substrate) - + # Get the first subid + subId = oct2int(head[0]) + oid = divmod(subId, 40) + + index = 1 + substrateLen = len(head) while index < substrateLen: - subId = oct2int(substrate[index]) - if subId < 128: - oid = oid + (subId,) - index = index + 1 - else: + subId = oct2int(head[index]) + index = index + 1 + if subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in sub-ID OID + # encoding, tolerating it opens a vulnerability. + # See http://www.cosic.esat.kuleuven.be/publications/article-1432.pdf page 7 + raise error.PyAsn1Error('Invalid leading 0x80 in sub-OID') + elif subId > 128: # Construct subid from a number of octets nextSubId = subId subId = 0 - while nextSubId >= 128 and index < substrateLen: + while nextSubId >= 128: subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (oid,) + ) + nextSubId = oct2int(head[index]) index = index + 1 - nextSubId = oct2int(substrate[index]) - if index == substrateLen: - raise error.SubstrateUnderrunError( - 'Short substrate for OID %s' % oid - ) subId = (subId << 7) + nextSubId - oid = oid + (subId,) - index = index + 1 - return self._createComponent(asn1Spec, tagSet, oid), substrate[index:] + oid = oid + (subId,) + return self._createComponent(asn1Spec, tagSet, oid), tail class RealDecoder(AbstractSimpleDecoder): protoComponent = univ.Real() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] - if not length: - raise error.SubstrateUnderrunError('Short substrate for Real') - fo = oct2int(substrate[0]); substrate = substrate[1:] - if fo & 0x40: # infinite value - value = fo & 0x01 and '-inf' or 'inf' - elif fo & 0x80: # binary enoding - if fo & 0x11 == 0: - n = 1 - elif fo & 0x01: - n = 2 - elif fo & 0x02: - n = 3 - else: - n = oct2int(substrate[0]) - eo, substrate = substrate[:n], substrate[n:] - if not eo or not substrate: + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0.0), tail + fo = oct2int(head[0]); head = head[1:] + if fo & 0x80: # binary enoding + n = (fo & 0x03) + 1 + if n == 4: + n = oct2int(head[0]) + eo, head = head[:n], head[n:] + if not eo or not head: raise error.PyAsn1Error('Real exponent screwed') - e = 0 + e = oct2int(eo[0]) & 0x80 and -1 or 0 while eo: # exponent e <<= 8 e |= oct2int(eo[0]) eo = eo[1:] p = 0 - while substrate: # value + while head: # value p <<= 8 - p |= oct2int(substrate[0]) - substrate = substrate[1:] + p |= oct2int(head[0]) + head = head[1:] if fo & 0x40: # sign bit p = -p value = (p, 2, e) + elif fo & 0x40: # infinite value + value = fo & 0x01 and '-inf' or 'inf' elif fo & 0xc0 == 0: # character encoding try: if fo & 0x3 == 0x1: # NR1 - value = (int(substrate), 10, 0) + value = (int(head), 10, 0) elif fo & 0x3 == 0x2: # NR2 - value = float(substrate) + value = float(head) elif fo & 0x3 == 0x3: # NR3 - value = float(substrate) + value = float(head) else: raise error.SubstrateUnderrunError( 'Unknown NR (tag %s)' % fo @@ -281,13 +292,11 @@ class RealDecoder(AbstractSimpleDecoder): raise error.SubstrateUnderrunError( 'Bad character Real syntax' ) - elif fo & 0xc0 == 0x40: # special real value - pass else: raise error.SubstrateUnderrunError( 'Unknown encoding (tag %s)' % fo ) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class SequenceDecoder(AbstractConstructedDecoder): protoComponent = univ.Sequence() @@ -301,17 +310,15 @@ class SequenceDecoder(AbstractConstructedDecoder): return r.getComponentPositionNearType(t, idx) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) idx = 0 - if not decodeFun: - return r, substrate - while substrate: + if substrateFun: + return substrateFun(r, substrate, length) + while head: asn1Spec = self._getComponentTagMap(r, idx) - component, substrate = decodeFun( - substrate, asn1Spec - ) + component, head = decodeFun(head, asn1Spec) idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx ) @@ -319,18 +326,19 @@ class SequenceDecoder(AbstractConstructedDecoder): idx = idx + 1 r.setDefaultComponents() r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) idx = 0 while substrate: asn1Spec = self._getComponentTagMap(r, idx) - if not decodeFun: - return r, substrate component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx @@ -348,32 +356,31 @@ class SequenceDecoder(AbstractConstructedDecoder): class SequenceOfDecoder(AbstractConstructedDecoder): protoComponent = univ.SequenceOf() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun( - substrate, asn1Spec - ) + while head: + component, head = decodeFun(head, asn1Spec) r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 @@ -401,43 +408,68 @@ class SetOfDecoder(SequenceOfDecoder): class ChoiceDecoder(AbstractConstructedDecoder): protoComponent = univ.Choice() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) if r.getTagSet() == tagSet: # explicitly tagged Choice - component, substrate = decodeFun( - substrate, r.getComponentTagMap() + component, head = decodeFun( + head, r.getComponentTagMap() ) else: - component, substrate = decodeFun( - substrate, r.getComponentTagMap(), tagSet, length, state + component, head = decodeFun( + head, r.getComponentTagMap(), tagSet, length, state ) if isinstance(component, univ.Choice): effectiveTagSet = component.getEffectiveTagSet() else: effectiveTagSet = component.getTagSet() r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) + return r, tail + + def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, + length, state, decodeFun, substrateFun): + r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) + if r.getTagSet() == tagSet: # explicitly tagged Choice + component, substrate = decodeFun(substrate, r.getComponentTagMap()) + eooMarker, substrate = decodeFun(substrate) # eat up EOO marker + if not eoo.endOfOctets.isSameTypeWith(eooMarker) or \ + eooMarker != eoo.endOfOctets: + raise error.PyAsn1Error('No EOO seen before substrate ends') + else: + component, substrate= decodeFun( + substrate, r.getComponentTagMap(), tagSet, length, state + ) + if isinstance(component, univ.Choice): + effectiveTagSet = component.getEffectiveTagSet() + else: + effectiveTagSet = component.getTagSet() + r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) return r, substrate - indefLenValueDecoder = valueDecoder - class AnyDecoder(AbstractSimpleDecoder): protoComponent = univ.Any() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is None or \ asn1Spec is not None and tagSet != asn1Spec.getTagSet(): # untagged Any container, recover inner header substrate length = length + len(fullSubstrate) - len(substrate) substrate = fullSubstrate - substrate = substrate[:length] - return self._createComponent(asn1Spec, tagSet, value=substrate), '' + if substrateFun: + return substrateFun(self._createComponent(asn1Spec, tagSet), + substrate, length) + head, tail = substrate[:length], substrate[length:] + return self._createComponent(asn1Spec, tagSet, value=head), tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is not None and tagSet == asn1Spec.getTagSet(): # tagged Any type -- consume header substrate header = '' @@ -450,11 +482,12 @@ class AnyDecoder(AbstractSimpleDecoder): # Any components do not inherit initial tag asn1Spec = self.protoComponent - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -550,7 +583,10 @@ class Decoder: self.__tagSetCache = {} def __call__(self, substrate, asn1Spec=None, tagSet=None, - length=None, state=stDecodeTag, recursiveFlag=1): + length=None, state=stDecodeTag, recursiveFlag=1, + substrateFun=None): + if debug.logger & debug.flagDecoder: + debug.logger('decoder called at scope %s with state %d, working with up to %d octets of substrate: %s' % (debug.scope, state, len(substrate), debug.hexdump(substrate))) fullSubstrate = substrate while state != stStop: if state == stDecodeTag: @@ -559,6 +595,9 @@ class Decoder: raise error.SubstrateUnderrunError( 'Short octet stream on tag decoding' ) + if not isOctetsType(substrate) and \ + not isinstance(substrate, univ.OctetString): + raise error.PyAsn1Error('Bad octet stream type') firstOctet = substrate[0] substrate = substrate[1:] @@ -598,6 +637,7 @@ class Decoder: else: tagSet = lastTag + tagSet state = stDecodeLength + debug.logger and debug.logger & debug.flagDecoder and debug.logger('tag decoded into %r, decoding length' % tagSet) if state == stDecodeLength: # Decode length if not substrate: @@ -625,12 +665,13 @@ class Decoder: for char in lengthString: length = (length << 8) | oct2int(char) size = size + 1 - state = stGetValueDecoder substrate = substrate[size:] if length != -1 and len(substrate) < length: raise error.SubstrateUnderrunError( '%d-octet short' % (length - len(substrate)) ) + state = stGetValueDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value length decoded into %d, payload substrate is: %s' % (length, debug.hexdump(length == -1 and substrate or substrate[:length]))) if state == stGetValueDecoder: if asn1Spec is None: state = stGetValueDecoderByTag @@ -669,14 +710,27 @@ class Decoder: state = stDecodeValue else: state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by a built-in type, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(concreteDecoder is None and '?' or concreteDecoder.protoComponent.__class__.__name__) if state == stGetValueDecoderByAsn1Spec: if isinstance(asn1Spec, (dict, tagmap.TagMap)): if tagSet in asn1Spec: __chosenSpec = asn1Spec[tagSet] else: __chosenSpec = None + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('candidate ASN.1 spec is a map of:') + for t, v in asn1Spec.getPosMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + if asn1Spec.getNegMap(): + debug.logger('but neither of: ') + for i in asn1Spec.getNegMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + debug.logger('new candidate ASN.1 spec is %s, chosen by %r' % (__chosenSpec is None and '' or __chosenSpec.__class__.__name__, tagSet)) else: __chosenSpec = asn1Spec + debug.logger and debug.logger & debug.flagDecoder and debug.logger('candidate ASN.1 spec is %s' % asn1Spec.__class__.__name__) if __chosenSpec is not None and ( tagSet == __chosenSpec.getTagSet() or \ tagSet in __chosenSpec.getTagMap() @@ -687,9 +741,11 @@ class Decoder: __chosenSpec.typeId in self.__typeMap: # ambiguous type concreteDecoder = self.__typeMap[__chosenSpec.typeId] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen for an ambiguous type by type ID %s' % (__chosenSpec.typeId,)) elif baseTagSet in self.__tagMap: # base type or tagged subtype concreteDecoder = self.__tagMap[baseTagSet] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen by base %r' % (baseTagSet,)) else: concreteDecoder = None if concreteDecoder: @@ -700,8 +756,13 @@ class Decoder: elif tagSet == self.__endOfOctetsTagSet: concreteDecoder = self.__tagMap[tagSet] state = stDecodeValue + debug.logger and debug.logger & debug.flagDecoder and debug.logger('end-of-octets found') else: + concreteDecoder = None state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by ASN.1 spec, decoding %s' % (state == stDecodeValue and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(__chosenSpec is None and '?' or __chosenSpec.__class__.__name__) if state == stTryAsExplicitTag: if tagSet and \ tagSet[0][1] == tag.tagFormatConstructed and \ @@ -710,34 +771,35 @@ class Decoder: concreteDecoder = explicitTagDecoder state = stDecodeValue else: + concreteDecoder = None state = self.defaultErrorState + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as failure')) if state == stDumpRawValue: concreteDecoder = self.defaultRawDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding value' % concreteDecoder.__class__.__name__) state = stDecodeValue if state == stDecodeValue: - if recursiveFlag: - decodeFun = self - else: - decodeFun = None + if recursiveFlag == 0 and not substrateFun: # legacy + substrateFun = lambda a,b,c: (a,b[:c]) if length == -1: # indef length value, substrate = concreteDecoder.indefLenValueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) else: - value, _substrate = concreteDecoder.valueDecoder( + value, substrate = concreteDecoder.valueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) - if recursiveFlag: - substrate = substrate[length:] - else: - substrate = _substrate state = stStop + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s yields type %s, value:\n%s\n...remaining substrate is: %s' % (concreteDecoder.__class__.__name__, value.__class__.__name__, value.prettyPrint(), substrate and debug.hexdump(substrate) or '')) if state == stErrorCondition: raise error.PyAsn1Error( '%r not in asn1Spec: %r' % (tagSet, asn1Spec) ) + if debug.logger and debug.logger & debug.flagDecoder: + debug.scope.pop() + debug.logger('decoder left scope %s, call completed' % debug.scope) return value, substrate decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/ber/encoder.py b/libs/pyasn1/codec/ber/encoder.py index 2149b0ba..173949d0 100644 --- a/libs/pyasn1/codec/ber/encoder.py +++ b/libs/pyasn1/codec/ber/encoder.py @@ -1,8 +1,8 @@ # BER encoder from pyasn1.type import base, tag, univ, char, useful from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import int2oct, ints2octs, null, str2octs -from pyasn1 import error +from pyasn1.compat.octets import int2oct, oct2int, ints2octs, null, str2octs +from pyasn1 import debug, error class Error(Exception): pass @@ -78,9 +78,24 @@ class ExplicitlyTaggedItemEncoder(AbstractItemEncoder): explicitlyTaggedItemEncoder = ExplicitlyTaggedItemEncoder() +class BooleanEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 + _true = ints2octs((1,)) + _false = ints2octs((0,)) + def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + return value and self._true or self._false, 0 + class IntegerEncoder(AbstractItemEncoder): supportIndefLenMode = 0 + supportCompactZero = False def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + if value == 0: # shortcut for zero value + if self.supportCompactZero: + # this seems to be a correct way for encoding zeros + return null, 0 + else: + # this seems to be a widespread way for encoding zeros + return ints2octs((0,)), 0 octets = [] value = int(value) # to save on ops on asn1 type while 1: @@ -149,18 +164,15 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): index = 5 else: if len(oid) < 2: - raise error.PyAsn1Error('Short OID %s' % value) + raise error.PyAsn1Error('Short OID %s' % (value,)) # Build the first twos - index = 0 - subid = oid[index] * 40 - subid = subid + oid[index+1] - if subid < 0 or subid > 0xff: + if oid[0] > 6 or oid[1] > 39 or oid[0] == 6 and oid[1] > 15: raise error.PyAsn1Error( - 'Initial sub-ID overflow %s in OID %s' % (oid[index:], value) + 'Initial sub-ID overflow %s in OID %s' % (oid[:2], value) ) - octets = (subid,) - index = index + 2 + octets = (oid[0] * 40 + oid[1],) + index = 2 # Cycle through subids for subid in oid[index:]: @@ -184,6 +196,7 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): return ints2octs(octets), 0 class RealEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 def encodeValue(self, encodeFun, value, defMode, maxChunkSize): if value.isPlusInfinity(): return int2oct(0x40), 0 @@ -206,9 +219,11 @@ class RealEncoder(AbstractItemEncoder): m >>= 1 e += 1 eo = null - while e: + while e not in (0, -1): eo = int2oct(e&0xff) + eo e >>= 8 + if e == 0 and eo and oct2int(eo[0]) & 0x80: + eo = int2oct(0) + eo n = len(eo) if n > 0xff: raise error.PyAsn1Error('Real exponent overflow') @@ -268,7 +283,7 @@ class AnyEncoder(OctetStringEncoder): tagMap = { eoo.endOfOctets.tagSet: EndOfOctetsEncoder(), - univ.Boolean.tagSet: IntegerEncoder(), + univ.Boolean.tagSet: BooleanEncoder(), univ.Integer.tagSet: IntegerEncoder(), univ.BitString.tagSet: BitStringEncoder(), univ.OctetString.tagSet: OctetStringEncoder(), @@ -313,6 +328,7 @@ class Encoder: self.__typeMap = typeMap def __call__(self, value, defMode=1, maxChunkSize=0): + debug.logger & debug.flagEncoder and debug.logger('encoder called in %sdef mode, chunk size %s for type %s, value:\n%s' % (not defMode and 'in' or '', maxChunkSize, value.__class__.__name__, value.prettyPrint())) tagSet = value.getTagSet() if len(tagSet) > 1: concreteEncoder = explicitlyTaggedItemEncoder @@ -322,13 +338,16 @@ class Encoder: elif tagSet in self.__tagMap: concreteEncoder = self.__tagMap[tagSet] else: - baseTagSet = value.baseTagSet - if baseTagSet in self.__tagMap: - concreteEncoder = self.__tagMap[baseTagSet] + tagSet = value.baseTagSet + if tagSet in self.__tagMap: + concreteEncoder = self.__tagMap[tagSet] else: - raise Error('No encoder for %s' % value) - return concreteEncoder.encode( + raise Error('No encoder for %s' % (value,)) + debug.logger & debug.flagEncoder and debug.logger('using value codec %s chosen by %r' % (concreteEncoder.__class__.__name__, tagSet)) + substrate = concreteEncoder.encode( self, value, defMode, maxChunkSize ) + debug.logger & debug.flagEncoder and debug.logger('built %s octets of substrate: %s\nencoder completed' % (len(substrate), debug.hexdump(substrate))) + return substrate encode = Encoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/cer/__init__.py b/libs/pyasn1/codec/cer/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/cer/__init__.py +++ b/libs/pyasn1/codec/cer/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/cer/decoder.py b/libs/pyasn1/codec/cer/decoder.py index 71395d22..9fd37c13 100644 --- a/libs/pyasn1/codec/cer/decoder.py +++ b/libs/pyasn1/codec/cer/decoder.py @@ -7,22 +7,25 @@ from pyasn1 import error class BooleanDecoder(decoder.AbstractSimpleDecoder): protoComponent = univ.Boolean(0) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - byte = oct2int(substrate[0]) + byte = oct2int(head[0]) + # CER/DER specifies encoding of TRUE as 0xFF and FALSE as 0x0, while + # BER allows any non-zero value as TRUE; cf. sections 8.2.2. and 11.1 + # in http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf if byte == 0xff: value = 1 elif byte == 0x00: value = 0 else: raise error.PyAsn1Error('Boolean CER violation: %s' % byte) - return self._createComponent(asn1Spec, tagSet, value), substrate[1:] + return self._createComponent(asn1Spec, tagSet, value), tail tagMap = decoder.tagMap.copy() tagMap.update({ - univ.Boolean.tagSet: BooleanDecoder(), + univ.Boolean.tagSet: BooleanDecoder() }) typeMap = decoder.typeMap diff --git a/libs/pyasn1/codec/der/__init__.py b/libs/pyasn1/codec/der/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/der/__init__.py +++ b/libs/pyasn1/codec/der/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/der/decoder.py b/libs/pyasn1/codec/der/decoder.py index 0f5a24ca..604abec2 100644 --- a/libs/pyasn1/codec/der/decoder.py +++ b/libs/pyasn1/codec/der/decoder.py @@ -2,4 +2,8 @@ from pyasn1.type import univ from pyasn1.codec.cer import decoder -decode = decoder.Decoder(decoder.tagMap, decoder.typeMap) +tagMap = decoder.tagMap +typeMap = decoder.typeMap +Decoder = decoder.Decoder + +decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/compat/__init__.py b/libs/pyasn1/compat/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/compat/__init__.py +++ b/libs/pyasn1/compat/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/compat/octets.py b/libs/pyasn1/compat/octets.py index d0303eaa..f7f2a29b 100644 --- a/libs/pyasn1/compat/octets.py +++ b/libs/pyasn1/compat/octets.py @@ -8,6 +8,7 @@ if version_info[0] <= 2: octs2ints = lambda s: [ oct2int(x) for x in s ] str2octs = lambda x: x octs2str = lambda x: x + isOctetsType = lambda s: isinstance(s, str) else: ints2octs = bytes int2oct = lambda x: ints2octs((x,)) @@ -16,3 +17,4 @@ else: octs2ints = lambda s: [ x for x in s ] str2octs = lambda x: x.encode() octs2str = lambda x: x.decode() + isOctetsType = lambda s: isinstance(s, bytes) diff --git a/libs/pyasn1/debug.py b/libs/pyasn1/debug.py new file mode 100644 index 00000000..c27cb1d4 --- /dev/null +++ b/libs/pyasn1/debug.py @@ -0,0 +1,65 @@ +import sys +from pyasn1.compat.octets import octs2ints +from pyasn1 import error +from pyasn1 import __version__ + +flagNone = 0x0000 +flagEncoder = 0x0001 +flagDecoder = 0x0002 +flagAll = 0xffff + +flagMap = { + 'encoder': flagEncoder, + 'decoder': flagDecoder, + 'all': flagAll + } + +class Debug: + defaultPrinter = sys.stderr.write + def __init__(self, *flags): + self._flags = flagNone + self._printer = self.defaultPrinter + self('running pyasn1 version %s' % __version__) + for f in flags: + if f not in flagMap: + raise error.PyAsn1Error('bad debug flag %s' % (f,)) + self._flags = self._flags | flagMap[f] + self('debug category \'%s\' enabled' % f) + + def __str__(self): + return 'logger %s, flags %x' % (self._printer, self._flags) + + def __call__(self, msg): + self._printer('DBG: %s\n' % msg) + + def __and__(self, flag): + return self._flags & flag + + def __rand__(self, flag): + return flag & self._flags + +logger = 0 + +def setLogger(l): + global logger + logger = l + +def hexdump(octets): + return ' '.join( + [ '%s%.2X' % (n%16 == 0 and ('\n%.5d: ' % n) or '', x) + for n,x in zip(range(len(octets)), octs2ints(octets)) ] + ) + +class Scope: + def __init__(self): + self._list = [] + + def __str__(self): return '.'.join(self._list) + + def push(self, token): + self._list.append(token) + + def pop(self): + return self._list.pop() + +scope = Scope() diff --git a/libs/pyasn1/type/__init__.py b/libs/pyasn1/type/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/type/__init__.py +++ b/libs/pyasn1/type/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/type/base.py b/libs/pyasn1/type/base.py index db31671e..40873719 100644 --- a/libs/pyasn1/type/base.py +++ b/libs/pyasn1/type/base.py @@ -120,7 +120,12 @@ class AbstractSimpleAsn1Item(Asn1ItemBase): def prettyIn(self, value): return value def prettyOut(self, value): return str(value) - def prettyPrint(self, scope=0): return self.prettyOut(self._value) + def prettyPrint(self, scope=0): + if self._value is noValue: + return '' + else: + return self.prettyOut(self._value) + # XXX Compatibility stub def prettyPrinter(self, scope=0): return self.prettyPrint(scope) diff --git a/libs/pyasn1/type/namedtype.py b/libs/pyasn1/type/namedtype.py index aa9c5678..48967a5f 100644 --- a/libs/pyasn1/type/namedtype.py +++ b/libs/pyasn1/type/namedtype.py @@ -60,12 +60,12 @@ class NamedTypes: tagMap = self.__namedTypes[idx].getType().getTagMap() for t in tagMap.getPosMap(): if t in self.__tagToPosIdx: - raise error.PyAsn1Error('Duplicate type %s' % t) + raise error.PyAsn1Error('Duplicate type %s' % (t,)) self.__tagToPosIdx[t] = idx try: return self.__tagToPosIdx[tagSet] except KeyError: - raise error.PyAsn1Error('Type %s not found' % tagSet) + raise error.PyAsn1Error('Type %s not found' % (tagSet,)) def getNameByPosition(self, idx): try: @@ -79,12 +79,12 @@ class NamedTypes: idx = idx - 1 n = self.__namedTypes[idx].getName() if n in self.__nameToPosIdx: - raise error.PyAsn1Error('Duplicate name %s' % n) + raise error.PyAsn1Error('Duplicate name %s' % (n,)) self.__nameToPosIdx[n] = idx try: return self.__nameToPosIdx[name] except KeyError: - raise error.PyAsn1Error('Name %s not found' % name) + raise error.PyAsn1Error('Name %s not found' % (name,)) def __buildAmbigiousTagMap(self): ambigiousTypes = () diff --git a/libs/pyasn1/type/namedval.py b/libs/pyasn1/type/namedval.py index 815e2d42..d0fea7cc 100644 --- a/libs/pyasn1/type/namedval.py +++ b/libs/pyasn1/type/namedval.py @@ -15,10 +15,10 @@ class NamedValues: name = namedValue val = automaticVal if name in self.nameToValIdx: - raise error.PyAsn1Error('Duplicate name %s' % name) + raise error.PyAsn1Error('Duplicate name %s' % (name,)) self.nameToValIdx[name] = val if val in self.valToNameIdx: - raise error.PyAsn1Error('Duplicate value %s' % name) + raise error.PyAsn1Error('Duplicate value %s=%s' % (name, val)) self.valToNameIdx[val] = name self.namedValues = self.namedValues + ((name, val),) automaticVal = automaticVal + 1 diff --git a/libs/pyasn1/type/tag.py b/libs/pyasn1/type/tag.py index 0cf67ebd..1144907f 100644 --- a/libs/pyasn1/type/tag.py +++ b/libs/pyasn1/type/tag.py @@ -18,7 +18,7 @@ class Tag: def __init__(self, tagClass, tagFormat, tagId): if tagId < 0: raise error.PyAsn1Error( - 'Negative tag ID (%s) not allowed' % tagId + 'Negative tag ID (%s) not allowed' % (tagId,) ) self.__tag = (tagClass, tagFormat, tagId) self.uniq = (tagClass, tagId) diff --git a/libs/pyasn1/type/tagmap.py b/libs/pyasn1/type/tagmap.py index 53e1791a..7cec3a10 100644 --- a/libs/pyasn1/type/tagmap.py +++ b/libs/pyasn1/type/tagmap.py @@ -28,7 +28,7 @@ class TagMap: def clone(self, parentType, tagMap, uniq=False): if self.__defType is not None and tagMap.getDef() is not None: - raise error.PyAsn1Error('Duplicate default value at %s' % self) + raise error.PyAsn1Error('Duplicate default value at %s' % (self,)) if tagMap.getDef() is not None: defType = tagMap.getDef() else: @@ -37,7 +37,7 @@ class TagMap: posMap = self.__posMap.copy() for k in tagMap.getPosMap(): if uniq and k in posMap: - raise error.PyAsn1Error('Duplicate positive key %s' % k) + raise error.PyAsn1Error('Duplicate positive key %s' % (k,)) posMap[k] = parentType negMap = self.__negMap.copy() diff --git a/libs/pyasn1/type/univ.py b/libs/pyasn1/type/univ.py index cb4f49b7..9cd16f8a 100644 --- a/libs/pyasn1/type/univ.py +++ b/libs/pyasn1/type/univ.py @@ -69,13 +69,18 @@ class Integer(base.AbstractSimpleAsn1Item): def prettyIn(self, value): if not isinstance(value, str): - return int(value) + try: + return int(value) + except: + raise error.PyAsn1Error( + 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) + ) r = self.__namedValues.getValue(value) if r is not None: return r try: return int(value) - except ValueError: + except: raise error.PyAsn1Error( 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) ) @@ -224,14 +229,14 @@ class BitString(base.AbstractSimpleAsn1Item): return tuple(r) else: raise error.PyAsn1Error( - 'Bad BIT STRING value notation %s' % value + 'Bad BIT STRING value notation %s' % (value,) ) else: for i in value.split(','): j = self.__namedValues.getValue(i) if j is None: raise error.PyAsn1Error( - 'Unknown bit identifier \'%s\'' % i + 'Unknown bit identifier \'%s\'' % (i,) ) if j >= len(r): r.extend([0]*(j-len(r)+1)) @@ -528,7 +533,7 @@ class Real(base.AbstractSimpleAsn1Item): ) if value[1] not in (2, 10): raise error.PyAsn1Error( - 'Prohibited base for Real value: %s' % value[1] + 'Prohibited base for Real value: %s' % (value[1],) ) if value[1] == 10: value = self.__normalizeBase10(value) @@ -648,7 +653,7 @@ class SetOf(base.AbstractConstructedAsn1Item): def _verifyComponent(self, idx, value): if self._componentType is not None and \ not self._componentType.isSuperTypeOf(value): - raise error.PyAsn1Error('Component type error %s' % value) + raise error.PyAsn1Error('Component type error %s' % (value,)) def getComponentByPosition(self, idx): return self._componentValues[idx] def setComponentByPosition(self, idx, value=None, verifyConstraints=True): @@ -924,9 +929,9 @@ class Choice(Set): return self._componentValues[self._currentIdx] >= other return NotImplemented if sys.version_info[0] <= 2: - def __nonzero__(self, other): return bool(self._componentValues) + def __nonzero__(self): return bool(self._componentValues) else: - def __bool__(self, other): return bool(self._componentValues) + def __bool__(self): return bool(self._componentValues) def __len__(self): return self._currentIdx is not None and 1 or 0 diff --git a/libs/pyutil/_version.py b/libs/pyutil/_version.py index 617d2205..376b2b9b 100644 --- a/libs/pyutil/_version.py +++ b/libs/pyutil/_version.py @@ -6,7 +6,7 @@ # pyutil.version_class for a description of what the different fields mean. __pkgname__ = "pyutil" -verstr = "1.9.3" +verstr = "1.9.7" try: from pyutil.version_class import Version as pyutil_Version __version__ = pyutil_Version(verstr) diff --git a/libs/pyutil/benchutil.py b/libs/pyutil/benchutil.py index 3e773a63..6c286346 100644 --- a/libs/pyutil/benchutil.py +++ b/libs/pyutil/benchutil.py @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -21,10 +21,10 @@ the second, e.g.: >>> rep_bench(fib, 25, UNITS_PER_SECOND=1000) best: 1.968e+00, 3th-best: 1.987e+00, mean: 2.118e+00, 3th-worst: 2.175e+00, worst: 2.503e+00 (of 10) -The output is reporting the number of milliseconds that executing the function -took, divided by N, from ten different invocations of fib(). It reports the -best, worst, M-th best, M-th worst, and mean, where "M" is the natural log of -the number of invocations (in this case 10). +The output is reporting the number of milliseconds that executing the +function took, divided by N, from ten different invocations of +fib(). It reports the best, worst, M-th best, M-th worst, and mean, +where "M" is 1/4 of the number of invocations (in this case 10). 2. Now run it with different values of N and look for patterns: @@ -74,10 +74,12 @@ and the main function is to make them be methods of the same object, e.g.: 4. Things to fix: - a. I used to have it hooked up to use the "hotshot" profiler on the code being - measured. I recently tried to change it to use the newer cProfile profiler - instead, but I don't understand the interface to cProfiler so it just gives an - exception if you pass profile=True. Please fix this and send me a patch. + a. I used to have it hooked up to use the "hotshot" profiler on the + code being measured. I recently tried to change it to use the newer + cProfile profiler instead, but I don't understand the interface to + cProfiler so it just gives an exception if you pass + profile=True. Please fix this and send me a patch. xxx change it to + statprof b. Wouldn't it be great if this script emitted results in a json format that was understood by a tool to make pretty interactive explorable graphs? The @@ -122,7 +124,7 @@ def mult(a, b): except TypeError: return to_decimal(a) * to_decimal(b) -def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): +def rep_bench(func, n, runtime=1.0, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME (wall-clock time) has already elapsed (unless MAXTIME is None). @@ -130,33 +132,43 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p @param quiet Don't print anything--just return the results dict. """ assert isinstance(n, int), (n, type(n)) + global worstemptymeasure + emsta = clock() + do_nothing(2**32) + emstop = clock() + empty = emstop - emsta + if empty > worstemptymeasure: + worstemptymeasure = empty + if (worstemptymeasure*2) >= runtime: + raise BadMeasure("Apparently simply invoking an empty Python function can take as long as %0.10f seconds, and we were running iterations for only about %0.10f seconds. So the measurement of the runtime of the code under benchmark is not reliable. Please pass a higher number for the 'runtime' argument to bench_it().") + startwallclocktime = time.time() - tls = [] # elapsed time in seconds + tls = [] # (elapsed time per iter in seconds, iters) bmes = [] while ((len(tls) < MAXREPS) or (MAXREPS is None)) and ((MAXTIME is None) or ((time.time() - startwallclocktime) < MAXTIME)): if initfunc: initfunc(n) try: - tl = bench_it(func, n, profile=profile, profresults=profresults) + tl, iters = bench_it(func, n, runtime=runtime, profile=profile, profresults=profresults) except BadMeasure, bme: bmes.append(bme) else: - tls.append(tl) + tls.append((tl, iters)) if len(tls) == 0: raise Exception("Couldn't get any measurements within time limits or number-of-attempts limits. Maybe something is wrong with your clock? %s" % (bmes,)) - sumtls = reduce(operator.__add__, tls) + sumtls = sum([tl for (tl, iters) in tls]) mean = sumtls / len(tls) tls.sort() - worst = tls[-1] - best = tls[0] - _assert(best > worstemptymeasure*MARGINOFERROR, "%s(n=%s) took %0.10f seconds, but we cannot measure times much less than about %0.10f seconds. Try a more time-consuming variant (such as higher n)." % (func, n, best, worstemptymeasure*MARGINOFERROR,)) + worst = tls[-1][0] + best = tls[0][0] + m = len(tls)/4 if m > 0: - mthbest = tls[m-1] - mthworst = tls[-m] + mthbest = tls[m-1][0] + mthworst = tls[-m][0] else: - mthbest = tls[0] - mthworst = tls[-1] + mthbest = tls[0][0] + mthworst = tls[-1][0] # The +/-0 index is the best/worst, the +/-1 index is the 2nd-best/worst, # etc, so we use mp1 to name it. @@ -196,26 +208,22 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=1.0, profile=False, profresults="pyutil-benchutil.prof"): if profile: - st = clock() - cProfile.run('func(n)', profresults) - sto = clock() + raise NotImplementedException() else: + iters = 0 st = clock() - func(n) + deadline = st + runtime sto = clock() + while sto < deadline: + func(n) + iters += 1 + sto = clock() timeelapsed = sto - st - if timeelapsed <= 0: - raise BadMeasure(timeelapsed) - global worstemptymeasure - emsta = clock() - do_nothing(2**32) - emstop = clock() - empty = emstop - emsta - if empty > worstemptymeasure: - worstemptymeasure = empty - return timeelapsed + if (timeelapsed <= 0) or (iters == 0): + raise BadMeasure((timeelapsed, iters)) + return (timeelapsed / iters, iters) def bench(func, initfunc=None, TOPXP=21, MAXREPS=5, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", outputjson=False, jsonresultsfname="pyutil-benchutil-results.json", UNITS_PER_SECOND=1): BSIZES = [] diff --git a/libs/pyutil/benchutil.py~ b/libs/pyutil/benchutil.py~ index 3ec323eb..a33111e8 100644 --- a/libs/pyutil/benchutil.py~ +++ b/libs/pyutil/benchutil.py~ @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -104,6 +104,24 @@ def makeg(func): func() return blah +def to_decimal(x): + """ + See if D(x) returns something. If instead it raises TypeError, x must have been a float, so convert it to Decimal by way of string. (In Python >= 2.7, D(x) does this automatically. + """ + try: + return D(x) + except TypeError: + return D("%0.54f" % (x,)) + +def mult(a, b): + """ + If we get TypeError from * (possibly because one is float and the other is Decimal), then promote them both to Decimal. + """ + try: + return a * b + except TypeError: + return to_decimal(a) * to_decimal(b) + def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME @@ -144,12 +162,12 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p # etc, so we use mp1 to name it. mp1 = m+1 res = { - 'worst': (worst*UNITS_PER_SECOND)/n, - 'best': (best*UNITS_PER_SECOND)/n, + 'worst': mult(worst, UNITS_PER_SECOND)/n, + 'best': mult(best, UNITS_PER_SECOND)/n, 'mp1': mp1, - 'mth-best': (mthbest*UNITS_PER_SECOND)/n, - 'mth-worst': (mthworst*UNITS_PER_SECOND)/n, - 'mean': (mean*UNITS_PER_SECOND)/n, + 'mth-best': mult(mthbest, UNITS_PER_SECOND)/n, + 'mth-worst': mult(mthworst, UNITS_PER_SECOND)/n, + 'mean': mult(mean, UNITS_PER_SECOND)/n, 'num': len(tls), } @@ -178,7 +196,10 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=0.1, profile=False, profresults="pyutil-benchutil.prof"): + """ + runtime is how many seconds to + """ if profile: st = clock() cProfile.run('func(n)', profresults) diff --git a/libs/pyutil/data/wordlist.txt b/libs/pyutil/data/wordlist.txt new file mode 100644 index 00000000..e1048b99 --- /dev/null +++ b/libs/pyutil/data/wordlist.txt @@ -0,0 +1,7248 @@ +fawn +yellow +four +prefix +payoff +scold +outwit +lore +lord +swivel +deli +pigment +foul +fur +disturb +prize +broiler +wooden +satchel +crotch +fritter +charter +tired +miller +bacon +second +tether +ruthless +thunder +fossil +succumb +cull +specialist +hero +avert +herb +splinter +here +herd +china +dogwood +cult +shriek +chink +pancreas +robin +neurologist +climber +diplomat +golden +gridiron +lengthen +summons +remnant +stern +unit +spoke +exhort +statesmanship +music +bedrock +passport +strike +teaspoon +relay +relax +hurt +meteorologist +glass +hurl +hole +hold +unpack +sweeten +blade +locker +locket +plunger +wand +wane +unjust +household +digit +malign +caution +want +rayon +hog +hoe +travel +copious +cutback +revisit +how +hot +hop +cheetah +diagram +possum +modest +antonym +pigtail +revolt +alias +decoy +wing +squint +wine +feedback +misdemeanor +kickoff +foodstuff +butcher +dreamer +fir +bowlder +fix +fib +fig +fin +undercut +enrich +slate +interrupt +sixteen +silver +scholar +thyme +seamstress +debut +arrow +debug +volcano +burial +whim +concord +knockout +garment +allah +spider +crocus +turnip +yiddish +fortnight +allay +whir +whip +diction +smirk +mason +semiconductor +re +adapt +outburst +knit +scruff +silicon +miaow +thumbtack +shopper +wasp +wash +instruct +rhododendron +tango +master +architect +bitter +listen +wisdom +swish +sulphur +crawl +trek +peril +outlay +coward +tree +shower +pneumonia +sheen +acclaim +entail +girder +runner +spectrum +headland +increment +quay +dozen +kidnap +gripe +hum +greenback +tipi +matriarch +stirrup +object +toil +microsecond +mouth +addict +letter +fluster +drought +thriller +expound +singer +upend +grove +professor +camp +detriment +nineteenth +scream +marvel +bomb +reactor +heckler +ulcer +caper +layout +menu +bust +cougar +bush +bliss +rich +mend +rice +plate +pocket +cushion +fetish +relish +jaguar +boarder +pretzel +patch +hasten +respond +fair +heirloom +radium +radius +result +fail +crouch +clef +best +irk +yogurt +ire +wage +extend +vestment +souvenir +extent +wheelbarrow +carbon +debt +roller +accident +trickster +veer +disdain +cup +logic +genus +rehash +gopher +canyon +bewilder +chrome +onomatopoeia +advert +grapefruit +stadium +jackass +counterattack +life +retrospect +spit +worker +wish +lift +toboggan +chile +child +chili +spin +wildcat +dissect +employ +calcium +delicatessen +locksmith +letdown +player +elicit +eighteen +violin +doorman +specter +hone +toaster +honk +rebellion +split +bid +european +typhoid +boiler +ownership +supper +tuna +tune +furlough +noblewoman +unhook +abound +bellow +beset +plight +brandish +previous +ham +hag +hay +prison +falter +east +hat +quirk +birth +shadow +gangplank +remind +pavement +battlefield +attorney +right +old +creek +crowd +creed +crown +glove +billboard +creep +chorus +okra +bottom +circumvent +inhuman +fox +foe +fog +binder +yoke +slither +recollect +despair +rebut +eightieth +sob +sod +overshadow +honeymoon +overgrow +sop +sow +wrap +fabric +panorama +support +tame +avail +width +hothead +call +overhand +overhang +telegraph +offer +thesaurus +beech +squalid +safeguard +otter +duel +misinform +paprika +vanguard +pest +duet +proud +tournament +proven +exist +quintuplet +dealer +leer +floor +glacier +actor +flood +role +entomologist +sunset +smell +leek +intend +glutton +ointment +asterisk +taurus +intent +cleaver +entrust +windscreen +puss +lowdown +time +push +gown +chain +viaduct +skate +chair +midst +millisecond +ballet +uneven +vex +crater +oversight +jerk +ameba +embark +flora +mourn +knapsack +southpaw +exact +epic +judaism +tear +teas +teat +crustacean +subway +team +skewer +prevent +meadow +gremlin +attic +sigh +milligram +heavyweight +crescent +playpen +crackpot +melt +current +boost +abscond +gnaw +splice +address +brilliant +endow +queue +influx +love +radish +prefer +piranha +fake +instal +forefront +sky +homesick +turret +wicker +wicket +scope +prosecutor +wicked +afford +refrain +visual +appendix +behalf +mascot +lumberjack +pretend +descriptor +dispossess +stole +winter +savor +sputter +meddler +slush +spot +textual +date +suck +dove +pulley +stress +conscious +bluster +wheelchair +quadrant +mango +so +skirmish +truce +drunken +archeologist +footstep +yearn +jig +disconnect +thumb +accordion +nearsighted +councillor +hubbub +suspicion +thump +apron +civilian +insomnia +nation +amulet +twilight +ketchup +handiwork +revert +fisherman +quarter +quartet +receipt +fireproof +breakthrough +sponsor +troll +naked +canvas +onrush +trauma +formula +dumbfound +million +envelop +vicious +disrespect +mime +plea +byte +workmanship +punk +wrong +ostrich +punt +footwear +neglect +gunshot +potter +one +reopen +chide +conifer +vote +paleontologist +languish +boulevard +wrath +convent +bite +extortion +shiver +draft +cite +starfish +shawl +artifact +snatch +antic +boyfriend +iceberg +rival +stammer +counselor +janitor +prospect +sac +greyhound +argument +alley +sad +say +borough +saw +handicraft +tulip +general +knead +zoo +note +take +destroy +printer +buffer +squalor +compress +buffet +crochet +knee +byway +lawn +enamel +blockhead +sale +cocoanut +wind +axe +salt +cobra +homespun +lotus +friction +bright +slot +slow +slop +unkind +gourd +transact +cloak +debunk +slog +hockey +slob +robe +clank +dissimilar +psychiatrist +clang +outlet +prime +artist +saliva +borrow +soloist +carrion +handcuff +primp +landlord +tortilla +where +xmas +vision +gout +gangster +cheesecloth +diver +bugler +mutton +plummet +bootleg +teacup +bureau +mope +vender +jumper +spars +screen +dome +supermarket +adept +jovial +spare +spark +quack +oust +fit +madcap +mane +flipper +backpack +twin +boar +supervisor +extinct +twig +boat +companionship +stretch +west +breath +reflex +gist +thousand +photon +cloudburst +turtleneck +former +jute +scarlet +straighten +spotlight +girth +brow +canon +dubious +monk +blab +fame +spunk +breakdown +hideaway +deft +barber +disown +booster +driftwood +veal +pewter +dimension +scholarship +summer +manifold +poach +disconcert +slime +rest +invalid +alarmist +mandolin +instrument +overthrow +stopwatch +haystack +joyful +sportsmanship +rejoin +dart +dark +brazier +snarl +traffic +cranium +vacuum +world +snare +dare +clan +clam +stranger +shutter +glamor +clay +claw +inter +kennel +clap +auditorium +obstruct +grub +potion +lobster +racial +endeavor +tote +tube +moslem +tuba +nook +exit +refer +zest +ration +leadership +standpoint +stone +ace +slender +meal +tumor +neighbor +act +mean +invert +braggart +homeless +wade +hypnotist +racquet +hew +burglar +her +gleam +glean +mindless +harpsichord +italic +hem +hen +defrost +epilog +pull +regimen +darken +wafer +rage +hooray +tripe +ruse +flirt +reprimand +whiz +torso +pulp +rust +ohm +gong +ad +fright +certain +epaulet +catchup +hoodlum +ay +ax +tranquil +jargon +slobber +cream +yoga +collector +abolish +tight +backgammon +congress +annex +slant +midget +brotherhood +slang +rostrum +neuter +thorn +groom +mask +kilogram +mash +mimic +mast +mass +ringworm +waiver +retch +gingham +influenza +consider +neigh +upkeep +taxicab +tinsel +to +tail +smile +norm +debit +baton +candid +salesperson +cobalt +strand +laud +pedant +sand +adjust +small +mammal +peon +ninetieth +plaid +past +burnish +gossip +canvass +healer +hick +offbeat +clock +section +succinct +method +contrast +full +hash +lobbyist +saleswoman +dramatist +backlash +brutal +prior +hamster +skyrocket +social +action +welder +raze +depart +vie +sherbet +regiment +captor +coercion +entrap +select +casket +enliven +petroleum +maltreat +pearl +sitter +morn +ballad +more +teen +teem +door +tester +signpost +nomad +doom +cunning +fatal +malt +chisel +patriarch +knocker +midstream +mall +learn +grope +male +stewardess +prompt +taunt +gallop +scab +accept +autumn +gallon +scar +rustler +condemn +huge +speedboat +fruition +cling +clink +plant +anoint +blotter +variant +unsound +plane +waver +flutter +pucker +wrench +trellis +patio +pant +instep +trade +paper +pang +brim +mislay +hearsay +buttercup +epoch +coarsen +bypass +motley +sucker +gadget +consign +imperil +skipper +harrow +nugget +fount +found +lantern +status +eyelash +clockwork +scribe +penicillin +lipstick +research +highway +bungler +belief +porcelain +bedlam +cockpit +loafer +suntan +acorn +riser +reproach +prefab +drivel +sicken +bumper +testament +clump +major +purport +limerick +number +feeder +slipper +footprint +florist +glitter +guess +guest +jet +swipe +vocalist +saint +gnash +relationship +tightwad +typhoon +mural +consult +grace +frock +getaway +vocal +video +defect +waft +pedestrian +graffito +caress +blond +gasket +sell +ballerina +ragamuffin +tarnish +spaghetti +self +trowel +poplar +brace +bobbin +kneecap +hypochondriac +blackboard +nasal +twine +raucous +virus +plan +wive +foyer +oyster +unequal +arson +covet +cover +barren +barrel +bulletin +chowder +golf +cruiser +affix +session +freight +impact +condor +writer +peculiar +condom +factor +downpour +dandelion +streamer +resent +actress +compass +banner +tumult +sojourn +caramel +enema +weaver +river +outlaw +prospectus +manger +set +creator +overwhelm +jade +sex +see +sea +contour +analog +project +urchin +fission +crossword +pickup +crosswalk +kneel +candor +mildew +hardship +disallow +incident +dividend +pagan +scatterbrain +lass +last +thou +opal +feminist +amoeba +lash +whole +load +loaf +electrician +pendulum +bell +loam +loan +hollow +scallop +church +psychoanalyst +underlay +napalm +airfield +devil +filth +imbed +proprietor +veneer +firm +sweetheart +champion +fire +infect +upstart +fund +deport +hostess +straight +budget +error +outskirt +real +pound +moth +vow +chasm +vanish +chase +starlight +seek +shorten +wasteland +specimen +commune +snail +teeter +cigar +epithet +alert +opinion +stack +recent +expend +clime +person +sixtieth +crayfish +telegram +aroma +belabor +amp +demerit +sandal +goblet +chest +eager +horseradish +homeland +wrongdoer +input +limp +cordon +format +bureaucrat +quest +cataclysm +blackjack +falcon +abduct +flannel +spine +consensus +crescendo +spring +beckon +palm +pall +sight +curious +sprint +battalion +pale +gruel +benefit +religion +be +odor +agreement +carol +by +scepter +coexist +hatchet +sacrament +ambush +biennial +repair +contributor +next +span +sock +submit +custom +spay +suit +spar +spat +blueprint +perplex +poster +lint +slump +pastor +overbear +link +atom +line +up +slander +foist +hornet +insignia +genial +aerial +nationalist +haunt +char +chap +chat +parsec +breather +phantom +paradox +tuft +uranium +scrape +parakeet +swirl +freighter +tart +tedium +scapegoat +trouser +scrub +gardenia +hackney +lane +land +fighter +algorithm +scotch +age +feud +summit +walker +fresh +crowbar +rescuer +hello +essay +code +partial +serviceman +scratch +broaden +totem +soften +leggin +renown +prim +flashback +young +send +moor +tremor +garret +armament +garden +quadruplet +llama +precinct +wipe +magic +harbor +eve +anxious +race +rack +mishap +crook +croon +odd +ode +victor +index +yock +sauerkraut +apparatus +indian +proffer +bird +inspect +leg +punch +acquit +let +fifteen +vinegar +great +casino +screech +scatter +survey +insulin +grandchild +buss +popcorn +mussel +maker +grower +sire +disobey +causal +zip +archbishop +theme +aeon +eleven +doubt +yardstick +midday +pencil +babe +shipwreck +patrol +rubbish +central +hoard +pout +pour +thin +drill +coffin +cherubim +bent +pawn +process +lock +slim +high +slit +bend +slip +pelvis +martyr +trumpet +weaken +rhubarb +delay +blackhead +luster +stow +halter +singular +await +wristwatch +notebook +tier +marrow +hawk +autograph +tomato +counter +robot +element +writ +allot +allow +alloy +thigh +mute +insight +spatula +comma +mutt +warren +perfect +decay +shudder +garnet +derelict +prosper +python +belch +bat +launder +dock +snake +kiss +bar +cage +wrangler +truth +scorch +subset +bump +static +thirteen +mete +jagged +disco +tenth +wander +matrix +bag +fatherland +venom +czar +oblong +lob +shut +perish +tempo +graze +tempt +shun +embarrass +minstrel +chilli +mainland +spill +length +stickler +scare +scarf +manuscript +scene +cobweb +owner +scent +prank +lop +opossum +sergeant +spaceship +painful +stomach +chagrin +vouch +rotunda +haven +steel +wet +bother +aggressor +psalm +disband +unman +steep +torrent +misunderstand +beggar +viewer +partnership +correspond +tonight +receptionist +fourteenth +mischief +depict +soak +bacterium +bassoon +hammer +adjunct +lilt +soap +soar +calculus +manor +raindrop +cipher +vise +segment +fervent +instil +locust +enlist +soprano +fiasco +brew +fact +bring +brine +bedroom +rough +asylum +trivial +brink +redirect +disillusion +planter +jay +jaw +jar +jam +tape +bourbon +flinch +hope +jackpot +move +familiar +scorn +sinus +wring +antagonist +smash +shaver +summon +stuff +rein +withstand +pronoun +packer +frame +packet +bellhop +airmail +dungeon +wire +mien +partisan +unravel +piston +pistol +email +browbeat +fetich +physicist +courtyard +lawsuit +tantrum +drum +quitter +ramp +drug +doorway +puff +roughen +medallion +revamp +migrant +distil +javelin +indict +chromium +lectern +mailman +gondola +quaver +blatant +feather +ballast +sheepish +crisscross +federalist +mannequin +altruism +banish +laser +runway +bathtub +maul +groin +ripe +lush +site +lust +mockingbird +tenor +passbook +ransom +tattoo +inquest +terrorist +buffoon +outbreak +android +balm +ball +balk +dusk +fiesta +bale +bald +dust +broccoli +mosaic +audit +off +shotgun +polyp +command +diphtheria +audio +maggot +compel +glut +glue +rambler +web +generous +clergyman +wee +wed +arrest +crack +scoundrel +government +chancellor +crux +haul +cedar +desk +password +recurs +placenta +crisp +onion +resin +alkali +stagger +imprison +nymph +sprain +overcast +foray +habitat +thief +daylight +flush +wisecrack +ballot +transport +henchman +disbelief +hoarder +avoid +disk +doer +passion +saucepan +stairway +putt +drift +ornithologist +stage +iris +sister +adverb +peal +ingest +union +artefact +parsley +assess +lung +mere +muck +commission +caviar +watchman +stamina +much +function +funnel +cosmopolitan +frisk +shyness +grate +rectum +count +congresswoman +smooth +monument +problem +baptism +cordial +kowtow +sirloin +retina +inn +replica +ink +anesthesia +furl +sexual +saviour +behold +reckless +chum +monday +repeal +veil +vein +ghost +eon +rule +dynamo +torrid +pension +tryout +abhor +buoy +inning +dote +rapid +mansion +defraud +voter +spew +bludgeon +bike +daze +regal +chill +regalia +whack +ale +compassion +blanket +distort +mania +chauvinist +chapel +whisk +daydream +pinch +scalar +handout +roadblock +unblock +math +triumph +chew +paperback +phoney +speck +heliport +sabbath +horn +chef +aristocrat +panda +stardom +lizard +walkout +toll +crunch +dustpan +pursuit +paraffin +sorceress +hairdo +daughter +envoy +adopt +tankard +smoke +loincloth +bunker +anarchist +envious +sanatorium +infield +spigot +thrust +hindsight +total +bra +plot +plow +plop +sweater +gloss +ploy +insult +plod +knoll +beeswax +solicit +award +yard +tariff +overrun +word +err +crest +work +grovel +tinder +era +elbow +spendthrift +quiver +serpent +flunk +impair +liter +chameleon +sever +moron +disappoint +beach +pizza +fever +lad +ladder +lag +lab +lay +law +arch +cosmonaut +retort +greet +greek +green +south +worst +order +greed +salon +gumption +devote +muffin +misconduct +mayor +sheaf +avocado +valor +carton +shear +then +fragment +safe +break +band +bang +coffer +overprint +tzar +bank +bread +crock +gallows +lisp +iguana +schemer +transient +prawn +sled +flock +slew +hostel +burlap +network +diesel +fellowship +amethyst +marigold +barrier +veto +standard +stencil +lollipop +morass +drench +ticket +maniac +raisin +flawless +renew +sprig +regress +vanquish +kin +render +system +hamstring +chopper +disembark +comic +overs +neck +upshot +tourniquet +kiwi +emblem +luncheon +cereal +rebuff +minibus +guild +target +tavern +hike +medley +iota +guilt +iron +minus +pessimist +lull +ponder +strength +realm +widen +silversmith +latter +hamper +transmit +curfew +maiden +boxcar +sue +negro +phase +proverb +grave +deacon +swamp +bracket +aunt +rickshaw +oppress +mitten +crust +boyhood +nephew +toast +geyser +layman +geologist +predecessor +do +ardor +ecologist +roundabout +slack +rebirth +runt +rune +rung +crucifixion +steak +steal +steam +ghoul +reread +misdirect +christian +goulash +pastel +gentleman +cellist +contraband +drawl +accord +unfold +kitchen +cop +cot +cow +brat +excrement +ill +cob +brag +cod +cog +bran +coo +con +emporium +eyesight +tone +spear +royal +trunk +nonconformist +infirm +speak +charisma +scarecrow +warmth +leech +baud +hacksaw +millionth +hoist +spellbind +gracious +physician +inhibit +gnu +launcher +air +aim +ail +abrupt +thrash +aid +stink +have +sticker +sting +throat +brake +cone +hebrew +uplift +stint +descent +perform +descend +decibel +wheel +raid +fuss +nil +swell +hang +evil +hand +fuse +nip +nit +scenario +drip +ragged +client +mamma +kinship +indigo +photo +victim +extol +thyroid +exalt +shout +cognac +board +zillion +righteous +plasma +intercom +fusion +boxer +cape +retreat +cooler +night +flatter +born +rile +flatten +bore +orchid +cede +humor +peek +peel +pose +confer +peer +peep +chafe +foreskin +chaff +diner +coral +visa +banker +horizon +cherish +gingerbread +octopus +croak +faint +dilemma +tetanus +float +profession +bound +curios +sedan +loin +beet +piggyback +wag +bookend +wad +frill +sovereign +fight +gybe +way +wax +burro +war +fizz +peninsula +holdup +boon +true +reset +absent +nursemaid +smidgeon +maximum +crystal +veterinarian +emir +emit +aorta +flat +abstract +molt +flaw +postscript +subsist +prayer +cacao +face +mold +mole +stake +shrine +test +upholster +unwilling +frolic +shrink +heyday +chairperson +hemophilia +faze +affidavit +loyal +longshoreman +igloo +concept +matron +consul +fulcrum +datum +horseback +supplement +toothpick +varnish +grape +zone +mallet +flask +graph +hump +flash +manicurist +glad +rhythm +tusk +terror +idealist +southwest +brown +congest +kitten +blast +brows +ophthalmologist +gun +gum +gut +guy +diarrhoea +upper +brave +regret +bravo +thinker +cost +helpless +tempest +cargo +appear +economist +menthol +medal +havoc +uniform +tarantula +appeal +caveat +genes +gawk +jester +disclaim +goldfish +teacher +buck +merriment +fogey +precursor +plotter +eke +disavow +trial +convertor +pillow +bolt +extra +paragon +keeper +marker +firearm +market +streetcar +prove +subvert +live +matador +club +cluck +clue +logarithm +prepay +graphic +slogan +car +cap +caw +cat +meow +can +cab +heart +hears +chip +sake +bridesmaid +abort +chin +chic +serum +bankrupt +freezer +write +lobe +storeroom +criterion +entrench +afternoon +product +dive +southern +bawl +motorway +pave +drastic +flourish +crepe +grandson +explicit +offend +barnyard +forfeit +haircut +ledger +brain +nitrogen +cold +braid +ethic +willow +theorem +window +artisan +factual +tiara +gizzard +nought +halt +fling +nod +rake +overcrowd +dishearten +hale +half +recap +courtship +taillight +provision +discuss +halo +wont +concerto +servant +drop +domain +supplant +year +operand +wavelength +happen +album +accomplish +space +thirst +rational +thong +carp +cart +virtuoso +quart +rebel +marina +prospector +card +care +fungus +tomahawk +british +honest +nonprofit +profess +blind +madam +blink +rink +rind +ring +drove +tomorrow +size +sheep +sheer +sheet +silent +bookmark +breed +callous +traction +checker +tragic +heartburn +friend +pomp +courier +that +peck +scalpel +rugged +recruit +magnesium +optimist +extinguish +angel +slay +slat +premier +slap +racetrack +slam +anger +breakfast +recover +slab +upbeat +veteran +shore +snout +siesta +begin +prick +halibut +price +foothold +dream +tooth +aerosol +washcloth +fifth +ground +gnat +snack +ratio +stair +proportion +jolt +stain +juror +shrill +pumpkin +cannon +loath +stroll +leather +thermal +husband +druggist +concert +burst +spore +whitewash +unfit +staunch +sport +incisor +concern +crawfish +glaze +complexion +import +clench +pluck +blame +broil +impromptu +whisker +guffaw +pertain +priestess +temper +aura +comet +evict +adroit +dispatch +exploit +semicolon +lioness +harmless +rebuild +toss +sisterhood +textbook +bloodhound +crumb +these +trick +scum +cherub +fool +marksman +zenith +mucus +soil +agnostic +laggard +bias +eras +bestial +beaver +waterway +petunia +helium +develop +media +pester +poetic +document +sweeper +finish +thunderbolt +foal +foam +cymbal +fruit +volley +trawler +smelt +quartz +theater +framework +patchwork +demean +taxi +livestock +fester +battleship +typhus +neon +touch +speed +death +refurbish +treatment +baloney +momentum +lade +ream +hover +frown +spectacular +larva +read +ruler +swig +leapfrog +earnest +detract +stunt +execution +reap +hovel +rear +postcard +incest +roll +engross +oblivion +output +downward +falsehood +laugh +verbal +landslid +squirm +garland +putter +cleanser +deficit +squirt +wheat +sadden +throb +sixth +tuition +strainer +bazaar +throw +comparison +placard +hiatus +chop +fell +wolf +parson +chow +ruff +assassin +processor +heater +outlook +earring +watchdog +your +restless +stare +grater +log +area +start +stealth +low +lot +wigwam +groan +pitcher +rump +posterior +recoil +omelet +lymph +thaw +corduroy +wholes +hire +fraud +default +bucket +draftsman +cornmeal +gibberish +charlatan +scanner +pickax +sheath +mover +antenna +housework +valid +colt +you +houseboat +poor +polar +poop +peat +pear +peas +overreact +podium +peak +pool +fiscal +assert +moonlight +repay +forswear +mourner +skeleton +breadth +groundwork +angler +month +unrest +bequeath +carpet +corps +gymnast +foster +spearhead +fervor +fountain +washroom +horror +verb +minded +heaven +ceaseless +saboteur +tendril +blackout +smock +homonym +resound +exodus +casual +bomber +protractor +lurch +milk +turmoil +vet +excess +strong +arena +divisor +noticeboard +outgrowth +vegetarian +whine +soldier +amount +base +trainer +put +haemoglobin +seventeenth +taker +helper +pup +titbit +assessor +chestnut +yuck +suction +grill +nine +parasol +transcend +pusher +boycott +archipelago +tract +phrase +magenta +frostbit +sheikh +warhead +spreadsheet +lyre +snippet +reject +gash +circuit +rude +sneak +denial +gasp +reelect +undergo +figment +overnight +ego +dread +egg +lynch +earthworm +help +reservoir +slouch +auburn +reclaim +preempt +soot +helm +hell +clarinet +limelight +prowler +lateral +heron +astonish +forbear +dim +food +musket +terrain +vomit +foot +stopper +holiday +payer +twelfth +bless +radial +trailer +pamper +thirteenth +talisman +event +magnet +vertigo +wedlock +teak +publish +eardrum +sustain +shrivel +outrun +ass +pun +drink +bass +dirt +dung +dune +cornstarch +reason +heroin +ask +ash +turnout +bask +bash +pus +launch +curtsey +round +bridegroom +caption +liaison +heartbeat +blush +assign +arsenal +demagog +elder +effortless +mist +miss +blossom +minion +station +expand +fingernail +scheme +banana +merciless +slosh +behind +bowel +trapper +boredom +sign +leotard +tunic +lament +ouster +hyaena +bride +bobsled +currant +toward +weapon +phobia +chipmunk +yowl +null +sensual +lid +lie +koala +cave +lip +useless +honeycomb +popular +quota +plunder +mace +watt +smoulder +clear +cleat +succor +clean +skein +blend +humid +phenomenon +cowgirl +flicker +sheik +crayon +copyright +paranoid +scooter +less +ikon +ampersand +waterproof +custodian +outlast +strut +strum +basement +chimney +monogram +fluff +chasten +geld +courteous +cramp +backtrack +grey +close +despatch +bandwagon +haddock +aqueduct +wow +grocer +won +woodchuck +wok +woe +stalk +bettor +wreath +philanthropist +spray +distinguish +zipper +garrison +delimit +eggplant +buzz +vault +protector +mausoleum +onward +oversleep +liken +proton +header +badminton +vessel +catwalk +stamp +damp +nape +damn +threaten +dame +alto +liven +exempt +deter +liver +hobnob +furrow +pact +loom +utmost +look +socialist +governor +rope +pace +while +smart +fleet +loot +loop +pack +petal +hoax +grant +belong +makeshift +discredit +grand +conflict +sham +hallway +optic +dime +bonus +banter +overweight +user +boa +grind +auditor +five +ambassador +chore +abstain +bearer +morsel +tick +botch +pier +carat +march +albino +game +jibe +banjo +optician +signal +manifest +eel +sleigh +sketch +creation +undress +yolk +urgent +impoverish +mustang +clothespin +fundamentalist +gild +simmer +slash +slapstick +run +rum +rub +booklet +benefactor +rug +stem +step +stew +taboo +subtract +rut +discus +shine +faith +pigpen +letup +portico +reappear +aye +congressman +block +foreswear +misbehavior +dude +within +gilt +pentagon +connector +syllabus +palomino +harem +frost +reed +womankind +reef +reek +reel +dull +skulk +swagger +chiropractor +ringlet +foresight +similar +psychopath +kidney +straitjacket +nab +sullen +nag +objection +obelisk +nap +department +nay +draw +resign +drag +tundra +drab +formal +horseplay +outing +orbit +depth +bribe +pinion +underbrush +cheerful +go +emboss +dunk +ammonia +compact +aquarium +baron +aria +stave +ameer +shack +geranium +warpath +epitaph +velour +schooner +virtuous +fond +wave +trough +cellular +tenet +nausea +stiff +asparagus +gender +button +hive +verdict +cloister +pilfer +picket +blitz +jump +booth +ardent +languor +cartel +click +poke +wallet +colonel +valet +cell +rotten +experiment +stanch +brooch +fifteenth +quell +weirdo +convert +chant +gent +repel +behead +ricksha +wig +daybreak +danger +win +clout +wit +ligament +infest +gimmick +wiz +cloud +metaphor +snoop +copperhead +crag +crab +cram +expressway +compatriot +mismatch +starter +salad +consort +ride +donut +archer +meet +control +wharf +halloween +glade +skirt +bandana +filament +circular +fare +farm +thunderstorm +canker +foment +corral +scoop +encyclopedia +scoot +agenda +american +cadet +sperm +gunman +hock +brood +broom +brook +walnut +youngster +skater +frond +auto +dike +relentless +snorkel +placid +stout +hands +front +refuel +muff +perfectionist +mode +upward +commonwealth +unwind +chunk +mollusc +seesaw +apartheid +mollusk +special +gallant +armor +confess +fathom +remiss +activist +playground +wick +obsess +umbrella +hopscotch +watermark +jilt +undo +advisor +sneer +princess +shrew +timer +keep +counterpart +elector +keel +mad +blogger +seventeen +bitch +drinker +equinox +dump +wrapper +chintz +attach +attack +jellyfish +final +beard +introvert +punish +feint +noun +plough +piecework +waist +photograph +spurn +cartoonist +beg +bed +bee +discolor +swindler +firework +spurt +bet +are +exhibit +fume +tabu +torment +sundown +portrait +need +border +rotor +bastard +sprinkler +gunner +jaunt +tactic +truck +detector +visor +brand +african +camper +rigor +awe +plumber +eject +spleen +urn +upset +snapshot +businessman +constrain +skunk +affair +indoor +crate +molest +cavort +sheriff +fiberglass +winner +wreak +rash +earner +rasp +gradual +fuel +sulfur +joint +fallout +buyer +endless +gray +tobacco +gust +ordain +topaz +nutmeg +she +contain +recoup +grab +conduct +widow +hardwood +shake +orphan +portend +computer +driveway +equip +portent +unearth +southward +tend +state +lug +tens +antler +tent +bleed +castoff +blinker +keg +bemoan +key +overhaul +thank +sniff +career +admit +spatter +plankton +jersey +christen +tuesday +poem +sari +tread +shaikh +yap +cent +quiz +yam +treat +yak +whisper +poet +fibber +spaniel +nuptial +brunch +debtor +novel +ripen +pandemonium +harden +neuron +hearten +steer +generic +balloon +speaker +northwest +blight +fireman +flesh +absorb +powwow +inbreed +spree +magician +rift +weld +surfboard +lunch +glower +well +drone +welt +underdog +discord +mistaken +dose +distant +laurel +skill +cinder +jackal +dais +ovum +snicker +stratum +possess +warrant +homework +canter +rafter +fate +burden +loss +clown +tablespoons +lose +divest +satirist +rote +page +backlog +shed +glare +twitter +hush +redress +home +peter +competitor +pinpoint +overlay +broad +overlap +hinder +individualist +journal +offset +instinct +smidgen +refuge +freedom +cleans +nightclub +rodeo +dominion +wallop +buzzard +cocoa +pointless +gerbil +snowplow +mastermind +museum +poinsettia +drumstick +mohair +jinx +backhand +cricket +north +gait +admonish +neutral +ho +technician +overflow +ear +eat +he +leper +limit +cello +display +wringer +twist +entreat +contest +meteor +finch +chemist +fodder +star +stay +stag +foil +stab +phosphorus +appoint +sphinx +shunt +broncho +atheist +atheism +portion +pardon +mackerel +demand +unfurl +protest +asian +captain +gamut +swab +swan +sinew +swat +swap +anticlimax +sway +loon +appal +void +vase +smack +govern +affect +hitch +vast +pilgrim +naturalist +vector +washout +whirlwind +quilt +crave +yack +cactus +quill +pander +wreck +orchestra +bikini +spokesman +haze +new +net +maverick +seventh +mew +onus +cardboard +interpret +taper +credit +harass +jamb +permit +prolog +menial +hunch +campaign +bayonet +moral +handlebar +overhear +ore +overheat +calk +overhead +calm +intrust +recommend +type +tell +calf +demon +wart +warp +warn +dogma +warm +pecan +adult +qualm +flotilla +ward +blindfold +confound +rook +room +candlestick +worth +bungalow +headway +root +defer +vodka +give +climax +assent +honey +surveyor +quail +freshen +polio +rib +stockyard +answer +abdomen +plank +coup +fracas +passageway +waterfront +lesbian +guerilla +attempt +third +maintain +capitalist +fetus +deck +keyboard +windshield +furlong +harmonica +crew +better +persist +pass +workout +microfilm +caterpillar +grammar +meat +mistrust +roast +side +bone +luck +caustic +aids +dawn +extract +jell +contend +velvet +gradient +open +crucial +content +reader +linear +whiff +bestow +mistress +needlework +steward +fleck +loud +skinflint +playwright +grade +hoop +hoot +buttock +hook +ditch +hoof +hood +hydrant +acquaint +spinach +historian +enthral +woodwind +brainwash +dwell +inferno +twister +gym +somewhat +gambler +symptom +preacher +affront +keyword +matter +loiter +mink +seep +quench +modern +mind +mine +ginger +seed +seem +churn +mint +unfasten +alibi +desist +chess +sleeper +quarterback +phonograph +chatterbox +regular +condominium +blacklist +don +alarm +impostor +dog +doe +solemn +digress +constrict +consumer +dot +hunger +visitor +probe +syntax +chord +sundial +northeast +explain +jailer +sugar +folder +inventor +edict +patter +smut +stop +coast +pincushion +watermelon +smug +earl +earn +peacock +bay +reload +bad +troop +cower +ban +stinger +linguist +enchant +attest +subject +snuff +scrap +sail +causeway +scram +baboon +warrior +triplet +vitamin +lotion +orthodontist +beautician +cousin +motto +sprinter +pate +typist +height +gusher +aftermath +arsenic +ether +accustom +tint +recur +three +erect +ting +chrysanthemum +trigger +interest +basic +basil +basin +idol +chug +mushroom +suppress +dismiss +deepen +encyclopaedia +unpopular +tank +affirm +tang +near +moratorium +neat +motorist +anchor +spawn +seven +cane +diaphragm +it +shame +jest +in +id +disappear +if +abscess +growl +sap +parish +make +kit +delight +squat +garlic +warden +unicorn +jealous +overt +bequest +kid +butter +romp +smoker +inherit +bedspread +diphthong +left +protocol +just +yen +unfair +psych +human +yes +cretin +yew +legion +character +wretch +save +opt +discreet +background +shoulder +nude +manual +pillar +dean +squander +deal +deaf +maxim +dead +revel +intern +dear +strife +sprawl +pail +collect +normal +councilor +flounder +bullfight +tartar +bold +statistician +burn +blackmail +cottontail +sift +protagonist +burp +burr +tartan +super +innuendo +crucifix +craftsman +commit +marshal +unsay +paunch +chimp +down +lieu +chime +initial +lampoon +editor +fraction +unseat +fork +undergrowth +form +fore +ford +diaper +overburden +analyst +fort +pavilion +whiskey +boomerang +cosmos +propaganda +shin +disciplinarian +classic +covert +sidestep +drive +gland +scrawl +fatherhood +ship +graft +vista +marxist +smidgin +excel +handed +venison +congeal +marxism +sling +faction +handicap +slink +felt +diet +parenthood +journey +reign +stoke +weekend +derail +billion +potato +jacket +gorilla +almanac +teeth +meander +befriend +proletariat +woodwork +skip +skit +invent +adjourn +mild +mile +skim +skin +mill +primer +proletarian +skid +surplus +seasick +misread +depend +swoon +father +countdown +deject +swoop +regatta +unburden +string +yeast +pathologist +merit +join +jettison +stiffen +hoorah +din +stapler +nectar +die +dig +democrat +noiseless +item +dip +blur +shave +thresher +villa +worm +slake +sunup +talker +fillet +suspect +drunkard +shoo +dwarf +dweller +wail +guardian +clerk +makeup +stallion +waif +detest +tangent +deceit +rue +wait +box +boy +cuckoo +shift +bow +dither +boo +raccoon +cyst +bob +nylon +bog +elect +plumb +kayak +surmount +transplant +saki +wealth +perk +visit +vineyard +somersault +sharpen +yoghurt +aspirin +labyrinth +curriculum +downtown +tandem +rigid +savior +effort +gnome +demolish +pageant +moccasin +melodrama +flu +soul +impel +soup +sour +claim +plaza +reflector +predict +agent +drawer +council +craze +pink +purr +arbor +tilt +clever +parch +pine +till +sunday +sword +tile +pathway +pint +map +mar +mat +may +gelatin +membership +mankind +tablecloth +grow +man +relinquish +aimless +hemlock +omen +tale +switch +jail +deposit +talc +unleash +basket +longhand +talk +shield +rabbi +moralist +lyric +pitch +solder +pointer +group +monitor +bedbug +maid +drummer +maim +mail +main +tonic +killer +shatter +minuet +safari +teller +rattler +outweigh +feverish +peasant +careless +rock +hijack +eyelid +latin +bookshop +poker +gavel +unlock +manifesto +girl +stitch +monolog +priest +dutch +blubber +sensor +correct +monster +zombi +vinyl +jaywalk +cough +underwear +waiter +buzzer +thing +registrar +blacksmith +think +frequent +first +lone +crib +long +extrovert +thoughtless +lap +autocrat +escort +daunt +mermaid +anus +yoghourt +memo +broadcast +butt +proofread +tractor +coconut +lick +piccolo +marijuana +dash +comedian +sulk +nazi +sherbert +stopgap +daredevil +acumen +squad +interior +channel +pain +trace +roster +track +acrid +zigzag +whizz +assault +billow +pair +synonym +napkin +typeset +scowl +voodoo +toucan +amir +shop +lexicon +shot +show +cornea +veranda +therapist +shoe +threshold +corner +label +cornet +enthusiast +fend +objector +flapjack +dice +plume +enough +syphon +black +consent +enthusiasm +fiendish +plump +get +straggler +stomp +midriff +slyness +gee +gibber +neckerchief +gem +disinherit +beseech +skull +businesswoman +yield +stupid +nostril +tallow +kernel +sear +eighteenth +seat +seam +seal +stigma +calendar +wonder +puma +parent +limber +ornament +forecast +gage +pump +august +foreword +slingshot +tacit +wednesday +gauntlet +childbirth +tug +tuck +trader +tour +tout +delirium +stretcher +cancer +spank +cancel +tub +mare +underworld +imp +undershirt +mark +mart +workshop +rancher +fiftieth +chalet +graveyard +squash +wake +sound +litterbug +epidermis +slumber +cock +strait +strain +sudden +protein +par +pat +harsh +paw +pay +woodland +same +heartbreak +pad +cotton +pal +pan +exhaust +oil +chloroform +munch +companion +foghorn +polygon +drain +vertebra +soundproof +outdoor +suitor +money +imprint +leeway +aspect +flavor +asthma +godchild +comradeship +forgo +pile +pill +grip +grit +mop +mow +moo +mom +mob +railroad +grim +grin +oxygen +server +chamber +nose +hallelujah +fulfil +sneaker +afflict +witchcraft +ascend +dole +ascent +spasm +gross +confirm +pioneer +inject +gladden +highbrow +linoleum +intravenous +knife +raincoat +broker +squall +bravado +racoon +opium +contagion +roar +island +insect +mixer +thrive +partizan +road +checkup +dagger +coupon +splint +empress +whiten +strip +uptown +skillet +paraphernalia +jigsaw +totalitarian +madden +tycoon +tripod +striker +shroud +hiccup +gore +spice +ember +magnolia +grouch +conqueror +embed +deadlock +affection +deer +deep +fellow +planetarium +deem +file +girlfriend +deed +hound +film +fill +tedious +selfish +personnel +hybrid +repent +drouth +field +prism +astronaut +fruitless +lapel +shelter +gander +unload +burrow +god +gangway +oral +motel +represent +forger +pheasant +forget +founder +suburban +dollar +rebind +zinc +implement +crimson +hideous +premium +parcel +straightforward +scout +scour +fall +bottleneck +pueblo +hinterland +dampen +dictatorship +flyover +neighborhood +clinch +gnarl +burger +zero +cottonwood +lawyer +further +misrepresent +ribbon +dial +skeptic +stool +trinket +stoop +plush +movement +girlhood +malaria +intrench +twang +mule +ranger +beacon +bigamist +capacitor +search +stupor +margin +airport +chipper +chieftain +narrow +fatten +quotient +wizard +caravan +transit +sadist +sadism +establish +dachshund +hobgoblin +eye +score +distinct +two +splash +libel +furor +wiper +diamond +brisk +opportunist +particular +disfavor +nineteen +town +hour +cluster +fast +dew +remain +paragraph +den +abandon +stubborn +shark +buttress +onslaught +share +sphere +minimum +rainstorm +attain +junket +sharp +botanist +siren +awkward +comfort +rapport +stir +bleat +whopper +blacken +blood +bloom +chute +coax +orchard +coat +doctor +spiteful +electron +blunder +mislead +coal +sect +infant +setback +radar +dough +lava +suffer +hundredth +sodium +bosom +late +speech +clamor +lath +lookout +goof +good +goon +detour +frigid +compound +detach +complain +bombard +headroom +countersign +token +monsoon +clamp +harm +hark +mental +hare +hard +beret +banquet +connect +fist +callus +hart +orient +harp +flower +creditor +trooper +pigeon +seaport +granola +print +foreground +assist +cockroach +pleasant +gig +faucet +prophet +omit +wither +pure +corkscrew +copper +perturb +barbarian +shoal +cups +jabber +razor +construct +paint +leash +statement +mama +hummingbird +catapult +pare +park +selector +glycerin +dentist +part +pars +youth +totter +plead +hangout +cistern +blanch +mountain +cardigan +couch +onset +build +zucchini +flute +salmon +chart +most +charm +moss +eskimo +organist +humanitarian +mammoth +pennant +squelch +weigh +standoff +sector +sparrow +fine +find +giant +merger +nervous +ruin +fiend +boulder +prowess +paperweight +cholera +express +ferret +cheapen +batter +breast +theft +silk +pellet +restart +silo +huff +common +archaeologist +printout +vine +lion +overeat +tender +expert +burner +myriad +stowaway +subscript +hypochondria +premiss +egotist +complement +figurehead +mailbox +pagoda +aircraft +sultan +archway +annual +foreign +point +smother +newborn +pamphlet +dancer +esophagus +platinum +pocketbook +secret +amnesia +reformat +finalist +ram +gay +gas +gap +holler +gal +understand +gag +chatter +gab +bile +politician +metro +solid +bill +holocaust +crutch +fun +lingo +manner +mystic +astound +rancor +eczema +ingrain +anaesthesia +sociologist +dishonor +ewe +seminar +corridor +neutron +itch +leopard +yesterday +moment +stripe +unveil +timid +task +werewolf +withdraw +landmark +grid +recant +spend +howl +darn +shape +snot +timber +rundown +impetus +cut +cur +pollster +snag +forbid +cue +punter +cub +snap +bridal +easter +brainstorm +bin +squawk +rebound +bib +judgement +redeem +bit +knock +disrepair +blemish +flue +fagot +flux +bamboo +foolish +walrus +sequin +transgress +often +back +impeach +extremist +mirror +lightning +scald +scale +culprit +pet +pelt +pew +pep +pen +scalp +lard +lark +peg +pea +patient +fed +megaton +constraint +oatmeal +drama +catnip +pediatrician +offshoot +obstetrician +gambit +maelstrom +tiff +clack +lesson +jockey +few +doll +errand +camera +handbook +forward +nougat +sideshow +showman +switchboard +calico +lifeguard +planet +jumbo +azalea +constant +flow +possessor +lye +curd +cure +curb +curl +prevail +stagecoach +leaflet +crypt +underweight +cellar +lend +tablespoon +papa +lens +cater +desert +statesman +mantel +notion +uterus +anguish +caribou +stroller +seaman +golfer +strew +parrakeet +peanut +welter +mower +rudder +compost +blaze +atlas +gravel +queen +dessert +rhyme +claustrophobia +surgeon +molar +verandah +knight +shock +crow +queer +crop +append +power +junior +anthem +access +clipboard +bachelor +intercept +sink +sing +roof +bode +implicit +remark +talent +conceit +resurrect +weekday +climb +honor +blizzard +liqueur +talon +oval +scandal +gateway +sermon +lime +patron +asteroid +butler +charcoal +trait +kiosk +thatch +trail +train +armadillo +harvest +fan +account +tunnel +carrot +obvious +smear +parkway +unread +fetch +employe +truism +sanitarium +teamster +boney +spruce +serial +contempt +hangar +lamb +lame +lamp +forest +goner +stock +roam +leukemia +bluff +terrier +fray +drape +bind +guru +liner +linen +chief +poacher +furious +furnish +disarm +meter +bunch +marshmallow +decorum +labor +kindergarten +heroism +willing +marsh +dad +junction +dab +dam +spell +swordfish +mention +courtroom +sonata +day +strive +flail +snowdrift +thrill +slacken +cider +memoir +sawdust +disregard +flair +thwart +jailor +jugular +pivot +cupboard +lentil +salesman +hippopotamus +matt +defend +rev +repress +stub +mate +barley +stud +smog +stun +red +franc +frank +hanker +fourteen +salami +likelihood +afterward +squadron +indent +mortar +skateboard +yarn +mortal +workbook +retain +retail +waitress +suffix +overshoot +ethnic +sack +brute +whoop +puppet +guidebook +vandal +pauper +ancient +monkey +bologna +laps +vulgar +vagina +hexagon +scant +liquor +cabin +sixteenth +gear +eavesdrop +bulldog +smolder +forethought +springboard +nun +bodyguard +prune +shrapnel +shampoo +linchpin +lover +anthropologist +tide +cavern +pedlar +countryman +waken +optimum +mix +parka +spinster +meek +dryness +hazel +eight +clamber +handbag +hoodwink +transcript +payment +gullet +gulley +gather +request +absurd +rendezvous +occasion +thicken +recess +kite +text +hamlet +traitor +industrialist +sidetrack +portfolio +floodlight +thicket +staff +communism +scorpion +madman +prolong +resubmit +satan +oppressor +communist +inferior +equilibrium +gumdrop +starch +beat +rush +bear +beam +bean +october +beak +bead +organ +ashtray +nutriment +eyebrow +motherhood +mascara +conform +showdown +infidel +racket +interview +reform +pattern +nebula +brunt +hammock +progress +tailspin +sorrow +stratagem +deliver +blackbird +boloney +exclaim +instant +joke +equal +kosher +swim +swallow +highland +guerrilla +glorious +wear +comment +vent +denim +overcoat +commend +vend +harpoon +manhood +citizenship +copier +newscast +gaze +teamwork +gulch +curtain +curtail +hyacinth +juggler +censor +goddess +bulk +bull +bulb +skew +carburetor +cypher +plain +homey +bray +kinfolk +bicker +dissent +squid +blimp +creak +prose +partner +inspector +lynchpin +portray +whirl +grinder +matchbook +defiant +anorak +tumbler +infer +whirr +tighten +pockmark +sauna +ion +grandstand +sunburn +judgment +retard +center +builder +pickpocket +thought +starboard +usual +coaster +humdrum +fingerprint +storey +clinic +interim +surpass +tough +earshot +flashlight +tong +flee +lupin +lake +bench +add +citizen +ado +crossroad +ravel +match +raven +cantaloup +punctual +newsstand +dryer +insert +flamingo +like +success +sofa +journalist +heed +arraign +chick +soft +heel +outfield +propel +fuze +hail +hair +convey +proper +paddock +novelist +shrug +shrub +slide +tureen +regain +pepper +hose +slight +host +panel +beaker +actual +socket +flake +preen +toadstool +pickaback +discard +tomb +tome +snitch +chronic +guard +esteem +custard +underpass +glimmer +gene +maze +globe +buy +bus +coke +sequel +but +bun +bum +bug +bud +embargo +woodsman +wise +ecosystem +debrief +flip +wisp +wist +trapezoid +condiment +plutonium +pin +garter +domino +circus +pie +pig +pit +campus +gush +oaf +cashew +oak +detail +virtual +detain +sewer +oar +redden +dresser +wallow +nutrient +godsend +yelp +baker +jab +hiker +pupil +yell +cookbook +vermin +sleek +sleet +sleep +liar +hate +trolley +sallow +tweet +glider +under +tweed +pride +merchant +lure +risk +rise +lurk +jack +confetti +anemia +school +parrot +enjoy +overdo +cracker +almond +direct +nail +street +monorail +ransack +blue +hide +worsen +poison +beater +supplier +dashboard +wink +even +pontoon +studio +path +crossbow +enrol +connoisseur +forum +ravish +auction +settler +mentor +midway +blowtorch +stray +straw +strap +cassino +would +phlegm +bayou +asset +spike +preview +musk +mush +saber +muse +grief +phone +muss +pouch +must +shoot +hutch +ma +ms +mr +machinist +fortress +quarrel +loosen +joyous +hemoglobin +dolphin +mayhem +attract +end +trill +keen +bunk +vagrant +rhinoceros +shred +toxin +gate +ancestor +dialect +moisten +kilowatt +mess +lump +mesh +sparkler +parallel +stronghold +splendid +spout +patent +enter +vapor +hedgehog +fetter +deform +clapper +sprout +over +bleach +mallard +oven +caster +digest +forehead +theologian +womanhood +comprehend +tramp +drawback +fade +croquet +tourist +plaster +roost +knighthood +monarch +rental +gloom +chuck +choir +prohibit +hanger +unscrew +gymnasium +poncho +truant +saturday +depress +goo +lair +dinosaur +nitwit +psychic +tonsil +gob +emphysema +nite +washer +resistor +carcass +rail +free +rain +acrobat +fret +harpist +ritual +filter +hopeless +soda +rang +accent +puck +rank +restrict +rant +sober +toy +their +sarcasm +top +tow +tot +fiction +ton +duress +toe +urban +murder +overdraw +tool +hearth +embellish +solicitor +toot +incur +western +nonpartisan +lather +prong +flame +mirth +countess +rag +donkey +fashion +handkerchief +ruckus +taint +raw +rat +rap +protract +spade +ray +snow +thorough +contact +hatch +snob +cleft +extravert +quicken +rider +evangelist +shallow +milkman +coil +coin +glow +interject +flop +metal +freeway +policewoman +flog +yank +chariot +bait +endear +saga +alight +random +sage +dupe +radio +rector +earth +bail +shellfish +spite +stanza +disgust +axiom +waltz +gees +watch +fluid +ultimatum +report +reconstruct +noon +spokesperson +egoism +public +erupt +pacifist +pummel +habit +wrest +nut +resist +corrupt +hourglass +mull +mud +mug +finger +mum +approach +wean +weak +contort +boss +toothbrush +southeast +larynx +devour +devout +censorship +newt +protect +irregular +fault +papyrus +facet +elf +smuggler +trust +bingo +bathroom +beef +legend +beer +spread +communion +loft +bladder +uncommon +craft +spearmint +catch +snipe +teapot +misfit +lessen +thousandth +referendum +pyramid +handrail +broth +lollypop +exterior +suggest +wound +overstep +utilitarian +complex +papaya +screw +pick +deflect +suburb +portal +postmark +tassel +ocean +mother +bough +bugger +rodent +shorthand +enlighten +elk +elm +moonbeam +flutist +kelp +misprint +teetotal +upturn +ramrod +dismount +quicksand +spanner +authorship +roach +befit +rumor +apart +ditto +gift +zeal +contradict +hunt +dishonest +zoom +mongrel +hunk +mosquito +hunchback +sanction +excerpt +curio +accost +usher +indirect +intellect +doorstep +nobleman +cooper +combat +letterhead +ice +rhino +newsprint +skylight +convict +christmas +splendor +cord +core +khaki +brawl +corn +brawn +cork +discount +shuck +plug +census +cowboy +plum +choke +surround +caulk +dinner +plus +alga +duke +abet +civic +civil +bath +engulf +cafeteria +art +scamper +transform +sunlight +forbad +virgin +gin +head +medium +amateur +heal +stereo +heat +hear +heap +raft +counsel +muster +bargain +bide +latch +adorn +trim +trio +forearm +cobbler +trip +no +tit +when +junta +tin +whet +tie +implant +depot +pseudonym +evergreen +cleric +toad +geneticist +bullet +navel +yacht +withhold +fasten +backward +coach +impression +rob +rod +focus +livelihood +snip +yokel +rot +discern +environment +aplomb +melon +prop +coop +impend +plantain +cook +cool +looney +level +brother +encroach +quick +lever +pork +drier +trend +bullfrog +pore +inland +voucher +takeoff +bake +port +colic +hymn +choral +postman +spire +theist +thresh +tormentor +humorist +water +fluke +entertain +witch +twentieth +tire +boast +catnap +blotch +cinnamon +prude +weird +tweak +brighten +touchdown +post +panacea +concoct +scan +handler +prey +today +chapter +conductor +altar +cashier +drown +dismal +inhabit +judo +conceal +flagship +hullabaloo +fauna +laughter +streak +overpass +sandbag +trump +stream +despot +stroke +cube +hydrogen +bigot +dress +vital +fourth +dope +ballroom +fascist +clone +scoff +fascism +birthday +apprenticeship +eighth +repeat +classroom +twitch +liquid +inform +reaper +lagoon +superscript +refund +rye +midnight +blare +worship +thermostat +apex +platform +farmer +meridian +cutter +underneath +conquer +fern +rescind +wagon +term +name +realist +opera +bunion +bullion +realism +ailment +torch +zebra +distributor +hysteria +hacker +concur +profit +middleman +gram +clover +hull +hulk +flyer +tuner +flare +highjack +motion +turn +place +swine +swing +turf +preach +childhood +origin +pelican +feign +suspend +insist +scollop +bobcat +array +peddler +given +afterthought +district +opus +trillion +plastic +assort +white +hue +hug +hub +cope +season +hut +enigma +naught +grunt +holder +wide +bewitch +spokeswoman +oath +powder +rend +froth +pro +ani +ant +rent +dragon +stolid +marathon +ideal +blunt +surf +sure +aspen +equestrian +tornado +freshman +librarian +bluebird +icon +latex +tendon +annul +seafood +later +koran +readjust +senior +slope +perch +convoy +cheat +cheap +trespass +hack +broach +hustler +trot +woolen +gulf +genius +gull +shimmer +crime +gulp +woof +wood +deign +wool +entrant +viewpoint +lighten +jazz +festoon +tailor +lighter +dye +homestead +reveal +aluminum +workman +joker +dumfound +bison +picnic +pane +vizor +prowl +optometrist +detect +crooked +review +spoons +hiss +smokestack +caucus +fearless +guitar +coma +comb +come +zodiac +isthmus +reaction +superstar +region +quiet +contract +railway +penal +adjoin +color +armchair +pot +period +pop +pole +colon +polo +pod +poll +runaway +turkey +hobo +schoolboy +tiger +padlock +hernia +careful +spirit +robber +pilot +case +shaft +amend +mount +cash +cask +cast +mound +ventriloquist +vest +exult +clutter +helmet +projector +author +alphabet +fender +bowl +check +macaroni +catfish +bellboy +hermit +week +sang +applaud +nest +driver +weed +director +petticoat +lute +puke +vowel +muffler +weep +cartoon +ranch +relief +model +reward +sinner +clod +clog +tip +kilt +ninth +clot +lavish +violent +kill +kiln +kilo +polish +satin +blow +blot +hint +rose +regent +except +blog +bloc +blob +hind +scrapbook +disrupt +impound +kingdom +blowout +sandman +mugger +towel +bracelet +snort +friar +tower +node +deduct +wombat +interlock +canteen +slice +mood +slick +legal +moon +moos +teardrop +moot +heir +porter +metropolis +quit +unmask +slaughter +quip +ok +oh +of +jeer +shrimp +pistachio +karat +stand +ox +doze +accredit +amber +tribe +vicar +polka +garb +spinal +forewarn +feudal +whaler +there +racism +strict +racist +valley +fish +gibe +relic +jug +regard +cabinet +castaway +strenuous +jut +terminus +feeler +grasp +grass +toilet +ruffian +cinema +frighten +lichen +encompass +bishop +incorrect +abyss +fiddler +heather +idiot +diarrhea +rubber +idiom +heathen +trash +stalwart +championship +symbol +cove +nucleus +serious +brass +wife +invest +derrick +treason +apparel +platter +all +lace +duplex +lack +spacecraft +disc +dish +follow +settlement +titter +wanton +thursday +program +neglig +woman +song +fat +roe +psychologist +retract +ultraviolet +awful +dapper +fad +induct +stimulus +list +trench +align +flick +ten +tea +tee +rate +design +chalk +what +sub +sun +sum +whimper +rascal +brief +overload +crush +version +pulpit +intersect +row +womb +lacquer +pumpernickel +backer +goodby +thrift +misinterpret +heifer +jogger +cataract +haemophilia +murmur +snug +snub +herring +proceed +tarpaulin +wield +hurray +rustic +quash +inlay +garnish +hurrah +minor +ladybug +wretched +flap +mire +protestor +stutter +flit +flag +stick +mellow +chaplain +berth +wrestler +plagiarist +searchlight +sunscreen +pond +court +goal +goad +goat +sandwich +okay +algebra +headrest +embalm +reflect +catalog +numb +short +ricochet +tsar +shade +waylay +mission +scientist +flaunt +reconnect +pretext +stride +islam +thirtieth +style +glide +pray +inward +wilder +abbey +mattress +resort +airstrip +bout +soccer +might +alter +return +hunter +underground +abacus +mathematician +liniment +policeman +refresh +tactless +friendship +weight +needless +duchess +falsetto +expect +inflict +wager +alcohol +disquiet +hilt +dugout +loll +health +hill +shipment +fiber +solvent +friday +differ +effect +disinfect +octagon +physic +teach +sidewalk +jew +blister +thread +threat +bushel +feed +dine +feel +sailor +revolution +least +blank +cigaret +idea +moan +script +gourmet +interact +grime +stork +swarm +storm +moat +syrup +store +mainstay +imperfect +option +hotel +fidget +king +kind +vial +kink +stall +cuff +foreleg +stale +restful +amass +cleaner +exert +strengthen +shrewd +bookworm +gale +gala +gall +remodel +smallpox +toughen +bacteria +chairman +donor +pianist +buff +gill +foreman +rapist +reckon +english +reach +react +nothing +quorum +hyena +amphibian +saloon +notch +scaffold +asphalt +memorandum +felon +font +anvil +firewood +betray +hip +shepherd +hit +deaden +reprint +him +adulthood +snowstorm +forego +stump +martyrdom +arc +bare +bard +bark +ark +arm +barn +blurt +parchment +various +plywood +nincompoop +solo +muslim +sole +outfit +succeed +inertia +orangutang +blazer +bandit +context +bond +cynic +sloth +flier +distress +chaperon +sweet +wastebasket +sweep +weasel +rave +shaykh +bolster +dub +overlook +dud +due +buttermilk +pa +watchword +brick +pi +flight +quintet +dropout +marihuana +cinch +temperament +instructor +heighten +toga +shove +batch +pitchfork +kick +behavior +incognito +lodger +bluegrass +sirup +rip +shamrock +rim +frantic +rig +rid +reprogram +chauffeur +shirt +kimono +viola +shirk +sliver +straightjacket +restraint +painless +throwback +cement +birch +robust +knack +lower +earmark +cheek +cheep +cheer +pollen +facial +vigilant +cabaret +continent +tablet +contractor +plateau +tuxedo +complaint +vendor +foreshadow +awaken +confront +uproar +distrust +breeder +hallmark +play +global +litter +wonderland +butterscotch +saucer +prow +seller +prom +prod +sag +perpendicular +tinker +raider +vivid +cautious +undertow +yawn +ordeal +militia +dialog +tomboy +conquest +momma +piteous +holster +vagabond +stench +canal +pundit +question +swill +parsnip +etch +filet +potassium +glamour +cloth +crank +usurp +delta +upright +crane +outpost +penguin +patriot +consist +apricot +caller +peppermint +husk +cartwheel +highlight +dill +freak +dismay +sublet +sagebrush +rainbow +lemon +riot +peach +grouper +saffron +nick +parlor +ferment +bandstand +mock +nice +mustard +chirp +meaning +vigil +vice +ocular +remit +pyre +buffalo +scroll +pervert +lean +alien +dispel +gang +theorist +gold +uphold +floss +breach +sniper +materialist +toenail +spool +spoon +spook +spoof +harlot +outdo +pleas +pleat +trawl +procession +fold +reunion +acid +folk +sandstorm +outsmart +acronym +relent +kangaroo +gloat +miser +cyclist +barb +survivor +guarantor +orangutan +armpit +shovel +duct +ensign +apt +volt +motor +duck +thick +redo +ape +use +fee +fen +frog +germ +modicum +fez +sort +parliament +porch +musician +impress +sore +rabbit +recount +penis +sculptor +annoy +topic +augment +critic +lumber +executor +proof +bittersweet +tap +tar +tax +villain +tag +condescend +tab +spa +silt +tan +rape +counterfeit +sir +sip +scuff +sit +tamper +six +outclass +occur +sic +carrier +goldsmith +toddler +panic +sin +defeat +tension +lesion +attend +tact +hazard +discomfort +tack +wrist +taco +footpath +aftereffect +light +arduous +schoolchild +sailboat +stamped +minnow +damsel +accompanist +hemp +tyrant +badger +glen +superior +inlet +sill +glee +nostalgia +flank +restrain +glisten +turban +redhead +bye +flex +crash +citrus +flour +flout +emerald +flea +republican +investor +successor +easel +footstool +profound +edit +feast +fuzz +trap +blacktop +cocoon +tray +lilac +mincemeat +interplay +our +proclaim +out +semen +tabloid +cocktail +sentiment +frontier +vehement +disarray +clatter +impart +plural +proviso +planner +utensil +tenement +pendant +gospel +tenant +greenhorn +tanker +zoologist +rivet +uproot +embryo +sew +bouquet +echo +bonnet +eleventh +synagog +salient +droop +unknown +galley +snore +anaemia +drool +boil +tidbit +shell +shelf +transistor +woo +diminish +persecutor +goblin +institution +kickback +frugal +brazen +yodel +laughingstock +clip +fowl +splatter +flunkey +blip +footwork +outstrip +disjoint +pallor +catholic +clove +rout +outward +bagel +lope +divert +trivia +pharmacist +divers +clash +petrol +siphon +filch +fortieth +class +clasp +fang +dens +dent +pipe +vernacular +gain +son +stove +sonnet +utter +chicken +feat +winch +dandruff +rioter +herald +piano +local +counsellor +vigor +sued +skimp +plaintiff +spud +watercolor +barter +bronco +spur +rite +ghetto +bisect +compliment +ascertain +sediment +view +unison +workbench +ebb +expel +hymnal +distract +violet +still +closet +superb +favor +viper +crude +torpedo +avow +jot +exam +amen +joy +foetus +job +spoil +jog +swift +memento +lifeboat +april +grain +commando +wall +hyphen +walk +respect +unclean +decent +trademark +tutor +reindeer +mike +nickel +cypress +penmanship +dearth +overturn +present +kerchief +corset +wilt +vanilla +priesthood +will +fingertip +wild +whirlpool +layer +mutant +motif +apprehend +rooster +lightweight +thug +thud +whore +headlight +cross +member +pediatrist +inch +grandeur +slave +diploma +outcast +beast +student +pedal +whale +collar +gutter +masochist +overwork +scissor +twirl +flint +outgrow +bandanna +rocker +cameo +rocket +camel +boot +wren +obtain +replenish +biologist +daub +distend +smite +now +panther +drunk +smith +hall +book +ski +enact +knob +sick +myth +know +knot +press +redesign +doughnut +loser +cutlet +vortex +clutch +exceed +setter +flagrant +birthmark +demeanor +growth +export +leaf +lead +leak +miner +leap +belt +leader +trout +obey +slur +mitt +slut +slum +pasta +mite +slug +throne +pike +throng +rare +linger +column +biscuit +fear +swear +sweat +udder +emperor +owl +outset +own +owe +weather +champ +brush +billfold +gape +rowboat +van +platoon +transfer +spiral +grotto +cliff +vat +nourish +catsup +unwrap +saunter +mutter +brassier +assail +tomcat +daffodil +nightgown +record +cake +faggot +maroon +boardwalk +abbot +counteract +limb +squirrel +mutual +glint +boor +percent +other +boom +branch +cutthroat +junk +mulch +june +squeak +squeal +extort +jewel +gynecologist +vane +sash diff --git a/libs/pyutil/fileutil.py~ b/libs/pyutil/fileutil.py~ deleted file mode 100644 index e37eb792..00000000 --- a/libs/pyutil/fileutil.py~ +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -""" -Futz with files like a pro. -""" - -import errno, exceptions, os, stat, tempfile - -try: - import bsddb -except ImportError: - DBNoSuchFileError = None -else: - DBNoSuchFileError = bsddb.db.DBNoSuchFileError - -# read_file() and write_file() copied from Mark Seaborn's blog post. Please -# read it for complete rationale: -# http://lackingrhoticity.blogspot.com/2009/12/readfile-and-writefile-in-python.html - -def read_file(filename, mode='rb'): - """ Read the contents of the file named filename and return it in - a string. This function closes the file handle before it returns - (even if the underlying Python implementation's garbage collector - doesn't). """ - fh = open(filename, mode) - try: - return fh.read() - finally: - fh.close() - -def write_file(filename, data, mode='wb'): - """ Write the string data into a file named filename. This - function closes the file handle (ensuring that the written data is - flushed from the perspective of the Python implementation) before - it returns (even if the underlying Python implementation's garbage - collector doesn't).""" - fh = open(filename, mode) - try: - fh.write(data) - finally: - fh.close() - -# For backwards-compatibility in case someone is using these names. We used to -# have a superkludge in fileutil.py under these names. -def rename(src, dst, tries=4, basedelay=0.1): - return os.rename(src, dst) - -def remove(f, tries=4, basedelay=0.1): - return os.remove(f) - -def rmdir(f, tries=4, basedelay=0.1): - return os.rmdir(f) - -class _Dir(object): - """ - Hold a set of files and subdirs and clean them all up when asked to. - """ - def __init__(self, name, cleanup=True): - self.name = name - self.cleanup = cleanup - self.files = [] - self.subdirs = set() - - def file(self, fname, mode=None): - """ - Create a file in the tempdir and remember it so as to close() it - before attempting to cleanup the temp dir. - - @rtype: file - """ - ffn = os.path.join(self.name, fname) - if mode is not None: - fo = open(ffn, mode) - else: - fo = open(ffn) - self.register_file(fo) - return fo - - def subdir(self, dirname): - """ - Create a subdirectory in the tempdir and remember it so as to call - shutdown() on it before attempting to clean up. - - @rtype: _Dir instance - """ - ffn = os.path.join(self.name, dirname) - sd = _Dir(ffn, self.cleanup) - self.register_subdir(sd) - make_dirs(sd.name) - return sd - - def register_file(self, fileobj): - """ - Remember the file object and call close() on it before attempting to - clean up. - """ - self.files.append(fileobj) - - def register_subdir(self, dirobj): - """ - Remember the _Dir object and call shutdown() on it before attempting - to clean up. - """ - self.subdirs.add(dirobj) - - def shutdown(self): - if self.cleanup: - for subdir in hasattr(self, 'subdirs') and self.subdirs or []: - subdir.shutdown() - for fileobj in hasattr(self, 'files') and self.files or []: - if DBNoSuchFileError is None: - fileobj.close() # "close()" is idempotent so we don't need to catch exceptions here - else: - try: - fileobj.close() - except DBNoSuchFileError: - # Ah, except that the bsddb module's file-like object (a DB object) has a non-idempotent close... - pass - - if hasattr(self, 'name'): - rm_dir(self.name) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - try: - self.shutdown() - except: - import traceback - traceback.print_exc() - -class NamedTemporaryDirectory(_Dir): - """ - Call tempfile.mkdtemp(), store the name of the dir in self.name, and - rm_dir() when it gets garbage collected or "shutdown()". - - Also keep track of file objects for files within the tempdir and call - close() on them before rm_dir(). This is a convenient way to open temp - files within the directory, and it is very helpful on Windows because you - can't delete a directory which contains a file which is currently open. - """ - - def __init__(self, cleanup=True, *args, **kwargs): - """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """ - name = tempfile.mkdtemp(*args, **kwargs) - _Dir.__init__(self, name, cleanup) - -class ReopenableNamedTemporaryFile: - """ - This uses tempfile.mkstemp() to generate a secure temp file. It then closes - the file, leaving a zero-length file as a placeholder. You can get the - filename with ReopenableNamedTemporaryFile.name. When the - ReopenableNamedTemporaryFile instance is garbage collected or its shutdown() - method is called, it deletes the file. - """ - def __init__(self, *args, **kwargs): - fd, self.name = tempfile.mkstemp(*args, **kwargs) - os.close(fd) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - self.shutdown() - - def shutdown(self): - remove(self.name) - -def make_dirs(dirname, mode=0777): - """ - An idempotent version of os.makedirs(). If the dir already exists, do - nothing and return without raising an exception. If this call creates the - dir, return without raising an exception. If there is an error that - prevents creation or if the directory gets deleted after make_dirs() creates - it and before make_dirs() checks that it exists, raise an exception. - """ - tx = None - try: - os.makedirs(dirname, mode) - except OSError, x: - tx = x - - if not os.path.isdir(dirname): - if tx: - raise tx - raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning... - -def rmtree(dirname): - """ - A threadsafe and idempotent version of shutil.rmtree(). If the dir is - already gone, do nothing and return without raising an exception. If this - call removes the dir, return without raising an exception. If there is an - error that prevents deletion or if the directory gets created again after - rm_dir() deletes it and before rm_dir() checks that it is gone, raise an - exception. - """ - excs = [] - try: - os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) - for f in os.listdir(dirname): - fullname = os.path.join(dirname, f) - if os.path.isdir(fullname): - rm_dir(fullname) - else: - remove(fullname) - os.rmdir(dirname) - except EnvironmentError, le: - # Ignore "No such file or directory", collect any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - excs.append(le) - except Exception, le: - excs.append(le) - - # Okay, now we've recursively removed everything, ignoring any "No - # such file or directory" errors, and collecting any other errors. - - if os.path.exists(dirname): - if len(excs) == 1: - raise excs[0] - if len(excs) == 0: - raise OSError, "Failed to remove dir for unknown reason." - raise OSError, excs - -def rm_dir(dirname): - # Renamed to be like shutil.rmtree and unlike rmdir. - return rmtree(dirname) - -def remove_if_possible(f): - try: - remove(f) - except EnvironmentError: - pass - -def remove_if_present(f): - try: - remove(f) - except EnvironmentError, le: - # Ignore "No such file or directory", re-raise any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - raise - -def rmdir_if_possible(f): - try: - rmdir(f) - except EnvironmentError: - pass - -def open_or_create(fname, binarymode=True): - try: - f = open(fname, binarymode and "r+b" or "r+") - except EnvironmentError: - f = open(fname, binarymode and "w+b" or "w+") - return f - -def du(basedir): - size = 0 - - for root, dirs, files in os.walk(basedir): - for f in files: - fn = os.path.join(root, f) - size += os.path.getsize(fn) - - return size diff --git a/libs/pyutil/iputil.py b/libs/pyutil/iputil.py index b8b96362..cb3e7c02 100644 --- a/libs/pyutil/iputil.py +++ b/libs/pyutil/iputil.py @@ -1,22 +1,12 @@ -# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) -# portions adapted from nattraverso.ipdiscover -# portions authored by Brian Warner, working for Allmydata -# most recent version authored by Zooko O'Whielacronx, working for Allmydata - # from the Python Standard Library -import os, re, socket, sys +import os, re, socket, sys, subprocess # from Twisted -from twisted.internet import defer, reactor -from twisted.python import failure +from twisted.internet import defer, threads, reactor from twisted.internet.protocol import DatagramProtocol -from twisted.internet.utils import getProcessOutput from twisted.python.procutils import which from twisted.python import log -# from pyutil -import observer - try: import resource def increase_rlimits(): @@ -77,6 +67,7 @@ except ImportError: # since one might be shadowing the other. This hack appeases pyflakes. increase_rlimits = _increase_rlimits + def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET """ Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad @@ -121,14 +112,16 @@ def get_local_ip_for(target): except socket.gaierror: # DNS isn't running, or somehow we encountered an error - # note: if an interface is configured and up, but nothing is connected to it, - # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror - # . This is synchronous and occurs for each node being started, so users of certain unit - # tests will see something like 120s of delay, which may be enough to hit the default - # trial timeouts. For that reason, get_local_addresses_async() was changed to default to - # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also - # makes node startup a tad faster. - + # note: if an interface is configured and up, but nothing is + # connected to it, gethostbyname("A.ROOT-SERVERS.NET") will take 20 + # seconds to raise socket.gaierror . This is synchronous and occurs + # for each node being started, so users of + # test.common.SystemTestMixin (like test_system) will see something + # like 120s of delay, which may be enough to hit the default trial + # timeouts. For that reason, get_local_addresses_async() was changed + # to default to the numerical ip address for A.ROOT-SERVERS.NET, to + # avoid this DNS lookup. This also makes node startup fractionally + # faster. return None udpprot = DatagramProtocol() port = reactor.listenUDP(0, udpprot) @@ -146,16 +139,29 @@ _platform_map = { "linux-i386": "linux", # redhat "linux-ppc": "linux", # redhat "linux2": "linux", # debian + "linux3": "linux", # debian "win32": "win32", "irix6-n32": "irix", "irix6-n64": "irix", "irix6": "irix", "openbsd2": "bsd", + "openbsd3": "bsd", + "openbsd4": "bsd", + "openbsd5": "bsd", "darwin": "bsd", # Mac OS X "freebsd4": "bsd", "freebsd5": "bsd", "freebsd6": "bsd", + "freebsd7": "bsd", + "freebsd8": "bsd", + "freebsd9": "bsd", "netbsd1": "bsd", + "netbsd2": "bsd", + "netbsd3": "bsd", + "netbsd4": "bsd", + "netbsd5": "bsd", + "netbsd6": "bsd", + "dragonfly2": "bsd", "sunos5": "sunos", "cygwin": "cygwin", } @@ -173,12 +179,12 @@ _win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\ # These work in Redhat 6.x and Debian 2.2 potato _linux_path = '/sbin/ifconfig' -_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +_linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) -# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD -_bsd_path = '/sbin/ifconfig' -_bsd_args = ('-a',) -_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +# NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X +_netbsd_path = '/sbin/ifconfig' +_netbsd_args = ('-a',) +_netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) # Irix 6.5 _irix_path = '/usr/etc/ifconfig' @@ -186,39 +192,6 @@ _irix_path = '/usr/etc/ifconfig' # Solaris 2.x _sunos_path = '/usr/sbin/ifconfig' -class SequentialTrier(object): - """ I hold a list of executables to try and try each one in turn - until one gives me a list of IP addresses.""" - - def __init__(self, exebasename, args, regex): - assert not os.path.isabs(exebasename) - self.exes_left_to_try = which(exebasename) - self.exes_left_to_try.reverse() - self.args = args - self.regex = regex - self.o = observer.OneShotObserverList() - self._try_next() - - def _try_next(self): - if not self.exes_left_to_try: - self.o.fire(None) - else: - exe = self.exes_left_to_try.pop() - d2 = _query(exe, self.args, self.regex) - - def cb(res): - if res: - self.o.fire(res) - else: - self._try_next() - - def eb(why): - self._try_next() - - d2.addCallbacks(cb, eb) - - def when_tried(self): - return self.o.when_fired() # k: platform string as provided in the value of _platform_map # v: tuple of (path_to_tool, args, regex,) @@ -226,19 +199,22 @@ _tool_map = { "linux": (_linux_path, (), _linux_re,), "win32": (_win32_path, _win32_args, _win32_re,), "cygwin": (_win32_path, _win32_args, _win32_re,), - "bsd": (_bsd_path, _bsd_args, _bsd_re,), - "irix": (_irix_path, _bsd_args, _bsd_re,), - "sunos": (_sunos_path, _bsd_args, _bsd_re,), + "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,), + "irix": (_irix_path, _netbsd_args, _netbsd_re,), + "sunos": (_sunos_path, _netbsd_args, _netbsd_re,), } + def _find_addresses_via_config(): - # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + return threads.deferToThread(_synchronously_find_addresses_via_config) + +def _synchronously_find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian's API platform = _platform_map.get(sys.platform) - (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + if not platform: + raise UnsupportedPlatformError(sys.platform) - # If the platform isn't known then we attempt BSD-style ifconfig. If it - # turns out that we don't get anything resembling a dotted quad IPv4 address - # out of it, then we'll raise UnsupportedPlatformError. + (pathtotool, args, regex,) = _tool_map[platform] # If pathtotool is a fully qualified path then we just try that. # If it is merely an executable name then we use Twisted's @@ -246,34 +222,33 @@ def _find_addresses_via_config(): # gives us something that resembles a dotted-quad IPv4 address. if os.path.isabs(pathtotool): - d = _query(pathtotool, args, regex) + return _query(pathtotool, args, regex) else: - d = SequentialTrier(pathtotool, args, regex).when_tried() - - d.addCallback(_check_result) - return d - -def _check_result(result): - if not result and not _platform_map.has_key(sys.platform): - return failure.Failure(UnsupportedPlatformError(sys.platform)) - else: - return result + exes_to_try = which(pathtotool) + for exe in exes_to_try: + try: + addresses = _query(exe, args, regex) + except Exception: + addresses = [] + if addresses: + return addresses + return [] def _query(path, args, regex): - d = getProcessOutput(path, args) - def _parse(output): - addresses = [] - outputsplit = output.split('\n') - for outline in outputsplit: - m = regex.match(outline) - if m: - addr = m.groupdict()['address'] - if addr not in addresses: - addresses.append(addr) + env = {'LANG': 'en_US.UTF-8'} + p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + (output, err) = p.communicate() - return addresses - d.addCallback(_parse) - return d + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses def _cygwin_hack_find_addresses(target): addresses = [] diff --git a/libs/pyutil/iputil.py~ b/libs/pyutil/iputil.py~ new file mode 100644 index 00000000..b8b96362 --- /dev/null +++ b/libs/pyutil/iputil.py~ @@ -0,0 +1,288 @@ +# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) +# portions adapted from nattraverso.ipdiscover +# portions authored by Brian Warner, working for Allmydata +# most recent version authored by Zooko O'Whielacronx, working for Allmydata + +# from the Python Standard Library +import os, re, socket, sys + +# from Twisted +from twisted.internet import defer, reactor +from twisted.python import failure +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.utils import getProcessOutput +from twisted.python.procutils import which +from twisted.python import log + +# from pyutil +import observer + +try: + import resource + def increase_rlimits(): + # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain + # systems (OS-X, probably solaris) start with a relatively low limit + # (256), and some unit tests want to open up more sockets than this. + # Most linux systems start with both hard and soft limits at 1024, + # which is plenty. + + # unfortunately the values to pass to setrlimit() vary widely from + # one system to another. OS-X reports (256, HUGE), but the real hard + # limit is 10240, and accepts (-1,-1) to mean raise it to the + # maximum. Cygwin reports (256, -1), then ignores a request of + # (-1,-1): instead you have to guess at the hard limit (it appears to + # be 3200), so using (3200,-1) seems to work. Linux reports a + # sensible (1024,1024), then rejects (-1,-1) as trying to raise the + # maximum limit, so you could set it to (1024,1024) but you might as + # well leave it alone. + + try: + current = resource.getrlimit(resource.RLIMIT_NOFILE) + except AttributeError: + # we're probably missing RLIMIT_NOFILE + return + + if current[0] >= 1024: + # good enough, leave it alone + return + + try: + if current[1] > 0 and current[1] < 1000000: + # solaris reports (256, 65536) + resource.setrlimit(resource.RLIMIT_NOFILE, + (current[1], current[1])) + else: + # this one works on OS-X (bsd), and gives us 10240, but + # it doesn't work on linux (on which both the hard and + # soft limits are set to 1024 by default). + resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1)) + new = resource.getrlimit(resource.RLIMIT_NOFILE) + if new[0] == current[0]: + # probably cygwin, which ignores -1. Use a real value. + resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1)) + + except ValueError: + log.msg("unable to set RLIMIT_NOFILE: current value %s" + % (resource.getrlimit(resource.RLIMIT_NOFILE),)) + except: + # who knows what. It isn't very important, so log it and continue + log.err() +except ImportError: + def _increase_rlimits(): + # TODO: implement this for Windows. Although I suspect the + # solution might be "be running under the iocp reactor and + # make this function be a no-op". + pass + # pyflakes complains about two 'def FOO' statements in the same time, + # since one might be shadowing the other. This hack appeases pyflakes. + increase_rlimits = _increase_rlimits + +def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET + """ + Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad + strings) that are currently configured on this host, sorted in descending + order of how likely we think they are to work. + + @param target: we want to learn an IP address they could try using to + connect to us; The default value is fine, but it might help if you + pass the address of a host that you are actually trying to be + reachable to. + """ + addresses = [] + local_ip = get_local_ip_for(target) + if local_ip: + addresses.append(local_ip) + + if sys.platform == "cygwin": + d = _cygwin_hack_find_addresses(target) + else: + d = _find_addresses_via_config() + + def _collect(res): + for addr in res: + if addr != "0.0.0.0" and not addr in addresses: + addresses.append(addr) + return addresses + d.addCallback(_collect) + + return d + +def get_local_ip_for(target): + """Find out what our IP address is for use by a given target. + + @return: the IP address as a dotted-quad string which could be used by + to connect to us. It might work for them, it might not. If + there is no suitable address (perhaps we don't currently have an + externally-visible interface), this will return None. + """ + + try: + target_ipaddr = socket.gethostbyname(target) + except socket.gaierror: + # DNS isn't running, or somehow we encountered an error + + # note: if an interface is configured and up, but nothing is connected to it, + # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror + # . This is synchronous and occurs for each node being started, so users of certain unit + # tests will see something like 120s of delay, which may be enough to hit the default + # trial timeouts. For that reason, get_local_addresses_async() was changed to default to + # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also + # makes node startup a tad faster. + + return None + udpprot = DatagramProtocol() + port = reactor.listenUDP(0, udpprot) + try: + udpprot.transport.connect(target_ipaddr, 7) + localip = udpprot.transport.getHost().host + except socket.error: + # no route to that host + localip = None + port.stopListening() # note, this returns a Deferred + return localip + +# k: result of sys.platform, v: which kind of IP configuration reader we use +_platform_map = { + "linux-i386": "linux", # redhat + "linux-ppc": "linux", # redhat + "linux2": "linux", # debian + "win32": "win32", + "irix6-n32": "irix", + "irix6-n64": "irix", + "irix6": "irix", + "openbsd2": "bsd", + "darwin": "bsd", # Mac OS X + "freebsd4": "bsd", + "freebsd5": "bsd", + "freebsd6": "bsd", + "netbsd1": "bsd", + "sunos5": "sunos", + "cygwin": "cygwin", + } + +class UnsupportedPlatformError(Exception): + pass + +# Wow, I'm really amazed at home much mileage we've gotten out of calling +# the external route.exe program on windows... It appears to work on all +# versions so far. Still, the real system calls would much be preferred... +# ... thus wrote Greg Smith in time immemorial... +_win32_path = 'route.exe' +_win32_args = ('print',) +_win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\d+)\s+(?P\d+)\s*$', flags=re.M|re.I|re.S) + +# These work in Redhat 6.x and Debian 2.2 potato +_linux_path = '/sbin/ifconfig' +_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD +_bsd_path = '/sbin/ifconfig' +_bsd_args = ('-a',) +_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# Irix 6.5 +_irix_path = '/usr/etc/ifconfig' + +# Solaris 2.x +_sunos_path = '/usr/sbin/ifconfig' + +class SequentialTrier(object): + """ I hold a list of executables to try and try each one in turn + until one gives me a list of IP addresses.""" + + def __init__(self, exebasename, args, regex): + assert not os.path.isabs(exebasename) + self.exes_left_to_try = which(exebasename) + self.exes_left_to_try.reverse() + self.args = args + self.regex = regex + self.o = observer.OneShotObserverList() + self._try_next() + + def _try_next(self): + if not self.exes_left_to_try: + self.o.fire(None) + else: + exe = self.exes_left_to_try.pop() + d2 = _query(exe, self.args, self.regex) + + def cb(res): + if res: + self.o.fire(res) + else: + self._try_next() + + def eb(why): + self._try_next() + + d2.addCallbacks(cb, eb) + + def when_tried(self): + return self.o.when_fired() + +# k: platform string as provided in the value of _platform_map +# v: tuple of (path_to_tool, args, regex,) +_tool_map = { + "linux": (_linux_path, (), _linux_re,), + "win32": (_win32_path, _win32_args, _win32_re,), + "cygwin": (_win32_path, _win32_args, _win32_re,), + "bsd": (_bsd_path, _bsd_args, _bsd_re,), + "irix": (_irix_path, _bsd_args, _bsd_re,), + "sunos": (_sunos_path, _bsd_args, _bsd_re,), + } +def _find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + + platform = _platform_map.get(sys.platform) + (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + + # If the platform isn't known then we attempt BSD-style ifconfig. If it + # turns out that we don't get anything resembling a dotted quad IPv4 address + # out of it, then we'll raise UnsupportedPlatformError. + + # If pathtotool is a fully qualified path then we just try that. + # If it is merely an executable name then we use Twisted's + # "which()" utility and try each executable in turn until one + # gives us something that resembles a dotted-quad IPv4 address. + + if os.path.isabs(pathtotool): + d = _query(pathtotool, args, regex) + else: + d = SequentialTrier(pathtotool, args, regex).when_tried() + + d.addCallback(_check_result) + return d + +def _check_result(result): + if not result and not _platform_map.has_key(sys.platform): + return failure.Failure(UnsupportedPlatformError(sys.platform)) + else: + return result + +def _query(path, args, regex): + d = getProcessOutput(path, args) + def _parse(output): + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses + d.addCallback(_parse) + return d + +def _cygwin_hack_find_addresses(target): + addresses = [] + for h in [target, "localhost", "127.0.0.1",]: + try: + addr = get_local_ip_for(h) + if addr not in addresses: + addresses.append(addr) + except socket.gaierror: + pass + + return defer.succeed(addresses) diff --git a/libs/pyutil/mathutil.py b/libs/pyutil/mathutil.py index 46781b0c..9c169801 100644 --- a/libs/pyutil/mathutil.py +++ b/libs/pyutil/mathutil.py @@ -11,7 +11,7 @@ def div_ceil(n, d): """ The smallest integer k such that k*d >= n. """ - return (n/d) + (n%d != 0) + return int((n//d) + (n%d != 0)) def next_multiple(n, k): """ diff --git a/libs/pyutil/mathutil.py~ b/libs/pyutil/mathutil.py~ new file mode 100644 index 00000000..46781b0c --- /dev/null +++ b/libs/pyutil/mathutil.py~ @@ -0,0 +1,106 @@ +# Copyright (c) 2005-2010 Zooko Wilcox-O'Hearn +# This file is part of pyutil; see README.rst for licensing terms. + +""" +A few commonly needed functions. +""" + +import math + +def div_ceil(n, d): + """ + The smallest integer k such that k*d >= n. + """ + return (n/d) + (n%d != 0) + +def next_multiple(n, k): + """ + The smallest multiple of k which is >= n. Note that if n is 0 then the + answer is 0. + """ + return div_ceil(n, k) * k + +def pad_size(n, k): + """ + The smallest number that has to be added to n to equal a multiple of k. + """ + if n%k: + return k - n%k + else: + return 0 + +def is_power_of_k(n, k): + return k**int(math.log(n, k) + 0.5) == n + +def next_power_of_k(n, k): + p = 1 + while p < n: + p *= k + return p + +def ave(l): + return sum(l) / len(l) + +def log_ceil(n, b): + """ + The smallest integer k such that b^k >= n. + + log_ceil(n, 2) is the number of bits needed to store any of n values, e.g. + the number of bits needed to store any of 128 possible values is 7. + """ + p = 1 + k = 0 + while p < n: + p *= b + k += 1 + return k + +def log_floor(n, b): + """ + The largest integer k such that b^k <= n. + """ + p = 1 + k = 0 + while p <= n: + p *= b + k += 1 + return k - 1 + +def linear_fit_slope(ps): + """ + Single-independent-variable linear regression -- least squares method. + + At least, I *think* this function computes that answer. I no longer + remember where I learned this trick and at the moment I can't prove to + myself that this is correct. + + @param ps a sequence of tuples of (x, y) + """ + avex = ave([x for (x, y) in ps]) + avey = ave([y for (x, y) in ps]) + sxy = sum([ (x - avex) * (y - avey) for (x, y) in ps ]) + sxx = sum([ (x - avex) ** 2 for (x, y) in ps ]) + if sxx == 0: + return None + return sxy / sxx + +def permute(l): + """ + Return all possible permutations of l. + + @type l: sequence + @rtype a set of sequences + """ + if len(l) == 1: + return [l,] + + res = [] + for i in range(len(l)): + l2 = list(l[:]) + x = l2.pop(i) + for l3 in permute(l2): + l3.append(x) + res.append(l3) + + return res + diff --git a/libs/pyutil/odict.py~ b/libs/pyutil/odict.py~ deleted file mode 100644 index 0ed5ce7b..00000000 --- a/libs/pyutil/odict.py~ +++ /dev/null @@ -1,552 +0,0 @@ -# Copyright (c) 2002-2009 Zooko "Zooko" Wilcox-O'Hearn - -""" -This module offers a Ordered Dict, which is a dict that preserves -insertion order. See PEP 372 for description of the problem. This -implementation uses a linked-list to get good O(1) asymptotic -performance. (Actually it is O(hashtable-update-cost), but whatever.) - -Warning: if -O optimizations are not turned on then OrderedDict performs -extensive self-analysis in every function call, which can take minutes -and minutes for a large cache. Turn on -O, or comment out assert -self._assert_invariants() -""" - -import operator - -from assertutil import _assert, precondition -from humanreadable import hr - -class OrderedDict: - """ - An efficient ordered dict. - - Adding an item that is already in the dict *does not* make it the - most- recently-added item although it may change the state of the - dict itself (if the value is different than the previous value). - - See also SmallOrderedDict (below), which is faster in some cases. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return (k, v,) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return k - - class ValIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - precondition(self.c.d.has_key(self.i), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - (v, p, n,) = self.c.d[self.i] - self.i = p - return v - - class Sentinel: - def __init__(self, msg): - self.msg = msg - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.msg,) - - def __init__(self, initialdata={}): - self.d = {} # k: k, v: [v, prev, next,] # the dict - self.hs = OrderedDict.Sentinel("hs") - self.ts = OrderedDict.Sentinel("ts") - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.update(initialdata) - - assert self._assert_invariants() - - def __repr_n__(self, n=None): - s = ["{",] - try: - iter = self.iteritems() - x = iter.next() - s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - i = 1 - while (n is None) or (i < n): - x = iter.next() - s.append(", "); s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - except StopIteration: - pass - s.append("}") - return ''.join(s) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(),) - - def __str__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(16),) - - def _assert_invariants(self): - _assert((len(self.d) > 2) == (self.d[self.hs][2] is not self.ts) == (self.d[self.ts][1] is not self.hs), "Head and tail point to something other than each other if and only if there is at least one element in the dictionary.", self.hs, self.ts, len(self.d)) - foundprevsentinel = 0 - foundnextsentinel = 0 - for (k, (v, p, n,)) in self.d.iteritems(): - _assert(v not in (self.hs, self.ts,)) - _assert(p is not self.ts, "A reference to the tail sentinel may not appear in prev.", k, v, p, n) - _assert(n is not self.hs, "A reference to the head sentinel may not appear in next.", k, v, p, n) - _assert(p in self.d, "Each prev is required to appear as a key in the dict.", k, v, p, n) - _assert(n in self.d, "Each next is required to appear as a key in the dict.", k, v, p, n) - if p is self.hs: - foundprevsentinel += 1 - _assert(foundprevsentinel <= 2, "No more than two references to the head sentinel may appear as a prev.", k, v, p, n) - if n is self.ts: - foundnextsentinel += 1 - _assert(foundnextsentinel <= 2, "No more than one reference to the tail sentinel may appear as a next.", k, v, p, n) - _assert(foundprevsentinel == 2, "A reference to the head sentinel is required appear as a prev (plus a self-referential reference).") - _assert(foundnextsentinel == 2, "A reference to the tail sentinel is required appear as a next (plus a self-referential reference).") - - count = 0 - for (k, v,) in self.iteritems(): - _assert(k not in (self.hs, self.ts,), k, self.hs, self.ts) - count += 1 - _assert(count == len(self.d)-2, count, len(self.d)) # -2 for the sentinels - - return True - - def move_to_most_recent(self, k, strictkey=False): - assert self._assert_invariants() - - if not self.d.has_key(k): - if strictkey: - raise KeyError, k - return - - node = self.d[k] - - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - - # move to front - hnode = self.d[self.hs] - - node[1] = self.hs - node[2] = hnode[2] - hnode[2] = k - self.d[node[2]][1] = k - - assert self._assert_invariants() - - def iteritems(self): - return OrderedDict.ItemIterator(self) - - def itervalues(self): - return OrderedDict.ValIterator(self) - - def iterkeys(self): - return self.__iter__() - - def __iter__(self): - return OrderedDict.KeyIterator(self) - - def __getitem__(self, key, default=None, strictkey=True): - node = self.d.get(key) - if not node: - if strictkey: - raise KeyError, key - return default - return node[0] - - def __setitem__(self, k, v=None): - assert self._assert_invariants() - - node = self.d.get(k) - if node: - node[0] = v - return - - hnode = self.d[self.hs] - n = hnode[2] - self.d[k] = [v, self.hs, n,] - hnode[2] = k - self.d[n][1] = k - - assert self._assert_invariants() - return v - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the value removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if self.d.has_key(key): - node = self.d[key] - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - del self.d[key] - assert self._assert_invariants() - return node[0] - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def has_key(self, key): - assert self._assert_invariants() - if self.d.has_key(key): - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def clear(self): - assert self._assert_invariants() - self.d.clear() - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - - for (k, v,) in otherdict.iteritems(): - assert self._assert_invariants() - self[k] = v - assert self._assert_invariants() - - def pop(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - self.remove(k) - assert self._assert_invariants() - return k - - def popitem(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - val = self.remove(k) - assert self._assert_invariants() - return (k, val,) - - def keys_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return t.keys() - - def keys(self): - res = [None] * len(self) - i = 0 - for k in self.iterkeys(): - res[i] = k - i += 1 - return res - - def values_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return map(operator.__getitem__, t.values(), [0]*len(t)) - - def values(self): - res = [None] * len(self) - i = 0 - for v in self.itervalues(): - res[i] = v - i += 1 - return res - - def items(self): - res = [None] * len(self) - i = 0 - for it in self.iteritems(): - res[i] = it - i += 1 - return res - - def __len__(self): - return len(self.d) - 2 - - def insert(self, key, val=None): - assert self._assert_invariants() - result = self.__setitem__(key, val) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def get(self, key, default=None): - return self.__getitem__(key, default, strictkey=False) - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - -class SmallOrderedDict(dict): - """ - SmallOrderedDict is faster than OrderedDict for small sets. How small? That - depends on your machine and which operations you use most often. Use - performance profiling to determine whether the cache class that you are - using makes any difference to the performance of your program, and if it - does, then run "quick_bench()" in test/test_cache.py to see which cache - implementation is faster for the size of your datasets. - - A simple least-recently-used cache. It keeps an LRU queue, and - when the number of items in the cache reaches maxsize, it removes - the least recently used item. - - "Looking" at an item or a key such as with "has_key()" makes that - item become the most recently used item. - - You can also use "refresh()" to explicitly make an item become the most - recently used item. - - Adding an item that is already in the dict *does* make it the - most- recently-used item although it does not change the state of - the dict itself. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return (k, dict.__getitem__(self.c, k),) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return k - - class ValueIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return dict.__getitem__(self.c, k) - - def __init__(self, initialdata={}, maxsize=128): - dict.__init__(self, initialdata) - self._lru = initialdata.keys() # contains keys - self._maxsize = maxsize - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - assert self._assert_invariants() - - def _assert_invariants(self): - _assert(len(self._lru) <= self._maxsize, "Size is required to be <= maxsize.") - _assert(len(filter(lambda x: dict.has_key(self, x), self._lru)) == len(self._lru), "Each key in self._lru is required to be in dict.", filter(lambda x: not dict.has_key(self, x), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(filter(lambda x: x in self._lru, self.keys())) == len(self), "Each key in dict is required to be in self._lru.", filter(lambda x: x not in self._lru, self.keys()), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) == len(self), "internal consistency", filter(lambda x: x not in self.keys(), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) <= self._maxsize, "internal consistency", len(self._lru), self._lru, self._maxsize) - return True - - def insert(self, key, item=None): - assert self._assert_invariants() - result = self.__setitem__(key, item) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def __setitem__(self, key, item=None): - assert self._assert_invariants() - if dict.has_key(self, key): - self._lru.remove(key) - else: - if len(self._lru) == self._maxsize: - # If this insert is going to increase the size of the cache to bigger than maxsize: - killkey = self._lru.pop(0) - dict.__delitem__(self, killkey) - dict.__setitem__(self, key, item) - self._lru.append(key) - assert self._assert_invariants() - return item - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the object removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if dict.has_key(self, key): - val = dict.__getitem__(self, key) - dict.__delitem__(self, key) - self._lru.remove(key) - assert self._assert_invariants() - return val - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def clear(self): - assert self._assert_invariants() - dict.clear(self) - self._lru = [] - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - if len(otherdict) > self._maxsize: - # Handling this special case here makes it possible to implement the - # other more common cases faster below. - dict.clear(self) - self._lru = [] - if self._maxsize > (len(otherdict) - self._maxsize): - dict.update(self, otherdict) - while len(self) > self._maxsize: - dict.popitem(self) - else: - for k, v, in otherdict.iteritems(): - if len(self) == self._maxsize: - break - dict.__setitem__(self, k, v) - self._lru = dict.keys(self) - assert self._assert_invariants() - return self - - for k in otherdict.iterkeys(): - if dict.has_key(self, k): - self._lru.remove(k) - self._lru.extend(otherdict.keys()) - dict.update(self, otherdict) - - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - - assert self._assert_invariants() - return self - - def has_key(self, key): - assert self._assert_invariants() - if dict.has_key(self, key): - assert key in self._lru, "key: %s, self._lru: %s" % tuple(map(hr, (key, self._lru,))) - self._lru.remove(key) - self._lru.append(key) - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def refresh(self, key, strictkey=True): - """ - @param strictkey: raise a KeyError exception if key isn't present - """ - assert self._assert_invariants() - if not dict.has_key(self, key): - if strictkey: - raise KeyError, key - return - self._lru.remove(key) - self._lru.append(key) - - def popitem(self): - if not self._lru: - raise KeyError, 'popitem(): dictionary is empty' - k = self._lru[-1] - obj = self.remove(k) - return (k, obj,) diff --git a/libs/pyutil/randutil.py b/libs/pyutil/randutil.py index a3efb74f..82eb3e18 100644 --- a/libs/pyutil/randutil.py +++ b/libs/pyutil/randutil.py @@ -80,6 +80,5 @@ seed = randobj.seed def randstr(n): return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) -import random as insecurerandom def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) + return os.urandom(n) diff --git a/libs/pyutil/randutil.py~ b/libs/pyutil/randutil.py~ deleted file mode 100644 index b0f1c4f9..00000000 --- a/libs/pyutil/randutil.py~ +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -import warnings -import os, random - -try: - import hashexpand - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=None, deterministic=True): - warnings.warn("deprecated", DeprecationWarning) - if not deterministic: - raise NotImplementedError, "SHA256Expander is always deterministic. For non-deterministic, try urandomRandom." - - hashexpand.SHA256Expander.__init__(self) - random.Random.__init__(self, seed) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - import increasing_timer - seed = repr(increasing_timer.time()) - hashexpand.SHA256Expander.seed(self, seed) - - - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=""): - warnings.warn("deprecated", DeprecationWarning) - hashexpand.SHA256Expander.__init__(self) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - seed = os.urandom(32) - hashexpand.SHA256Expander.seed(self, seed) -except ImportError, le: - class InsecureSHA256Random: - def __init__(self, seed=None): - raise ImportError, le - class SHA256Random: - def __init__(self, seed=""): - raise ImportError, le - -class devrandomRandom(random.Random): - """ The problem with using this one, of course, is that it blocks. This - is, of course, a security flaw. (On Linux and probably on other - systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def __init__(self): - warnings.warn("deprecated", DeprecationWarning) - self.dr = open("/dev/random", "r") - - def get(self, bytes): - return self.dr.read(bytes) - - -class devurandomRandom(random.Random): - """ The problem with using this one is that it gives answers even when it - has never been properly seeded, e.g. when you are booting from CD and have - just started up and haven't yet gathered enough entropy to actually be - unguessable. (On Linux and probably on other systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def get(self, bytes): - warnings.warn("deprecated", DeprecationWarning) - return os.urandom(bytes) - - -randobj = devurandomRandom() -get = randobj.get -random = randobj.random -randrange = randobj.randrange -shuffle = randobj.shuffle -choice = randobj.choice -seed = randobj.seed - -def randstr(n): - return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) - -import random as insecurerandom -def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) diff --git a/libs/pyutil/scripts/passphrase.py b/libs/pyutil/scripts/passphrase.py new file mode 100644 index 00000000..bed79c13 --- /dev/null +++ b/libs/pyutil/scripts/passphrase.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse, math, random + +from pyutil.mathutil import div_ceil + +from pkg_resources import resource_stream + +def recursive_subset_sum(entropy_needed, wordlists): + # Pick a minimalish set of numbers which sum to at least + # entropy_needed. + + # Okay now what's the smallest number of words which will give us + # at least this much entropy? + entropy_of_biggest_wordlist = wordlists[-1][0] + assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1] + needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist) + # How much entropy do we need from each word? + needed_entropy_per_word = entropy_needed / needed_words + # What's the smallest wordlist that offers at least this much + # entropy per word? + for (wlentropy, wl) in wordlists: + if wlentropy >= needed_entropy_per_word: + break + assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word) + + result = [(wlentropy, wl)] + # If we need more, recurse... + if wlentropy < entropy_needed: + rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists) + result.extend(rest) + return result + +def gen_passphrase(entropy, allwords): + maxlenwords = [] + i = 2 # The smallest set is words of length 1 or 2. + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + while len(maxlenwords[-1][1]) < len(allwords): + i += 1 + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + + sr = random.SystemRandom() + passphrase = [] + + wordlists_to_use = recursive_subset_sum(entropy, maxlenwords) + + passphraseentropy = 0.0 + for (wle, wl) in wordlists_to_use: + passphrase.append(sr.choice(wl)) + passphraseentropy += wle + + return (u".".join(passphrase), passphraseentropy) + +def main(): + parser = argparse.ArgumentParser(prog="chbs", description="Create a random passphrase by picking a few random words.") + + parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use chbs's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT") + parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS") + args = parser.parse_args() + + dicti = args.dictionary + if not dicti: + dicti = resource_stream('pyutil', 'data/wordlist.txt') + allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()]) + + passphrase, bits = gen_passphrase(args.bits, allwords) + + print u"Your new password is: '%s'. It is worth about %s bits." % (passphrase, bits) diff --git a/libs/pyutil/scripts/time_comparisons.py b/libs/pyutil/scripts/time_comparisons.py new file mode 100644 index 00000000..15a38852 --- /dev/null +++ b/libs/pyutil/scripts/time_comparisons.py @@ -0,0 +1,209 @@ +# If you run this file, it will make up a random secret and then crack it +# using timing information from a string comparison function. Maybe--if it +# gets lucky. It takes a long, long time to work. + +# So, the thing I need help with is statistics. The way this thing works is +# extremely stupid. Suppose you want to know which function invocation takes +# longer: comparison(secret, guess1) or comparison(secret, guess2)? + +# If you can correctly determine that one of them takes longer than the +# other, then (a) you can use that to crack the secret, and (b) this is a +# unit test demonstrating that comparison() is not timing-safe. + +# So how does this script do it? Extremely stupidly. First of all, you can't +# reliably measure tiny times, so to measure the time that a function takes, +# we run that function 10,000 times in a row, measure how long that took, and +# divide by 10,000 to estimate how long any one run would have taken. + +# Then, we do that 100 times in a row, and take the fastest of 100 runs. (I +# also experimented with taking the mean of 100 runs instead of the fastest.) + +# Then, we just say whichever comparison took longer (for its fastest run of +# 100 runs of 10,000 executions per run) is the one we think is a closer +# guess to the secret. + +# Now I would *like* to think that there is some kind of statistical analysis +# more sophisticated than "take the slowest of the fastest of 100 runs of +# 10,000 executions". Such improved statistical analysis would hopefully be +# able to answer these two questions: + +# 1. Are these two function calls -- comparison(secret, guess1) and +# comparison(secret, guess2) -- drawing from the same distribution or +# different? If you can answer that question, then you've answered the +# question of whether "comparison" is timing-safe or not. + +# And, this would also allow the cracker to recover from a false step. If it +# incorrectly decides the the prefix of the secret is ABCX, when the real +# secret is ABCD, then after that every next step it takes will be the +# "drawing from the same distribution" kind -- any difference between ABCXQ +# and ABCXR will be just due to noise, since both are equally far from the +# correct answer, which startsw with ABCD. If it could realize that there is +# no real difference between the distributions, then it could back-track and +# recover. + +# 2. Giving the ability to measure, noisily, the time taken by comparison(), +# how can you most efficiently figure out which guess takes the longest? If +# you can do that more efficiently, you can crack secrets more efficiently. + +# The script takes two arguments. The first is how many symbols in the +# secret, and the second is how big the alphabet from which the symbols are +# drawn. To prove that this script can *ever* work, try passing length 5 and +# alphabet size 2. Also try editing the code to let is use sillycomp. That'll +# definitely make it work. If you can improve this script (as per the thing +# above about "needing better statistics") to the degree that it can crack a +# secret with length 32 and alphabet size 256, then that would be awesome. + +# See the result of this commandline: + +# $ python -c 'import time_comparisons ; time_comparisons.print_measurements()' + + +from pyutil import benchutil + +import hashlib, random, os + +from decimal import Decimal +D=Decimal + +p1 = 'a'*32 +p1a = 'a'*32 +p2 = 'a'*31+'b' # close, but no cigar +p3 = 'b'*32 # different in the first byte + +def randstr(n, alphabetsize): + alphabet = [ chr(x) for x in range(alphabetsize) ] + return ''.join([random.choice(alphabet) for i in range(n)]) + +def compare(n, f, a, b): + for i in xrange(n): + f(a, b) + +def eqeqcomp(a, b): + return a == b + +def sillycomp(a, b): + # This exposes a lot of information in its timing about how many leading bytes match. + for i in range(len(a)): + if a[i] != b[i]: + return False + for i in xrange(2**9): + pass + if len(a) == len(b): + return True + else: + return False + +def hashcomp(a, b): + # Brian Warner invented this for Tahoe-LAFS. It seems like it should be very safe agaist timing leakage of any kind, because of the inclusion of a new random randkey every time. Note that exposing the value of the hash (i.e. the output of md5(randkey+secret)) is *not* a security problem. You can post that on your web site and let all attackers have it, no problem. (Provided that the value of "randkey" remains secret.) + + randkey = os.urandom(32) + return hashlib.md5(randkey+ a).digest() == hashlib.md5(randkey+b).digest() + +def xorcomp(a, b): + # This appears to be the most popular timing-insensitive string comparison function. I'm not completely sure it is fully timing-insensitive. (There are all sorts of funny things inside Python, such as caching of integer objects < 100...) + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + +def print_measurements(): + N=10**4 + REPS=10**2 + + print "all times are in nanoseconds per comparison (in scientific notation)" + print + + for comparator in [eqeqcomp, hashcomp, xorcomp, sillycomp]: + print "using comparator ", comparator + + # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: + trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] + random.shuffle(trials) + for (a, b, desc) in trials: + print "comparing two strings that are %s to each other" % (desc,) + + def f(n): + compare(n, comparator, a, b) + + benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) + + print + +def try_to_crack_secret(cracker, comparator, secretlen, alphabetsize): + secret = randstr(secretlen, alphabetsize) + + def test_guess(x): + return comparator(secret, x) + + print "Giving cracker %s a chance to figure out the secret. Don't tell him, but the secret is %s. Whenever he makes a guess, we'll use comparator %s to decide if his guess is right ..." % (cracker, secret.encode('hex'), comparator,) + + guess = cracker(test_guess, secretlen, alphabetsize) + + print "Cracker %s guessed %r" % (cracker, guess,) + if guess == secret: + print "HE FIGURED IT OUT!? HOW DID HE DO THAT." + else: + print "HAHA. Our secret is safe." + +def byte_at_a_time_cracker(test_guess, secretlen, alphabetsize): + # If we were cleverer, we'd add some backtracking behaviour where, if we can't find any x such that ABCx stands out from the crowd as taking longer than all the other ABCy's, then we start to think that we've taken a wrong step and we go back to trying ABy's. Make sense? But we're not that clever. Once we take a step, we don't backtrack. + + print + + guess=[] + + while len(guess) < secretlen: + best_next_byte = None + best_next_byte_time = None + + # For each possible byte... + for next_byte in range(alphabetsize): + c = chr(next_byte) + + # Construct a guess with our best candidate so far... + candidate_guess = guess[:] + + # Plus that byte... + candidate_guess.append(c) + s = ''.join(candidate_guess) + + # Plus random bytes... + s += os.urandom(32 - len(s)) + + # And see how long it takes the test_guess to consider it... + def f(n): + for i in xrange(n): + test_guess(s) + + times = benchutil.rep_bench(f, 10**7, MAXREPS=10**3, quiet=True) + + fastesttime = times['mean'] + + print "%s..."%(c.encode('hex'),), + if best_next_byte is None or fastesttime > best_next_byte_time: + print "new candidate for slowest next-char: %s, took: %s" % (c.encode('hex'), fastesttime,), + + best_next_byte_time = fastesttime + best_next_byte = c + + # Okay we've tried all possible next bytes. Our guess is this one (the one that took longest to be tested by test_guess): + guess.append(best_next_byte) + print "SLOWEST next-char %s! Current guess at secret: %s" % (best_next_byte.encode('hex'), ''.join(guess).encode('hex'),) + + guess = ''.join(guess) + print "Our guess for the secret: %r" % (guess,) + return guess + +if __name__ == '__main__': + import sys + secretlen = int(sys.argv[1]) + alphabetsize = int(sys.argv[2]) + if alphabetsize > 256: + raise Exception("We assume we can fit one element of the alphabet into a byte.") + + print "secretlen: %d, alphabetsize: %d" % (secretlen, alphabetsize,) + + # try_to_crack_secret(byte_at_a_time_cracker, sillycomp, secretlen, alphabetsize) + try_to_crack_secret(byte_at_a_time_cracker, eqeqcomp, secretlen, alphabetsize) diff --git a/libs/pyutil/test/current/test_mathutil.py b/libs/pyutil/test/current/test_mathutil.py index 7c189dfb..da788758 100644 --- a/libs/pyutil/test/current/test_mathutil.py +++ b/libs/pyutil/test/current/test_mathutil.py @@ -42,6 +42,13 @@ class MathUtilTestCase(unittest.TestCase): self.failUnlessEqual(f(5, 3), 2) self.failUnlessEqual(f(6, 3), 2) self.failUnlessEqual(f(7, 3), 3) + self.failUnless(isinstance(f(0.0, 1), int)) + self.failUnlessEqual(f(7.0, 3.0), 3) + self.failUnlessEqual(f(7, 3.0), 3) + self.failUnlessEqual(f(7.0, 3), 3) + self.failUnlessEqual(f(6.0, 3.0), 2) + self.failUnlessEqual(f(6.0, 3), 2) + self.failUnlessEqual(f(6, 3.0), 2) def test_next_multiple(self): f = mathutil.next_multiple diff --git a/libs/pyutil/test/current/test_mathutil.py~ b/libs/pyutil/test/current/test_mathutil.py~ new file mode 100644 index 00000000..7c189dfb --- /dev/null +++ b/libs/pyutil/test/current/test_mathutil.py~ @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import unittest + +from pyutil import mathutil +from pyutil.assertutil import _assert + +class MathUtilTestCase(unittest.TestCase): + def _help_test_is_power_of_k(self, k): + for i in range(2, 40): + _assert(mathutil.is_power_of_k(k**i, k), k, i) + + def test_is_power_of_k(self): + for i in range(2, 5): + self._help_test_is_power_of_k(i) + + def test_log_ceil(self): + f = mathutil.log_ceil + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 2), 2) + + def test_log_floor(self): + f = mathutil.log_floor + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 0) + self.failUnlessEqual(f(3, 2), 1) + + def test_div_ceil(self): + f = mathutil.div_ceil + self.failUnlessEqual(f(0, 1), 0) + self.failUnlessEqual(f(0, 2), 0) + self.failUnlessEqual(f(0, 3), 0) + self.failUnlessEqual(f(1, 3), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 3), 1) + self.failUnlessEqual(f(4, 3), 2) + self.failUnlessEqual(f(5, 3), 2) + self.failUnlessEqual(f(6, 3), 2) + self.failUnlessEqual(f(7, 3), 3) + + def test_next_multiple(self): + f = mathutil.next_multiple + self.failUnlessEqual(f(5, 1), 5) + self.failUnlessEqual(f(5, 2), 6) + self.failUnlessEqual(f(5, 3), 6) + self.failUnlessEqual(f(5, 4), 8) + self.failUnlessEqual(f(5, 5), 5) + self.failUnlessEqual(f(5, 6), 6) + self.failUnlessEqual(f(32, 1), 32) + self.failUnlessEqual(f(32, 2), 32) + self.failUnlessEqual(f(32, 3), 33) + self.failUnlessEqual(f(32, 4), 32) + self.failUnlessEqual(f(32, 5), 35) + self.failUnlessEqual(f(32, 6), 36) + self.failUnlessEqual(f(32, 7), 35) + self.failUnlessEqual(f(32, 8), 32) + self.failUnlessEqual(f(32, 9), 36) + self.failUnlessEqual(f(32, 10), 40) + self.failUnlessEqual(f(32, 11), 33) + self.failUnlessEqual(f(32, 12), 36) + self.failUnlessEqual(f(32, 13), 39) + self.failUnlessEqual(f(32, 14), 42) + self.failUnlessEqual(f(32, 15), 45) + self.failUnlessEqual(f(32, 16), 32) + self.failUnlessEqual(f(32, 17), 34) + self.failUnlessEqual(f(32, 18), 36) + self.failUnlessEqual(f(32, 589), 589) + + def test_pad_size(self): + f = mathutil.pad_size + self.failUnlessEqual(f(0, 4), 0) + self.failUnlessEqual(f(1, 4), 3) + self.failUnlessEqual(f(2, 4), 2) + self.failUnlessEqual(f(3, 4), 1) + self.failUnlessEqual(f(4, 4), 0) + self.failUnlessEqual(f(5, 4), 3) + + def test_is_power_of_k_part_2(self): + f = mathutil.is_power_of_k + for i in range(1, 100): + if i in (1, 2, 4, 8, 16, 32, 64): + self.failUnless(f(i, 2), "but %d *is* a power of 2" % i) + else: + self.failIf(f(i, 2), "but %d is *not* a power of 2" % i) + for i in range(1, 100): + if i in (1, 3, 9, 27, 81): + self.failUnless(f(i, 3), "but %d *is* a power of 3" % i) + else: + self.failIf(f(i, 3), "but %d is *not* a power of 3" % i) + + def test_next_power_of_k(self): + f = mathutil.next_power_of_k + self.failUnlessEqual(f(0,2), 1) + self.failUnlessEqual(f(1,2), 1) + self.failUnlessEqual(f(2,2), 2) + self.failUnlessEqual(f(3,2), 4) + self.failUnlessEqual(f(4,2), 4) + for i in range(5, 8): self.failUnlessEqual(f(i,2), 8, "%d" % i) + for i in range(9, 16): self.failUnlessEqual(f(i,2), 16, "%d" % i) + for i in range(17, 32): self.failUnlessEqual(f(i,2), 32, "%d" % i) + for i in range(33, 64): self.failUnlessEqual(f(i,2), 64, "%d" % i) + for i in range(65, 100): self.failUnlessEqual(f(i,2), 128, "%d" % i) + + self.failUnlessEqual(f(0,3), 1) + self.failUnlessEqual(f(1,3), 1) + self.failUnlessEqual(f(2,3), 3) + self.failUnlessEqual(f(3,3), 3) + for i in range(4, 9): self.failUnlessEqual(f(i,3), 9, "%d" % i) + for i in range(10, 27): self.failUnlessEqual(f(i,3), 27, "%d" % i) + for i in range(28, 81): self.failUnlessEqual(f(i,3), 81, "%d" % i) + for i in range(82, 200): self.failUnlessEqual(f(i,3), 243, "%d" % i) + + def test_ave(self): + f = mathutil.ave + self.failUnlessEqual(f([1,2,3]), 2) + self.failUnlessEqual(f([0,0,0,4]), 1) + self.failUnlessAlmostEqual(f([0.0, 1.0, 1.0]), .666666666666) + + def failUnlessEqualContents(self, a, b): + self.failUnlessEqual(sorted(a), sorted(b)) + + def test_permute(self): + f = mathutil.permute + self.failUnlessEqualContents(f([]), []) + self.failUnlessEqualContents(f([1]), [[1]]) + self.failUnlessEqualContents(f([1,2]), [[1,2], [2,1]]) + self.failUnlessEqualContents(f([1,2,3]), + [[1,2,3], [1,3,2], + [2,1,3], [2,3,1], + [3,1,2], [3,2,1]]) diff --git a/libs/pyutil/time_comparisons.py b/libs/pyutil/time_comparisons.py deleted file mode 100644 index ee1bcfa0..00000000 --- a/libs/pyutil/time_comparisons.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyutil import benchutil - -import hashlib, random, os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeqcomp(a, b): - return a == b - -def hashcomp(a, b): - salt = os.urandom(32) - return hashlib.md5(salt+ a).digest() == hashlib.md5(salt+b).digest() - -N=10**4 -REPS=10**2 - -print "all times are in nanoseconds per comparison (scientific notation)" -print - -for comparator in [eqeqcomp, hashcomp]: - print "using comparator ", comparator - - # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: - trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] - random.shuffle(trials) - for (a, b, desc) in trials: - print "comparing two strings that are %s to each other" % (desc,) - - def f(n): - compare(n, comparator, a, b) - - benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - - print diff --git a/libs/pyutil/time_comparisons.py~ b/libs/pyutil/time_comparisons.py~ deleted file mode 100644 index abf151ad..00000000 --- a/libs/pyutil/time_comparisons.py~ +++ /dev/null @@ -1,72 +0,0 @@ -from pyutil import benchutil - -import hashlib -import os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeq(a, b): - return a == b - -def equalsequals_s(n): - # return compare(n, eqeq, - for i in xrange(n): - p1 == p1a - -def equalsequals_c(n): - for i in xrange(n): - p1 == p2 - -def equalsequals_f(n): - for i in xrange(n): - p1 == p3 - -def hash_s(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p1a).digest() - -def hash_c(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p2).digest() - -def hash_f(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p3).digest() - -N=10**4 -REPS=10**2 - -print "using '=='" - -print "same" -benchutil.rep_bench(equalsequals_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(equalsequals_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(equalsequals_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "using hash" - -print "same" -benchutil.rep_bench(hash_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(hash_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(hash_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 0cbfa7c0..a2cef356 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -549,7 +549,7 @@ class OAuth2Mixin(object): @return_future def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None, - callback=None): + callback=None, scope=None, response_type="code"): """Redirects the user to obtain OAuth authorization for this service. Some providers require that you register a redirect URL with @@ -566,10 +566,13 @@ class OAuth2Mixin(object): """ args = { "redirect_uri": redirect_uri, - "client_id": client_id + "client_id": client_id, + "response_type": response_type } if extra_params: args.update(extra_params) + if scope: + args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) callback() @@ -945,6 +948,67 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): return OpenIdMixin.get_authenticated_user(self) +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2.""" + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = 'google_oauth' + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Google user, returning a user object. + + Example usage:: + + class GoogleOAuth2LoginHandler(LoginHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.asynchronous + @tornado.gen.coroutine + def get(self): + if self.get_argument("code", False): + user = yield self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + yield self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings["google_consumer_key"], + scope=['openid', 'email'], + response_type='code', + extra_params={"approval_prompt": "auto"}) + """ + http = self.get_auth_http_client() + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, + "code": code, + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], + "grant_type": "authorization_code", + }) + + http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + self.async_callback(self._on_access_token, callback), + method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) + + def _on_access_token(self, future, response): + """Callback function for the exchange to the access token.""" + if response.error: + future.set_exception(AuthError('Google auth error: %s' % str(response))) + return + + args = escape.json_decode(response.body) + future.set_result(args) + + def get_auth_http_client(self): + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + class FacebookMixin(object): """Facebook Connect authentication. diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 05754299..79cccb49 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -16,11 +16,15 @@ """xAutomatically restart the server when a source file is modified. -Most applications should not access this module directly. Instead, pass the -keyword argument ``debug=True`` to the `tornado.web.Application` constructor. -This will enable autoreload mode as well as checking for changes to templates -and static resources. Note that restarting is a destructive operation -and any requests in progress will be aborted when the process restarts. +Most applications should not access this module directly. Instead, +pass the keyword argument ``autoreload=True`` to the +`tornado.web.Application` constructor (or ``debug=True``, which +enables this setting and several others). This will enable autoreload +mode as well as checking for changes to templates and static +resources. Note that restarting is a destructive operation and any +requests in progress will be aborted when the process restarts. (If +you want to disable autoreload while using other debug-mode features, +pass both ``debug=True`` and ``autoreload=False``). This module can also be used as a command-line wrapper around scripts such as unit test runners. See the `main` method for details. @@ -38,6 +42,7 @@ Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) because it re-executes Python using ``sys.executable`` and ``sys.argv``. Additionally, modifying these variables will cause reloading to behave incorrectly. + """ from __future__ import absolute_import, division, print_function, with_statement diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index e0900569..cb97710a 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -360,6 +360,7 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.PROXYUSERPWD, credentials) else: curl.setopt(pycurl.PROXY, '') + curl.unsetopt(pycurl.PROXYUSERPWD) if request.validate_cert: curl.setopt(pycurl.SSL_VERIFYPEER, 1) curl.setopt(pycurl.SSL_VERIFYHOST, 2) @@ -382,6 +383,8 @@ def _curl_setup_request(curl, request, buffer, headers): # that we can't reach, so allow ipv6 unless the user asks to disable. # (but see version check in _process_queue above) curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + else: + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER) # Set the request method through curl's irritating interface which makes # up names for almost every single method @@ -404,6 +407,11 @@ def _curl_setup_request(curl, request, buffer, headers): # Handle curl's cryptic options for every individual HTTP method if request.method in ("POST", "PUT"): + if request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % request.method) + request_buffer = BytesIO(utf8(request.body)) curl.setopt(pycurl.READFUNCTION, request_buffer.read) if request.method == "POST": @@ -414,6 +422,9 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.POSTFIELDSIZE, len(request.body)) else: curl.setopt(pycurl.INFILESIZE, len(request.body)) + elif request.method == "GET": + if request.body is not None: + raise AssertionError('Body must be empty for GET request') if request.auth_username is not None: userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index 92b7458e..217ebdf5 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -38,8 +38,8 @@ since it is both shorter and provides better exception handling):: def get(self): yield gen.Task(AsyncHTTPClient().fetch, "http://example.com") -You can also yield a list of ``Futures`` and/or ``Tasks``, which will be -started at the same time and run in parallel; a list of results will +You can also yield a list or dict of ``Futures`` and/or ``Tasks``, which will be +started at the same time and run in parallel; a list or dict of results will be returned when they are all finished:: @gen.coroutine @@ -47,6 +47,13 @@ be returned when they are all finished:: http_client = AsyncHTTPClient() response1, response2 = yield [http_client.fetch(url1), http_client.fetch(url2)] + response_dict = yield dict(response3=http_client.fetch(url3), + response4=http_client.fetch(url4)) + response3 = response_dict['response3'] + response4 = response_dict['response4'] + +.. versionchanged:: 3.2 + Dict support added. For more complicated interfaces, `Task` can be split into two parts: `Callback` and `Wait`:: @@ -404,6 +411,10 @@ class Multi(YieldPoint): a list of ``YieldPoints``. """ def __init__(self, children): + self.keys = None + if isinstance(children, dict): + self.keys = list(children.keys()) + children = children.values() self.children = [] for i in children: if isinstance(i, Future): @@ -423,7 +434,11 @@ class Multi(YieldPoint): return not self.unfinished_children def get_result(self): - return [i.get_result() for i in self.children] + result = (i.get_result() for i in self.children) + if self.keys is not None: + return dict(zip(self.keys, result)) + else: + return list(result) class _NullYieldPoint(YieldPoint): @@ -523,7 +538,7 @@ class Runner(object): self.finished = True self.yield_point = _null_yield_point raise - if isinstance(yielded, list): + if isinstance(yielded, (list, dict)): yielded = Multi(yielded) elif isinstance(yielded, Future): yielded = YieldFuture(yielded) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 67675894..b58a8348 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -282,7 +282,8 @@ class HTTPRequest(object): :arg int max_redirects: Limit for ``follow_redirects`` :arg string user_agent: String to send as ``User-Agent`` header :arg bool use_gzip: Request gzip encoding from the server - :arg string network_interface: Network interface to use for request + :arg string network_interface: Network interface to use for request. + ``curl_httpclient`` only; see note below. :arg callable streaming_callback: If set, ``streaming_callback`` will be run with each chunk of data as it is received, and ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in @@ -310,14 +311,26 @@ class HTTPRequest(object): :arg bool validate_cert: For HTTPS requests, validate the server's certificate? :arg string ca_certs: filename of CA certificates in PEM format, - or None to use defaults. Note that in ``curl_httpclient``, if - any request uses a custom ``ca_certs`` file, they all must (they - don't have to all use the same ``ca_certs``, but it's not possible - to mix requests with ``ca_certs`` and requests that use the defaults. + or None to use defaults. See note below when used with + ``curl_httpclient``. :arg bool allow_ipv6: Use IPv6 when available? Default is false in ``simple_httpclient`` and true in ``curl_httpclient`` - :arg string client_key: Filename for client SSL key, if any - :arg string client_cert: Filename for client SSL certificate, if any + :arg string client_key: Filename for client SSL key, if any. See + note below when used with ``curl_httpclient``. + :arg string client_cert: Filename for client SSL certificate, if any. + See note below when used with ``curl_httpclient``. + + .. note:: + + When using ``curl_httpclient`` certain options may be + inherited by subsequent fetches because ``pycurl`` does + not allow them to be cleanly reset. This applies to the + ``ca_certs``, ``client_key``, ``client_cert``, and + ``network_interface`` arguments. If you use these + options, you should pass them on every request (you don't + have to always use the same values, but it's not possible + to mix requests that specify these options with ones that + use the defaults). .. versionadded:: 3.1 The ``auth_mode`` argument. @@ -372,6 +385,9 @@ class HTTPResponse(object): * headers: `tornado.httputil.HTTPHeaders` object + * effective_url: final location of the resource after following any + redirects + * buffer: ``cStringIO`` object for response body * body: response body as string (created on demand from ``self.buffer``) diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index d005545e..34e7b768 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -29,6 +29,7 @@ from __future__ import absolute_import, division, print_function, with_statement import socket import ssl import time +import copy from tornado.escape import native_str, parse_qs_bytes from tornado import httputil @@ -326,8 +327,8 @@ class HTTPConnection(object): self.request_callback(self._request) except _BadRequestException as e: - gen_log.info("Malformed HTTP request from %s: %s", - self.address[0], e) + gen_log.info("Malformed HTTP request from %r: %s", + self.address, e) self.close() return @@ -336,7 +337,10 @@ class HTTPConnection(object): if self._request.method in ("POST", "PATCH", "PUT"): httputil.parse_body_arguments( self._request.headers.get("Content-Type", ""), data, - self._request.arguments, self._request.files) + self._request.body_arguments, self._request.files) + + for k, v in self._request.body_arguments.items(): + self._request.arguments.setdefault(k, []).extend(v) self.request_callback(self._request) @@ -403,6 +407,20 @@ class HTTPRequest(object): `.RequestHandler.get_argument`, which returns argument values as unicode strings. + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + .. attribute:: files File uploads are available in the files property, which maps file @@ -457,6 +475,8 @@ class HTTPRequest(object): self.path, sep, self.query = uri.partition('?') self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 3e7337d9..2575bc56 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -320,7 +320,11 @@ def parse_body_arguments(content_type, body, arguments, files): with the parsed contents. """ if content_type.startswith("application/x-www-form-urlencoded"): - uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + try: + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + except Exception as e: + gen_log.warning('Invalid x-www-form-urlencoded body: %s', e) + uri_arguments = {} for name, values in uri_arguments.items(): if values: arguments.setdefault(name, []).extend(values) diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 91ee2c5b..a36ab7a5 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -676,8 +676,7 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - if self._handlers.has_key(fd): - self._handlers[fd](fd, events) + self._handlers[fd](fd, events) except (OSError, IOError) as e: if e.args[0] == errno.EPIPE: # Happens when the client closes the connection diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 6bdc6397..08430cea 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -774,7 +774,7 @@ class IOStream(BaseIOStream): # Sometimes setsockopt will fail if the socket is closed # at the wrong time. This can happen with HTTPServer # resetting the value to false between requests. - if e.errno != errno.EINVAL: + if e.errno not in (errno.EINVAL, errno.ECONNRESET): raise diff --git a/libs/tornado/log.py b/libs/tornado/log.py index fa11f379..648db5c6 100755 --- a/libs/tornado/log.py +++ b/libs/tornado/log.py @@ -51,7 +51,7 @@ gen_log = logging.getLogger("tornado.general") def _stderr_supports_color(): color = False - if curses and sys.stderr.isatty(): + if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 9dc8506e..21db4755 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -20,7 +20,6 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os -import re import socket import ssl import stat @@ -30,6 +29,13 @@ from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec from tornado.util import Configurable +if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ + ssl_match_hostname = ssl.match_hostname + SSLCertificateError = ssl.CertificateError +else: + import backports.ssl_match_hostname + ssl_match_hostname = backports.ssl_match_hostname.match_hostname + SSLCertificateError = backports.ssl_match_hostname.CertificateError def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. @@ -391,73 +397,3 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): return context.wrap_socket(socket, **kwargs) else: return ssl.wrap_socket(socket, **dict(context, **kwargs)) - -if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ - ssl_match_hostname = ssl.match_hostname - SSLCertificateError = ssl.CertificateError -else: - # match_hostname was added to the standard library ssl module in python 3.2. - # The following code was backported for older releases and copied from - # https://bitbucket.org/brandon/backports.ssl_match_hostname - class SSLCertificateError(ValueError): - pass - - def _dnsname_to_pat(dn, max_wildcards=1): - pats = [] - for frag in dn.split(r'.'): - if frag.count('*') > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survery of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise SSLCertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - if frag == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - else: - # Otherwise, '*' matches any dotless fragment. - frag = re.escape(frag) - pats.append(frag.replace(r'\*', '[^.]*')) - return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - - def ssl_match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules - are mostly followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate") - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise SSLCertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise SSLCertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise SSLCertificateError("no appropriate commonName or " - "subjectAltName fields were found") diff --git a/libs/tornado/platform/asyncio.py b/libs/tornado/platform/asyncio.py new file mode 100644 index 00000000..a8f5bad4 --- /dev/null +++ b/libs/tornado/platform/asyncio.py @@ -0,0 +1,134 @@ +"""Bridges between the `asyncio` module and Tornado IOLoop. + +This is a work in progress and interfaces are subject to change. + +To test: +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOLoop +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOMainLoop +(the tests log a few warnings with AsyncIOMainLoop because they leave some +unfinished callbacks on the event loop that fail when it resumes) +""" +import asyncio +import datetime +import functools +import os + +from tornado.ioloop import IOLoop +from tornado import stack_context + +class BaseAsyncIOLoop(IOLoop): + def initialize(self, asyncio_loop, close_loop=False): + self.asyncio_loop = asyncio_loop + self.close_loop = close_loop + self.asyncio_loop.call_soon(self.make_current) + # Maps fd to handler function (as in IOLoop.add_handler) + self.handlers = {} + # Set of fds listening for reads/writes + self.readers = set() + self.writers = set() + self.closing = False + + def close(self, all_fds=False): + self.closing = True + for fd in list(self.handlers): + self.remove_handler(fd) + if all_fds: + os.close(fd) + if self.close_loop: + self.asyncio_loop.close() + + def add_handler(self, fd, handler, events): + if fd in self.handlers: + raise ValueError("fd %d added twice" % fd) + self.handlers[fd] = stack_context.wrap(handler) + if events & IOLoop.READ: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + if events & IOLoop.WRITE: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + + def update_handler(self, fd, events): + if events & IOLoop.READ: + if fd not in self.readers: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + else: + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if events & IOLoop.WRITE: + if fd not in self.writers: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + else: + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + + def remove_handler(self, fd): + if fd not in self.handlers: + return + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + del self.handlers[fd] + + def _handle_events(self, fd, events): + self.handlers[fd](fd, events) + + def start(self): + self.asyncio_loop.run_forever() + + def stop(self): + self.asyncio_loop.stop() + + def _run_callback(self, callback, *args, **kwargs): + try: + callback(*args, **kwargs) + except Exception: + self.handle_callback_exception(callback) + + def add_timeout(self, deadline, callback): + if isinstance(deadline, (int, float)): + delay = max(deadline - self.time(), 0) + elif isinstance(deadline, datetime.timedelta): + delay = deadline.total_seconds() + else: + raise TypeError("Unsupported deadline %r", deadline) + return self.asyncio_loop.call_later(delay, self._run_callback, + stack_context.wrap(callback)) + + def remove_timeout(self, timeout): + timeout.cancel() + + def add_callback(self, callback, *args, **kwargs): + if self.closing: + raise RuntimeError("IOLoop is closing") + if kwargs: + self.asyncio_loop.call_soon_threadsafe(functools.partial( + self._run_callback, stack_context.wrap(callback), + *args, **kwargs)) + else: + self.asyncio_loop.call_soon_threadsafe( + self._run_callback, stack_context.wrap(callback), *args) + + add_callback_from_signal = add_callback + + +class AsyncIOMainLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(), + close_loop=False) + +class AsyncIOLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOLoop, self).initialize(asyncio.new_event_loop(), + close_loop=True) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index ffd2d29d..942c5c3f 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -92,7 +92,8 @@ def fork_processes(num_processes, max_restarts=100): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the debug=True option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``fork_processes``. diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index d8dbb271..2558ada8 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -72,6 +72,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.max_clients = max_clients self.queue = collections.deque() self.active = {} + self.waiting = {} self.max_buffer_size = max_buffer_size if resolver: self.resolver = resolver @@ -89,7 +90,16 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.resolver.close() def fetch_impl(self, request, callback): - self.queue.append((request, callback)) + key = object() + self.queue.append((key, request, callback)) + if not len(self.active) < self.max_clients: + timeout_handle = self.io_loop.add_timeout( + self.io_loop.time() + min(request.connect_timeout, + request.request_timeout), + functools.partial(self._on_timeout, key)) + else: + timeout_handle = None + self.waiting[key] = (request, callback, timeout_handle) self._process_queue() if self.queue: gen_log.debug("max_clients limit reached, request queued. " @@ -99,8 +109,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _process_queue(self): with stack_context.NullContext(): while self.queue and len(self.active) < self.max_clients: - request, callback = self.queue.popleft() - key = object() + key, request, callback = self.queue.popleft() + if key not in self.waiting: + continue + self._remove_timeout(key) self.active[key] = (request, callback) release_callback = functools.partial(self._release_fetch, key) self._handle_request(request, release_callback, callback) @@ -113,6 +125,22 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): del self.active[key] self._process_queue() + def _remove_timeout(self, key): + if key in self.waiting: + request, callback, timeout_handle = self.waiting[key] + if timeout_handle is not None: + self.io_loop.remove_timeout(timeout_handle) + del self.waiting[key] + + def _on_timeout(self, key): + request, callback, timeout_handle = self.waiting[key] + self.queue.remove((key, request, callback)) + timeout_response = HTTPResponse( + request, 599, error=HTTPError(599, "Timeout"), + request_time=self.io_loop.time() - request.start_time) + self.io_loop.add_callback(callback, timeout_response) + del self.waiting[key] + class _HTTPConnection(object): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) @@ -162,15 +190,18 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) self.resolver.resolve(host, port, af, callback=self._on_resolve) def _on_resolve(self, addrinfo): + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout + return self.stream = self._create_stream(addrinfo) - timeout = min(self.request.connect_timeout, self.request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) self.stream.set_close_callback(self._on_close) # ipv6 addresses are broken (in self.parsed.hostname) until # 2.7, here is correctly parsed value calculated in __init__ @@ -199,10 +230,10 @@ class _HTTPConnection(object): # the SSL_OP_NO_SSLv2, but that wasn't exposed to python # until 3.2. Python 2.7 adds the ciphers argument, which # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to SSLv3. This is + # on python 2.6, we set ssl_version to TLSv1. This is # more narrow than we'd like since it also breaks - # compatibility with servers configured for TLSv1 only, - # but nearly all servers support SSLv3: + # compatibility with servers configured for SSLv3 only, + # but nearly all servers support both SSLv3 and TLSv1: # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html if sys.version_info >= (2, 7): ssl_options["ciphers"] = "DEFAULT:!SSLv2" @@ -210,7 +241,7 @@ class _HTTPConnection(object): # This is really only necessary for pre-1.0 versions # of openssl, but python 2.6 doesn't expose version # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 return SSLIOStream(socket.socket(af), io_loop=self.io_loop, @@ -233,6 +264,8 @@ class _HTTPConnection(object): def _on_connect(self): self._remove_timeout() + if self.final_callback is None: + return if self.request.request_timeout: self._timeout = self.io_loop.add_timeout( self.start_time + self.request.request_timeout, @@ -269,9 +302,15 @@ class _HTTPConnection(object): self.request.headers["User-Agent"] = self.request.user_agent if not self.request.allow_nonstandard_methods: if self.request.method in ("POST", "PATCH", "PUT"): - assert self.request.body is not None + if self.request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % self.request.method) else: - assert self.request.body is None + if self.request.body is not None: + raise AssertionError( + 'Body must be empty for "%s" request' + % self.request.method) if self.request.body is not None: self.request.headers["Content-Length"] = str(len( self.request.body)) diff --git a/libs/tornado/speedups.c b/libs/tornado/speedups.c new file mode 100644 index 00000000..8a316c58 --- /dev/null +++ b/libs/tornado/speedups.c @@ -0,0 +1,49 @@ +#include + +static PyObject* websocket_mask(PyObject* self, PyObject* args) { + const char* mask; + int mask_len; + const char* data; + int data_len; + int i; + + if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { + return NULL; + } + + PyObject* result = PyBytes_FromStringAndSize(NULL, data_len); + if (!result) { + return NULL; + } + char* buf = PyBytes_AsString(result); + for (i = 0; i < data_len; i++) { + buf[i] = data[i] ^ mask[i % 4]; + } + + return result; +} + +static PyMethodDef methods[] = { + {"websocket_mask", websocket_mask, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef speedupsmodule = { + PyModuleDef_HEAD_INIT, + "speedups", + NULL, + -1, + methods +}; + +PyMODINIT_FUNC +PyInit_speedups() { + return PyModule_Create(&speedupsmodule); +} +#else // Python 2.x +PyMODINIT_FUNC +initspeedups() { + Py_InitModule("tornado.speedups", methods); +} +#endif diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index 8473a21a..c0773732 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -180,7 +180,8 @@ class TCPServer(object): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the ``debug=True`` option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``TCPServer.start(n)``. """ diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 5f8d6091..b6d7e97e 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -250,7 +250,7 @@ class RequestHandler(object): not self.request.connection.no_keep_alive): conn_header = self.request.headers.get("Connection") if conn_header and (conn_header.lower() == "keep-alive"): - self.set_header("Connection", "Keep-Alive") + self._headers["Connection"] = "Keep-Alive" self._write_buffer = [] self._status_code = 200 self._reason = httputil.responses[200] @@ -348,12 +348,7 @@ class RequestHandler(object): The returned value is always unicode. """ - args = self.get_arguments(name, strip=strip) - if not args: - if default is self._ARG_DEFAULT: - raise MissingArgumentError(name) - return default - return args[-1] + return self._get_argument(name, default, self.request.arguments, strip) def get_arguments(self, name, strip=True): """Returns a list of the arguments with the given name. @@ -362,9 +357,73 @@ class RequestHandler(object): The returned values are always unicode. """ + return self._get_arguments(name, self.request.arguments, strip) + def get_body_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request body. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.body_arguments, strip) + + def get_body_arguments(self, name, strip=True): + """Returns a list of the body arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.body_arguments, strip) + + def get_query_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request query string. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.query_arguments, strip) + + def get_query_arguments(self, name, strip=True): + """Returns a list of the query arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.query_arguments, strip) + + def _get_argument(self, name, default, source, strip=True): + args = self._get_arguments(name, source, strip=strip) + if not args: + if default is self._ARG_DEFAULT: + raise MissingArgumentError(name) + return default + return args[-1] + + def _get_arguments(self, name, source, strip=True): values = [] - for v in self.request.arguments.get(name, []): + for v in source.get(name, []): v = self.decode_argument(v, name=name) if isinstance(v, unicode_type): # Get rid of any weird control chars (unless decoding gave @@ -838,7 +897,7 @@ class RequestHandler(object): else: self.finish(self.get_error_html(status_code, **kwargs)) return - if self.settings.get("debug") and "exc_info" in kwargs: + if self.settings.get("serve_traceback") and "exc_info" in kwargs: # in debug mode, try to send a traceback self.set_header('Content-Type', 'text/plain') for line in traceback.format_exception(*kwargs["exc_info"]): @@ -1318,6 +1377,12 @@ def asynchronous(method): if not self._finished: self.finish() IOLoop.current().add_future(result, future_complete) + # Once we have done this, hide the Future from our + # caller (i.e. RequestHandler._when_complete), which + # would otherwise set up its own callback and + # exception handler (resulting in exceptions being + # logged twice). + return None return result return wrapper @@ -1383,10 +1448,16 @@ class Application(object): or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. + The request class can be specified as either a class object or a + (fully-qualified) name. - Each tuple can contain an optional third element, which should be - a dictionary if it is present. That dictionary is passed as - keyword arguments to the contructor of the handler. This pattern + Each tuple can contain additional elements, which correspond to the + arguments to the `URLSpec` constructor. (Prior to Tornado 3.2, this + only tuples of two or three elements were allowed). + + A dictionary may be passed as the third element of the tuple, + which will be used as keyword arguments to the handler's + constructor and `~RequestHandler.initialize` method. This pattern is used for the `StaticFileHandler` in this example (note that a `StaticFileHandler` can be installed automatically with the static_path setting described below):: @@ -1409,6 +1480,7 @@ class Application(object): and ``/robots.txt`` from the same directory. A custom subclass of `StaticFileHandler` can be specified with the ``static_handler_class`` setting. + """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1447,8 +1519,14 @@ class Application(object): if handlers: self.add_handlers(".*$", handlers) + if self.settings.get('debug'): + self.settings.setdefault('autoreload', True) + self.settings.setdefault('compiled_template_cache', False) + self.settings.setdefault('static_hash_cache', False) + self.settings.setdefault('serve_traceback', True) + # Automatically reload modified modules - if self.settings.get("debug") and not wsgi: + if self.settings.get('autoreload') and not wsgi: from tornado import autoreload autoreload.start() @@ -1493,20 +1571,8 @@ class Application(object): for spec in host_handlers: if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3) - pattern = spec[0] - handler = spec[1] - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - if len(spec) == 3: - kwargs = spec[2] - else: - kwargs = {} - spec = URLSpec(pattern, handler, kwargs) + assert len(spec) in (2, 3, 4) + spec = URLSpec(*spec) handlers.append(spec) if spec.name: if spec.name in self.named_handlers: @@ -1597,14 +1663,23 @@ class Application(object): args = [unquote(s) for s in match.groups()] break if not handler: - handler = ErrorHandler(self, request, status_code=404) + if self.settings.get('default_handler_class'): + handler_class = self.settings['default_handler_class'] + handler_args = self.settings.get( + 'default_handler_args', {}) + else: + handler_class = ErrorHandler + handler_args = dict(status_code=404) + handler = handler_class(self, request, **handler_args) - # In debug mode, re-compile templates and reload static files on every + # If template cache is disabled (usually in the debug mode), + # re-compile templates and reload static files on every # request so you don't need to restart to see changes - if self.settings.get("debug"): + if not self.settings.get("compiled_template_cache", True): with RequestHandler._template_loader_lock: for loader in RequestHandler._template_loaders.values(): loader.reset() + if not self.settings.get('static_hash_cache', True): StaticFileHandler.reset() handler._execute(transforms, *args, **kwargs) @@ -2454,7 +2529,7 @@ class _UIModuleNamespace(object): class URLSpec(object): """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler_class, kwargs=None, name=None): + def __init__(self, pattern, handler, kwargs=None, name=None): """Parameters: * ``pattern``: Regular expression to be matched. Any groups @@ -2475,7 +2550,13 @@ class URLSpec(object): assert len(self.regex.groupindex) in (0, self.regex.groups), \ ("groups in url regexes must either be all named or all " "positional: %r" % self.regex.pattern) - self.handler_class = handler_class + + if isinstance(handler, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + handler = import_object(handler) + + self.handler_class = handler self.kwargs = kwargs or {} self.name = name self._path, self._group_count = self._find_groups() diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 676d21bf..8c2f5a64 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -33,7 +33,7 @@ import tornado.web from tornado.concurrent import TracebackFuture from tornado.escape import utf8, native_str -from tornado import httpclient +from tornado import httpclient, httputil from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log @@ -52,6 +52,10 @@ class WebSocketError(Exception): class WebSocketClosedError(WebSocketError): + """Raised by operations on a closed connection. + + .. versionadded:: 3.2 + """ pass @@ -163,6 +167,12 @@ class WebSocketHandler(tornado.web.RequestHandler): encoded as json). If the ``binary`` argument is false, the message will be sent as utf8; in binary mode any byte string is allowed. + + If the connection is already closed, raises `WebSocketClosedError`. + + .. versionchanged:: 3.2 + `WebSocketClosedError` was added (previously a closed connection + would raise an `AttributeError`) """ if self.ws_connection is None: raise WebSocketClosedError() @@ -586,7 +596,7 @@ class WebSocketProtocol13(WebSocketProtocol): frame += struct.pack("!BQ", 127 | mask_bit, l) if self.mask_outgoing: mask = os.urandom(4) - data = mask + self._apply_mask(mask, data) + data = mask + _websocket_mask(mask, data) frame += data self.stream.write(frame) @@ -671,21 +681,8 @@ class WebSocketProtocol13(WebSocketProtocol): except StreamClosedError: self._abort() - def _apply_mask(self, mask, data): - mask = array.array("B", mask) - unmasked = array.array("B", data) - for i in xrange(len(data)): - unmasked[i] = unmasked[i] ^ mask[i % 4] - if hasattr(unmasked, 'tobytes'): - # tostring was deprecated in py32. It hasn't been removed, - # but since we turn on deprecation warnings in our tests - # we need to use the right one. - return unmasked.tobytes() - else: - return unmasked.tostring() - def _on_masked_frame_data(self, data): - self._on_frame_data(self._apply_mask(self._frame_mask, data)) + self._on_frame_data(_websocket_mask(self._frame_mask, data)) def _on_frame_data(self, data): if self._frame_opcode_is_control: @@ -771,7 +768,11 @@ class WebSocketProtocol13(WebSocketProtocol): class WebSocketClientConnection(simple_httpclient._HTTPConnection): - """WebSocket client connection.""" + """WebSocket client connection. + + This class should not be instantiated directly; use the + `websocket_connect` function instead. + """ def __init__(self, io_loop, request): self.connect_future = TracebackFuture() self.read_future = None @@ -793,9 +794,19 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): io_loop, None, request, lambda: None, self._on_http_response, 104857600, self.resolver) + def close(self): + """Closes the websocket connection. + + .. versionadded:: 3.2 + """ + if self.protocol is not None: + self.protocol.close() + self.protocol = None + def _on_close(self): self.on_message(None) self.resolver.close() + super(WebSocketClientConnection, self)._on_close() def _on_http_response(self, response): if not self.connect_future.done(): @@ -859,13 +870,54 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): Takes a url and returns a Future whose result is a `WebSocketClientConnection`. + + .. versionchanged:: 3.2 + Also accepts ``HTTPRequest`` objects in place of urls. """ if io_loop is None: io_loop = IOLoop.current() - request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) + if isinstance(url, httpclient.HTTPRequest): + assert connect_timeout is None + request = url + # Copy and convert the headers dict/object (see comments in + # AsyncHTTPClient.fetch) + request.headers = httputil.HTTPHeaders(request.headers) + else: + request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) request = httpclient._RequestProxy( request, httpclient.HTTPRequest._DEFAULTS) conn = WebSocketClientConnection(io_loop, request) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future + +def _websocket_mask_python(mask, data): + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. + Returns a `bytes` object of the same length as `data` with the mask applied + as specified in section 5.3 of RFC 6455. + + This pure-python implementation may be replaced by an optimized version when available. + """ + mask = array.array("B", mask) + unmasked = array.array("B", data) + for i in xrange(len(data)): + unmasked[i] = unmasked[i] ^ mask[i % 4] + if hasattr(unmasked, 'tobytes'): + # tostring was deprecated in py32. It hasn't been removed, + # but since we turn on deprecation warnings in our tests + # we need to use the right one. + return unmasked.tobytes() + else: + return unmasked.tostring() + +if os.environ.get('TORNADO_NO_EXTENSION'): + # This environment variable exists to make it easier to do performance comparisons; + # it's not guaranteed to remain supported in the future. + _websocket_mask = _websocket_mask_python +else: + try: + from tornado.speedups import websocket_mask as _websocket_mask + except ImportError: + _websocket_mask = _websocket_mask_python diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 5e25a564..8e5ddedb 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -33,6 +33,7 @@ from __future__ import absolute_import, division, print_function, with_statement import sys import time +import copy import tornado from tornado import escape @@ -142,11 +143,14 @@ class HTTPRequest(object): self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) self.uri = self.path self.arguments = {} + self.query_arguments = {} + self.body_arguments = {} self.query = environ.get("QUERY_STRING", "") if self.query: self.uri += "?" + self.query self.arguments = parse_qs_bytes(native_str(self.query), keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) self.version = "HTTP/1.1" self.headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): @@ -171,7 +175,10 @@ class HTTPRequest(object): # Parse request body self.files = {} httputil.parse_body_arguments(self.headers.get("Content-Type", ""), - self.body, self.arguments, self.files) + self.body, self.body_arguments, self.files) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) self._start_time = time.time() self._finish_time = None
20010504
+ # Keep a ref to this for special handling of whitespace in self.processSpaceCharactersNonPre = self.processSpaceCharacters self.startTagHandler = utils.MethodDispatcher([ ("html", self.startTagHtml), - (("base", "basefont", "bgsound", "command", "link", "meta", - "noframes", "script", "style", "title"), + (("base", "basefont", "bgsound", "command", "link", "meta", + "noframes", "script", "style", "title"), self.startTagProcessInHead), ("body", self.startTagBody), ("frameset", self.startTagFrameset), (("address", "article", "aside", "blockquote", "center", "details", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "menu", "nav", "ol", "p", + "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", "section", "summary", "ul"), - self.startTagCloseP), + self.startTagCloseP), (headingElements, self.startTagHeading), (("pre", "listing"), self.startTagPreListing), ("form", self.startTagForm), (("li", "dd", "dt"), self.startTagListItem), - ("plaintext",self.startTagPlaintext), + ("plaintext", self.startTagPlaintext), ("a", self.startTagA), - (("b", "big", "code", "em", "font", "i", "s", "small", "strike", - "strong", "tt", "u"),self.startTagFormatting), + (("b", "big", "code", "em", "font", "i", "s", "small", "strike", + "strong", "tt", "u"), self.startTagFormatting), ("nobr", self.startTagNobr), ("button", self.startTagButton), (("applet", "marquee", "object"), self.startTagAppletMarqueeObject), @@ -961,21 +912,21 @@ def getPhases(debug): self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("body",self.endTagBody), - ("html",self.endTagHtml), - (("address", "article", "aside", "blockquote", "center", - "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", + ("body", self.endTagBody), + ("html", self.endTagHtml), + (("address", "article", "aside", "blockquote", "button", "center", + "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "section", "summary", "ul"), self.endTagBlock), ("form", self.endTagForm), - ("p",self.endTagP), + ("p", self.endTagP), (("dd", "dt", "li"), self.endTagListItem), (headingElements, self.endTagHeading), (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"), self.endTagFormatting), - (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), + (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), ("br", self.endTagBr), - ]) + ]) self.endTagHandler.default = self.endTagOther def isMatchingFormattingElement(self, node1, node2): @@ -995,14 +946,14 @@ def getPhases(debug): def addFormattingElement(self, token): self.tree.insertElement(token) element = self.tree.openElements[-1] - + matchingElements = [] for node in self.tree.activeFormattingElements[::-1]: if node is Marker: break elif self.isMatchingFormattingElement(node, element): matchingElements.append(node) - + assert len(matchingElements) <= 3 if len(matchingElements) == 3: self.tree.activeFormattingElements.remove(matchingElements[-1]) @@ -1017,7 +968,7 @@ def getPhases(debug): if node.name not in allowed_elements: self.parser.parseError("expected-closing-tag-but-got-eof") break - #Stop parsing + # Stop parsing def processSpaceCharactersDropNewline(self, token): # Sometimes (start of , , and blocks) we @@ -1026,19 +977,19 @@ def getPhases(debug): self.processSpaceCharacters = self.processSpaceCharactersNonPre if (data.startswith("\n") and self.tree.openElements[-1].name in ("pre", "listing", "textarea") - and not self.tree.openElements[-1].hasContent()): + and not self.tree.openElements[-1].hasContent()): data = data[1:] if data: self.tree.reconstructActiveFormattingElements() self.tree.insertText(data) def processCharacters(self, token): - if token["data"] == u"\u0000": - #The tokenizer should always emit null on its own + if token["data"] == "\u0000": + # The tokenizer should always emit null on its own return self.tree.reconstructActiveFormattingElements() self.tree.insertText(token["data"]) - #This must be bad for performance + # This must be bad for performance if (self.parser.framesetOK and any([char not in spaceCharacters for char in token["data"]])): @@ -1054,11 +1005,11 @@ def getPhases(debug): def startTagBody(self, token): self.parser.parseError("unexpected-start-tag", {"name": "body"}) if (len(self.tree.openElements) == 1 - or self.tree.openElements[1].name != "body"): + or self.tree.openElements[1].name != "body"): assert self.parser.innerHTML else: self.parser.framesetOK = False - for attr, value in token["data"].iteritems(): + for attr, value in token["data"].items(): if attr not in self.tree.openElements[1].attributes: self.tree.openElements[1].attributes[attr] = value @@ -1090,7 +1041,7 @@ def getPhases(debug): def startTagForm(self, token): if self.tree.formPointer: - self.parser.parseError(u"unexpected-start-tag", {"name": "form"}) + self.parser.parseError("unexpected-start-tag", {"name": "form"}) else: if self.tree.elementInScope("p", variant="button"): self.endTagP(impliedTagToken("p")) @@ -1100,9 +1051,9 @@ def getPhases(debug): def startTagListItem(self, token): self.parser.framesetOK = False - stopNamesMap = {"li":["li"], - "dt":["dt", "dd"], - "dd":["dt", "dd"]} + stopNamesMap = {"li": ["li"], + "dt": ["dt", "dd"], + "dd": ["dt", "dd"]} stopNames = stopNamesMap[token["name"]] for node in reversed(self.tree.openElements): if node.name in stopNames: @@ -1110,7 +1061,7 @@ def getPhases(debug): impliedTagToken(node.name, "EndTag")) break if (node.nameTuple in specialElements and - node.name not in ("address", "div", "p")): + node.name not in ("address", "div", "p")): break if self.tree.elementInScope("p", variant="button"): @@ -1137,7 +1088,7 @@ def getPhases(debug): afeAElement = self.tree.elementInActiveFormattingElements("a") if afeAElement: self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "a", "endName": "a"}) + {"startName": "a", "endName": "a"}) self.endTagFormatting(impliedTagToken("a")) if afeAElement in self.tree.openElements: self.tree.openElements.remove(afeAElement) @@ -1154,7 +1105,7 @@ def getPhases(debug): self.tree.reconstructActiveFormattingElements() if self.tree.elementInScope("nobr"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "nobr", "endName": "nobr"}) + {"startName": "nobr", "endName": "nobr"}) self.processEndTag(impliedTagToken("nobr")) # XXX Need tests that trigger the following self.tree.reconstructActiveFormattingElements() @@ -1163,7 +1114,7 @@ def getPhases(debug): def startTagButton(self, token): if self.tree.elementInScope("button"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "button", "endName": "button"}) + {"startName": "button", "endName": "button"}) self.processEndTag(impliedTagToken("button")) return token else: @@ -1203,8 +1154,8 @@ def getPhases(debug): framesetOK = self.parser.framesetOK self.startTagVoidFormatting(token) if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): - #input type=hidden doesn't change framesetOK + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + # input type=hidden doesn't change framesetOK self.parser.framesetOK = framesetOK def startTagParamSource(self, token): @@ -1223,7 +1174,7 @@ def getPhases(debug): def startTagImage(self, token): # No really... self.parser.parseError("unexpected-start-tag-treated-as", - {"originalName": "image", "newName": "img"}) + {"originalName": "image", "newName": "img"}) self.processStartTag(impliedTagToken("img", "StartTag", attributes=token["data"], selfClosing=token["selfClosing"])) @@ -1243,18 +1194,18 @@ def getPhases(debug): if "prompt" in token["data"]: prompt = token["data"]["prompt"] else: - prompt = u"This is a searchable index. Enter search keywords: " + prompt = "This is a searchable index. Enter search keywords: " self.processCharacters( - {"type":tokenTypes["Characters"], "data":prompt}) + {"type": tokenTypes["Characters"], "data": prompt}) attributes = token["data"].copy() if "action" in attributes: del attributes["action"] if "prompt" in attributes: del attributes["prompt"] attributes["name"] = "isindex" - self.processStartTag(impliedTagToken("input", "StartTag", - attributes = attributes, - selfClosing = + self.processStartTag(impliedTagToken("input", "StartTag", + attributes=attributes, + selfClosing= token["selfClosing"])) self.processEndTag(impliedTagToken("label")) self.processStartTag(impliedTagToken("hr", "StartTag")) @@ -1287,7 +1238,7 @@ def getPhases(debug): if self.parser.phase in (self.parser.phases["inTable"], self.parser.phases["inCaption"], self.parser.phases["inColumnGroup"], - self.parser.phases["inTableBody"], + self.parser.phases["inTableBody"], self.parser.phases["inRow"], self.parser.phases["inCell"]): self.parser.phase = self.parser.phases["inSelectInTable"] @@ -1307,8 +1258,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["mathml"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1319,8 +1270,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["svg"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1362,7 +1313,7 @@ def getPhases(debug): "tbody", "td", "tfoot", "th", "thead", "tr", "body", "html")): - #Not sure this is the correct name for the parse error + # Not sure this is the correct name for the parse error self.parser.parseError( "expected-one-end-tag-but-got-another", {"expectedName": "body", "gotName": node.name}) @@ -1370,20 +1321,20 @@ def getPhases(debug): self.parser.phase = self.parser.phases["afterBody"] def endTagHtml(self, token): - #We repeat the test for the body end tag token being ignored here + # We repeat the test for the body end tag token being ignored here if self.tree.elementInScope("body"): self.endTagBody(impliedTagToken("body")) return token def endTagBlock(self, token): - #Put us back in the right whitespace handling mode + # Put us back in the right whitespace handling mode if token["name"] == "pre": self.processSpaceCharacters = self.processSpaceCharactersNonPre inScope = self.tree.elementInScope(token["name"]) if inScope: self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != token["name"]: - self.parser.parseError("end-tag-too-early", {"name": token["name"]}) + self.parser.parseError("end-tag-too-early", {"name": token["name"]}) if inScope: node = self.tree.openElements.pop() while node.name != token["name"]: @@ -1394,7 +1345,7 @@ def getPhases(debug): self.tree.formPointer = None if node is None or not self.tree.elementInScope(node): self.parser.parseError("unexpected-end-tag", - {"name":"form"}) + {"name": "form"}) else: self.tree.generateImpliedEndTags() if self.tree.openElements[-1] != node: @@ -1410,7 +1361,7 @@ def getPhases(debug): if not self.tree.elementInScope(token["name"], variant=variant): self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) else: - self.tree.generateImpliedEndTags(exclude = token["name"]) + self.tree.generateImpliedEndTags(exclude=token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError( "end-tag-too-early", @@ -1436,65 +1387,105 @@ def getPhases(debug): def endTagFormatting(self, token): """The much-feared adoption agency algorithm""" - # http://www.whatwg.org/specs/web-apps/current-work/#adoptionAgency + # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867 # XXX Better parseError messages appreciated. - name = token["name"] + # Step 1 outerLoopCounter = 0 + + # Step 2 while outerLoopCounter < 8: + + # Step 3 outerLoopCounter += 1 - # Step 1 paragraph 1 + # Step 4: + + # Let the formatting element be the last element in + # the list of active formatting elements that: + # - is between the end of the list and the last scope + # marker in the list, if any, or the start of the list + # otherwise, and + # - has the same tag name as the token. formattingElement = self.tree.elementInActiveFormattingElements( token["name"]) - if (not formattingElement or + if (not formattingElement or (formattingElement in self.tree.openElements and not self.tree.elementInScope(formattingElement.name))): - self.parser.parseError("adoption-agency-1.1", {"name": token["name"]}) + # If there is no such node, then abort these steps + # and instead act as described in the "any other + # end tag" entry below. + self.endTagOther(token) return - # Step 1 paragraph 2 + # Otherwise, if there is such a node, but that node is + # not in the stack of open elements, then this is a + # parse error; remove the element from the list, and + # abort these steps. elif formattingElement not in self.tree.openElements: self.parser.parseError("adoption-agency-1.2", {"name": token["name"]}) self.tree.activeFormattingElements.remove(formattingElement) return - # Step 1 paragraph 3 - if formattingElement != self.tree.openElements[-1]: - self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + # Otherwise, if there is such a node, and that node is + # also in the stack of open elements, but the element + # is not in scope, then this is a parse error; ignore + # the token, and abort these steps. + elif not self.tree.elementInScope(formattingElement.name): + self.parser.parseError("adoption-agency-4.4", {"name": token["name"]}) + return - # Step 2 - # Start of the adoption agency algorithm proper + # Otherwise, there is a formatting element and that + # element is in the stack and is in scope. If the + # element is not the current node, this is a parse + # error. In any case, proceed with the algorithm as + # written in the following steps. + else: + if formattingElement != self.tree.openElements[-1]: + self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + + # Step 5: + + # Let the furthest block be the topmost node in the + # stack of open elements that is lower in the stack + # than the formatting element, and is an element in + # the special category. There might not be one. afeIndex = self.tree.openElements.index(formattingElement) furthestBlock = None for element in self.tree.openElements[afeIndex:]: if element.nameTuple in specialElements: furthestBlock = element break - # Step 3 + + # Step 6: + + # If there is no furthest block, then the UA must + # first pop all the nodes from the bottom of the stack + # of open elements, from the current node up to and + # including the formatting element, then remove the + # formatting element from the list of active + # formatting elements, and finally abort these steps. if furthestBlock is None: element = self.tree.openElements.pop() while element != formattingElement: element = self.tree.openElements.pop() self.tree.activeFormattingElements.remove(element) return - commonAncestor = self.tree.openElements[afeIndex-1] - # Step 5 - #if furthestBlock.parent: - # furthestBlock.parent.removeChild(furthestBlock) + # Step 7 + commonAncestor = self.tree.openElements[afeIndex - 1] - # Step 5 + # Step 8: # The bookmark is supposed to help us identify where to reinsert - # nodes in step 12. We have to ensure that we reinsert nodes after + # nodes in step 15. We have to ensure that we reinsert nodes after # the node before the active formatting element. Note the bookmark - # can move in step 7.4 + # can move in step 9.7 bookmark = self.tree.activeFormattingElements.index(formattingElement) - # Step 6 + # Step 9 lastNode = node = furthestBlock innerLoopCounter = 0 - + index = self.tree.openElements.index(node) while innerLoopCounter < 3: innerLoopCounter += 1 @@ -1504,15 +1495,13 @@ def getPhases(debug): if node not in self.tree.activeFormattingElements: self.tree.openElements.remove(node) continue - # Step 6.3 + # Step 9.6 if node == formattingElement: break - # Step 6.4 + # Step 9.7 if lastNode == furthestBlock: - bookmark = (self.tree.activeFormattingElements.index(node) - + 1) - # Step 6.5 - #cite = node.parent + bookmark = self.tree.activeFormattingElements.index(node) + 1 + # Step 9.8 clone = node.cloneNode() # Replace node with clone self.tree.activeFormattingElements[ @@ -1520,20 +1509,18 @@ def getPhases(debug): self.tree.openElements[ self.tree.openElements.index(node)] = clone node = clone - - # Step 6.6 + # Step 9.9 # Remove lastNode from its parents, if any if lastNode.parent: lastNode.parent.removeChild(lastNode) node.appendChild(lastNode) - # Step 7.7 + # Step 9.10 lastNode = node - # End of inner loop - # Step 7 + # Step 10 # Foster parent lastNode if commonAncestor is a - # table, tbody, tfoot, thead, or tr we need to foster parent the - # lastNode + # table, tbody, tfoot, thead, or tr we need to foster + # parent the lastNode if lastNode.parent: lastNode.parent.removeChild(lastNode) @@ -1543,23 +1530,23 @@ def getPhases(debug): else: commonAncestor.appendChild(lastNode) - # Step 8 + # Step 11 clone = formattingElement.cloneNode() - # Step 9 + # Step 12 furthestBlock.reparentChildren(clone) - # Step 10 + # Step 13 furthestBlock.appendChild(clone) - # Step 11 + # Step 14 self.tree.activeFormattingElements.remove(formattingElement) self.tree.activeFormattingElements.insert(bookmark, clone) - # Step 12 + # Step 15 self.tree.openElements.remove(formattingElement) self.tree.openElements.insert( - self.tree.openElements.index(furthestBlock) + 1, clone) + self.tree.openElements.index(furthestBlock) + 1, clone) def endTagAppletMarqueeObject(self, token): if self.tree.elementInScope(token["name"]): @@ -1575,7 +1562,7 @@ def getPhases(debug): def endTagBr(self, token): self.parser.parseError("unexpected-end-tag-treated-as", - {"originalName": "br", "newName": "br element"}) + {"originalName": "br", "newName": "br element"}) self.tree.reconstructActiveFormattingElements() self.tree.insertElement(impliedTagToken("br", "StartTag")) self.tree.openElements.pop() @@ -1600,31 +1587,31 @@ def getPhases(debug): self.startTagHandler = utils.MethodDispatcher([]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("script", self.endTagScript)]) + ("script", self.endTagScript)]) self.endTagHandler.default = self.endTagOther def processCharacters(self, token): self.tree.insertText(token["data"]) def processEOF(self): - self.parser.parseError("expected-named-closing-tag-but-got-eof", - self.tree.openElements[-1].name) + self.parser.parseError("expected-named-closing-tag-but-got-eof", + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase return True def startTagOther(self, token): - assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode"%token['name'] + assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name'] def endTagScript(self, token): node = self.tree.openElements.pop() assert node.name == "script" self.parser.phase = self.parser.originalPhase - #The rest of this method is all stuff that only happens if - #document.write works + # The rest of this method is all stuff that only happens if + # document.write works def endTagOther(self, token): - node = self.tree.openElements.pop() + self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase class InTablePhase(Phase): @@ -1656,7 +1643,7 @@ def getPhases(debug): def clearStackToTableContext(self): # "clear the stack back to a table context" while self.tree.openElements[-1].name not in ("table", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() # When the current node is it's an innerHTML case @@ -1667,7 +1654,7 @@ def getPhases(debug): self.parser.parseError("eof-in-table") else: assert self.parser.innerHTML - #Stop parsing + # Stop parsing def processSpaceCharacters(self, token): originalPhase = self.parser.phase @@ -1682,7 +1669,7 @@ def getPhases(debug): self.parser.phase.processCharacters(token) def insertText(self, token): - #If we get here there must be at least one non-whitespace character + # If we get here there must be at least one non-whitespace character # Do the table magic! self.tree.insertFromTable = True self.parser.phases["inBody"].processCharacters(token) @@ -1714,7 +1701,7 @@ def getPhases(debug): def startTagTable(self, token): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "table", "endName": "table"}) + {"startName": "table", "endName": "table"}) self.parser.phase.processEndTag(impliedTagToken("table")) if not self.parser.innerHTML: return token @@ -1723,8 +1710,8 @@ def getPhases(debug): return self.parser.phases["inHead"].processStartTag(token) def startTagInput(self, token): - if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + if ("type" in token["data"] and + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): self.parser.parseError("unexpected-hidden-input-in-table") self.tree.insertElement(token) # XXX associate with form @@ -1751,8 +1738,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "table": self.parser.parseError("end-tag-too-early-named", - {"gotName": "table", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "table", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "table": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1781,7 +1768,7 @@ def getPhases(debug): def flushCharacters(self): data = "".join([item["data"] for item in self.characterTokens]) if any([item not in spaceCharacters for item in data]): - token = {"type":tokenTypes["Characters"], "data":data} + token = {"type": tokenTypes["Characters"], "data": data} self.parser.phases["inTable"].insertText(token) elif data: self.tree.insertText(data) @@ -1798,12 +1785,12 @@ def getPhases(debug): return True def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.characterTokens.append(token) def processSpaceCharacters(self, token): - #pretty sure we should never reach here + # pretty sure we should never reach here self.characterTokens.append(token) # assert False @@ -1817,7 +1804,6 @@ def getPhases(debug): self.parser.phase = self.originalPhase return token - class InCaptionPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-caption def __init__(self, parser, tree): @@ -1849,7 +1835,7 @@ def getPhases(debug): def startTagTableElement(self, token): self.parser.parseError() - #XXX Have to duplicate logic here to find out if the tag is ignored + # XXX Have to duplicate logic here to find out if the tag is ignored ignoreEndTag = self.ignoreEndTagCaption() self.parser.phase.processEndTag(impliedTagToken("caption")) if not ignoreEndTag: @@ -1864,8 +1850,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "caption": self.parser.parseError("expected-one-end-tag-but-got-another", - {"gotName": "caption", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "caption", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "caption": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1889,7 +1875,6 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) - class InColumnGroupPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-column @@ -1955,7 +1940,6 @@ def getPhases(debug): if not ignoreEndTag: return token - class InTableBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table0 def __init__(self, parser, tree): @@ -1980,8 +1964,8 @@ def getPhases(debug): # helper methods def clearStackToTableBodyContext(self): while self.tree.openElements[-1].name not in ("tbody", "tfoot", - "thead", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + "thead", "html"): + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() if self.tree.openElements[-1].name == "html": @@ -2003,7 +1987,7 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inRow"] def startTagTableCell(self, token): - self.parser.parseError("unexpected-cell-in-table-body", + self.parser.parseError("unexpected-cell-in-table-body", {"name": token["name"]}) self.startTagTr(impliedTagToken("tr", "StartTag")) return token @@ -2012,7 +1996,7 @@ def getPhases(debug): # XXX AT Any ideas on how to share this with endTagTable? if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2032,12 +2016,12 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inTable"] else: self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagTable(self, token): if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2049,12 +2033,11 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) - class InRowPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-row def __init__(self, parser, tree): @@ -2072,7 +2055,7 @@ def getPhases(debug): ("table", self.endTagTable), (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), (("body", "caption", "col", "colgroup", "html", "td", "th"), - self.endTagIgnore) + self.endTagIgnore) ]) self.endTagHandler.default = self.endTagOther @@ -2080,7 +2063,7 @@ def getPhases(debug): def clearStackToTableRowContext(self): while self.tree.openElements[-1].name not in ("tr", "html"): self.parser.parseError("unexpected-implied-end-tag-in-table-row", - {"name": self.tree.openElements[-1].name}) + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() def ignoreEndTagTr(self): @@ -2091,7 +2074,7 @@ def getPhases(debug): self.parser.phases["inTable"].processEOF() def processSpaceCharacters(self, token): - return self.parser.phases["inTable"].processSpaceCharacters(token) + return self.parser.phases["inTable"].processSpaceCharacters(token) def processCharacters(self, token): return self.parser.phases["inTable"].processCharacters(token) @@ -2139,7 +2122,7 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-row", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) @@ -2178,7 +2161,7 @@ def getPhases(debug): def startTagTableOther(self, token): if (self.tree.elementInScope("td", variant="table") or - self.tree.elementInScope("th", variant="table")): + self.tree.elementInScope("th", variant="table")): self.closeCell() return token else: @@ -2194,7 +2177,7 @@ def getPhases(debug): self.tree.generateImpliedEndTags(token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError("unexpected-cell-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) while True: node = self.tree.openElements.pop() if node.name == token["name"]: @@ -2249,7 +2232,7 @@ def getPhases(debug): assert self.parser.innerHTML def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.tree.insertText(token["data"]) @@ -2283,19 +2266,19 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-select", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOption(self, token): if self.tree.openElements[-1].name == "option": self.tree.openElements.pop() else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "option"}) + {"name": "option"}) def endTagOptgroup(self, token): # implicitly closes if (self.tree.openElements[-1].name == "option" and - self.tree.openElements[-2].name == "optgroup"): + self.tree.openElements[-2].name == "optgroup"): self.tree.openElements.pop() # It also closes if self.tree.openElements[-1].name == "optgroup": @@ -2303,7 +2286,7 @@ def getPhases(debug): # But nothing else else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "optgroup"}) + {"name": "optgroup"}) def endTagSelect(self, token): if self.tree.elementInScope("select", variant="select"): @@ -2318,8 +2301,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-select", - {"name": token["name"]}) - + {"name": token["name"]}) class InSelectInTablePhase(Phase): def __init__(self, parser, tree): @@ -2360,64 +2342,64 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inSelect"].processEndTag(token) - class InForeignContentPhase(Phase): - breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", + breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", - "em", "embed", "h1", "h2", "h3", + "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", - "li", "listing", "menu", "meta", "nobr", - "ol", "p", "pre", "ruby", "s", "small", - "span", "strong", "strike", "sub", "sup", + "li", "listing", "menu", "meta", "nobr", + "ol", "p", "pre", "ruby", "s", "small", + "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var"]) + def __init__(self, parser, tree): Phase.__init__(self, parser, tree) def adjustSVGTagNames(self, token): - replacements = {u"altglyph":u"altGlyph", - u"altglyphdef":u"altGlyphDef", - u"altglyphitem":u"altGlyphItem", - u"animatecolor":u"animateColor", - u"animatemotion":u"animateMotion", - u"animatetransform":u"animateTransform", - u"clippath":u"clipPath", - u"feblend":u"feBlend", - u"fecolormatrix":u"feColorMatrix", - u"fecomponenttransfer":u"feComponentTransfer", - u"fecomposite":u"feComposite", - u"feconvolvematrix":u"feConvolveMatrix", - u"fediffuselighting":u"feDiffuseLighting", - u"fedisplacementmap":u"feDisplacementMap", - u"fedistantlight":u"feDistantLight", - u"feflood":u"feFlood", - u"fefunca":u"feFuncA", - u"fefuncb":u"feFuncB", - u"fefuncg":u"feFuncG", - u"fefuncr":u"feFuncR", - u"fegaussianblur":u"feGaussianBlur", - u"feimage":u"feImage", - u"femerge":u"feMerge", - u"femergenode":u"feMergeNode", - u"femorphology":u"feMorphology", - u"feoffset":u"feOffset", - u"fepointlight":u"fePointLight", - u"fespecularlighting":u"feSpecularLighting", - u"fespotlight":u"feSpotLight", - u"fetile":u"feTile", - u"feturbulence":u"feTurbulence", - u"foreignobject":u"foreignObject", - u"glyphref":u"glyphRef", - u"lineargradient":u"linearGradient", - u"radialgradient":u"radialGradient", - u"textpath":u"textPath"} + replacements = {"altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath"} if token["name"] in replacements: token["name"] = replacements[token["name"]] def processCharacters(self, token): - if token["data"] == u"\u0000": - token["data"] = u"\uFFFD" - elif (self.parser.framesetOK and + if token["data"] == "\u0000": + token["data"] = "\uFFFD" + elif (self.parser.framesetOK and any(char not in spaceCharacters for char in token["data"])): self.parser.framesetOK = False Phase.processCharacters(self, token) @@ -2428,9 +2410,9 @@ def getPhases(debug): (token["name"] == "font" and set(token["data"].keys()) & set(["color", "face", "size"]))): self.parser.parseError("unexpected-html-element-in-foreign-content", - token["name"]) + {"name": token["name"]}) while (self.tree.openElements[-1].namespace != - self.tree.defaultNamespace and + self.tree.defaultNamespace and not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])): self.tree.openElements.pop() @@ -2453,11 +2435,11 @@ def getPhases(debug): nodeIndex = len(self.tree.openElements) - 1 node = self.tree.openElements[-1] if node.name != token["name"]: - self.parser.parseError("unexpected-end-tag", token["name"]) + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) while True: if node.name.translate(asciiUpper2Lower) == token["name"]: - #XXX this isn't in the spec but it seems necessary + # XXX this isn't in the spec but it seems necessary if self.parser.phase == self.parser.phases["inTableText"]: self.parser.phase.flushCharacters() self.parser.phase = self.parser.phase.originalPhase @@ -2475,21 +2457,20 @@ def getPhases(debug): break return new_token - class AfterBodyPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) self.startTagHandler = utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) + ("html", self.startTagHtml) + ]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([("html", self.endTagHtml)]) self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processComment(self, token): @@ -2507,11 +2488,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token - def endTagHtml(self,name): + def endTagHtml(self, name): if self.parser.innerHTML: self.parser.parseError("unexpected-end-tag-after-body-innerhtml") else: @@ -2519,7 +2500,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2562,7 +2543,7 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagFrameset(self, token): if self.tree.openElements[-1].name == "html": @@ -2571,15 +2552,14 @@ def getPhases(debug): else: self.tree.openElements.pop() if (not self.parser.innerHTML and - self.tree.openElements[-1].name != "frameset"): + self.tree.openElements[-1].name != "frameset"): # If we're not in innerHTML mode and the the current node is not a # "frameset" element (anymore) then switch. self.parser.phase = self.parser.phases["afterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterFramesetPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#after3 @@ -2598,7 +2578,7 @@ def getPhases(debug): self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processCharacters(self, token): @@ -2609,15 +2589,14 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagHtml(self, token): self.parser.phase = self.parser.phases["afterAfterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterAfterBodyPhase(Phase): def __init__(self, parser, tree): @@ -2647,13 +2626,13 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2687,12 +2666,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) - + {"name": token["name"]}) return { "initial": InitialPhase, @@ -2719,14 +2697,16 @@ def getPhases(debug): "afterAfterBody": AfterAfterBodyPhase, "afterAfterFrameset": AfterAfterFramesetPhase, # XXX after after frameset - } + } -def impliedTagToken(name, type="EndTag", attributes = None, - selfClosing = False): + +def impliedTagToken(name, type="EndTag", attributes=None, + selfClosing=False): if attributes is None: attributes = {} - return {"type":tokenTypes[type], "name":unicode(name), "data":attributes, - "selfClosing":selfClosing} + return {"type": tokenTypes[type], "name": name, "data": attributes, + "selfClosing": selfClosing} + class ParseError(Exception): """Error in parsed document""" diff --git a/libs/html5lib/ihatexml.py b/libs/html5lib/ihatexml.py index dd785639..0fc79308 100644 --- a/libs/html5lib/ihatexml.py +++ b/libs/html5lib/ihatexml.py @@ -1,25 +1,105 @@ -import re +from __future__ import absolute_import, division, unicode_literals -baseChar = """[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | [#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | [#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | [#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | [#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | [#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | [#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | [#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | [#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | [#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | [#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | [#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | [#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | [#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | [#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | [#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | [#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | [#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | [#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | [#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | [#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | [#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | [#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | [#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | [#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | [#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | [#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | [#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | [#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | #x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | #x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | #x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | [#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | [#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | #x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | [#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | [#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | [#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | [#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | [#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | #x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | [#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | [#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | [#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | [#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" +import re +import warnings + +from .constants import DataLossWarning + +baseChar = """ +[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | +[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | +[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | +[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | +[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | +[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | +[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | +[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | +[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | +[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | +[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | +[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | +[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | +[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | +[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | +[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | +[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | +[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | +[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | +[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | +[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | +[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | +[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | +[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | +[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | +[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | +[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | +[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | +[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | +[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | +#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | +#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | +#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | +[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | +[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | +#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | +[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | +[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | +[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | +[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | +[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | +#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | +[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | +[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | +[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | +[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]""" -combiningCharacter = """[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | [#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | [#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | [#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | #x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | [#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | [#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | #x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | [#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | [#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | #x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | [#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | [#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | [#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | [#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | [#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | #x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | [#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | #x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | [#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | [#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | #x3099 | #x309A""" +combiningCharacter = """ +[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | +[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | +[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | +[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | +#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | +[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | +[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | +#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | +[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | +[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | +#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | +[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | +[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | +[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | +[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | +[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | +#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | +[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | +#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | +[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | +[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | +#x3099 | #x309A""" -digit = """[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | [#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | [#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | [#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" +digit = """ +[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | +[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | +[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | +[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" -extender = """#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | [#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" +extender = """ +#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | +#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" letter = " | ".join([baseChar, ideographic]) -#Without the -name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, - extender]) +# Without the +name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, + extender]) nameFirst = " | ".join([letter, "_"]) reChar = re.compile(r"#x([\d|A-F]{4,4})") reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]") + def charStringToList(chars): charRanges = [item.strip() for item in chars.split(" | ")] rv = [] @@ -30,16 +110,17 @@ def charStringToList(chars): if match is not None: rv.append([hexToInt(item) for item in match.groups()]) if len(rv[-1]) == 1: - rv[-1] = rv[-1]*2 + rv[-1] = rv[-1] * 2 foundMatch = True break if not foundMatch: assert len(item) == 1 - + rv.append([ord(item)] * 2) rv = normaliseCharList(rv) return rv + def normaliseCharList(charList): charList = sorted(charList) for item in charList: @@ -49,61 +130,69 @@ def normaliseCharList(charList): while i < len(charList): j = 1 rv.append(charList[i]) - while i + j < len(charList) and charList[i+j][0] <= rv[-1][1] + 1: - rv[-1][1] = charList[i+j][1] + while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1: + rv[-1][1] = charList[i + j][1] j += 1 i += j return rv -#We don't really support characters above the BMP :( +# We don't really support characters above the BMP :( max_unicode = int("FFFF", 16) - + + def missingRanges(charList): rv = [] if charList[0] != 0: rv.append([0, charList[0][0] - 1]) for i, item in enumerate(charList[:-1]): - rv.append([item[1]+1, charList[i+1][0] - 1]) + rv.append([item[1] + 1, charList[i + 1][0] - 1]) if charList[-1][1] != max_unicode: rv.append([charList[-1][1] + 1, max_unicode]) return rv + def listToRegexpStr(charList): rv = [] for item in charList: if item[0] == item[1]: - rv.append(escapeRegexp(unichr(item[0]))) + rv.append(escapeRegexp(chr(item[0]))) else: - rv.append(escapeRegexp(unichr(item[0])) + "-" + - escapeRegexp(unichr(item[1]))) - return "[%s]"%"".join(rv) + rv.append(escapeRegexp(chr(item[0])) + "-" + + escapeRegexp(chr(item[1]))) + return "[%s]" % "".join(rv) + def hexToInt(hex_str): return int(hex_str, 16) + def escapeRegexp(string): specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}", - "[", "]", "|", "(", ")", "-") + "[", "]", "|", "(", ")", "-") for char in specialCharacters: string = string.replace(char, "\\" + char) - if char in string: - print string return string -#output from the above -nonXmlNameBMPRegexp = re.compile(u'[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') +# output from the above +nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +# Simpler things +nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\-\'()+,./:=?;!*#@$_%]") -nonXmlNameFirstBMPRegexp = re.compile(u'[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') class InfosetFilter(object): replacementRegexp = re.compile(r"U[\dA-F]{5,5}") - def __init__(self, replaceChars = None, - dropXmlnsLocalName = False, - dropXmlnsAttrNs = False, - preventDoubleDashComments = False, - preventDashAtCommentEnd = False, - replaceFormFeedCharacters = True): + + def __init__(self, replaceChars=None, + dropXmlnsLocalName=False, + dropXmlnsAttrNs=False, + preventDoubleDashComments=False, + preventDashAtCommentEnd=False, + replaceFormFeedCharacters=True, + preventSingleQuotePubid=False): self.dropXmlnsLocalName = dropXmlnsLocalName self.dropXmlnsAttrNs = dropXmlnsAttrNs @@ -113,14 +202,17 @@ class InfosetFilter(object): self.replaceFormFeedCharacters = replaceFormFeedCharacters + self.preventSingleQuotePubid = preventSingleQuotePubid + self.replaceCache = {} def coerceAttribute(self, name, namespace=None): if self.dropXmlnsLocalName and name.startswith("xmlns:"): - #Need a datalosswarning here + warnings.warn("Attributes cannot begin with xmlns", DataLossWarning) return None - elif (self.dropXmlnsAttrNs and + elif (self.dropXmlnsAttrNs and namespace == "http://www.w3.org/2000/xmlns/"): + warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning) return None else: return self.toXmlName(name) @@ -131,20 +223,35 @@ class InfosetFilter(object): def coerceComment(self, data): if self.preventDoubleDashComments: while "--" in data: + warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning) data = data.replace("--", "- -") return data - + def coerceCharacters(self, data): if self.replaceFormFeedCharacters: + for i in range(data.count("\x0C")): + warnings.warn("Text cannot contain U+000C", DataLossWarning) data = data.replace("\x0C", " ") - #Other non-xml characters + # Other non-xml characters return data + def coercePubid(self, data): + dataOutput = data + for char in nonPubidCharRegexp.findall(data): + warnings.warn("Coercing non-XML pubid", DataLossWarning) + replacement = self.getReplacementCharacter(char) + dataOutput = dataOutput.replace(char, replacement) + if self.preventSingleQuotePubid and dataOutput.find("'") >= 0: + warnings.warn("Pubid cannot contain single quote", DataLossWarning) + dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'")) + return dataOutput + def toXmlName(self, name): nameFirst = name[0] nameRest = name[1:] m = nonXmlNameFirstBMPRegexp.match(nameFirst) if m: + warnings.warn("Coercing non-XML name", DataLossWarning) nameFirstOutput = self.getReplacementCharacter(nameFirst) else: nameFirstOutput = nameFirst @@ -152,10 +259,11 @@ class InfosetFilter(object): nameRestOutput = nameRest replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest)) for char in replaceChars: + warnings.warn("Coercing non-XML name", DataLossWarning) replacement = self.getReplacementCharacter(char) nameRestOutput = nameRestOutput.replace(char, replacement) return nameFirstOutput + nameRestOutput - + def getReplacementCharacter(self, char): if char in self.replaceCache: replacement = self.replaceCache[char] @@ -169,9 +277,9 @@ class InfosetFilter(object): return name def escapeChar(self, char): - replacement = "U" + hex(ord(char))[2:].upper().rjust(5, "0") + replacement = "U%05X" % ord(char) self.replaceCache[char] = replacement return replacement def unescapeChar(self, charcode): - return unichr(int(charcode[1:], 16)) + return chr(int(charcode[1:], 16)) diff --git a/libs/html5lib/inputstream.py b/libs/html5lib/inputstream.py index edec1329..004bdd4a 100644 --- a/libs/html5lib/inputstream.py +++ b/libs/html5lib/inputstream.py @@ -1,19 +1,33 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + import codecs import re -import types -import sys -from constants import EOF, spaceCharacters, asciiLetters, asciiUppercase -from constants import encodings, ReparseException -import utils +from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase +from .constants import encodings, ReparseException +from . import utils -#Non-unicode versions of constants for use in the pre-parser -spaceCharactersBytes = frozenset([str(item) for item in spaceCharacters]) -asciiLettersBytes = frozenset([str(item) for item in asciiLetters]) -asciiUppercaseBytes = frozenset([str(item) for item in asciiUppercase]) -spacesAngleBrackets = spaceCharactersBytes | frozenset([">", "<"]) +from io import StringIO -invalid_unicode_re = re.compile(u"[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") +try: + from io import BytesIO +except ImportError: + BytesIO = StringIO + +try: + from io import BufferedIOBase +except ImportError: + class BufferedIOBase(object): + pass + +# Non-unicode versions of constants for use in the pre-parser +spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters]) +asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters]) +asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase]) +spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"]) + +invalid_unicode_re = re.compile("[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, @@ -23,22 +37,23 @@ non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF]) -ascii_punctuation_re = re.compile(ur"[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") +ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") # Cache for charsUntil() charsUntilRegEx = {} - -class BufferedStream: + + +class BufferedStream(object): """Buffering for streams that do not have buffering of their own - The buffer is implemented as a list of chunks on the assumption that + The buffer is implemented as a list of chunks on the assumption that joining many strings will be slow since it is O(n**2) """ - + def __init__(self, stream): self.stream = stream self.buffer = [] - self.position = [-1,0] #chunk number, offset + self.position = [-1, 0] # chunk number, offset def tell(self): pos = 0 @@ -48,11 +63,11 @@ class BufferedStream: return pos def seek(self, pos): - assert pos < self._bufferedBytes() + assert pos <= self._bufferedBytes() offset = pos i = 0 while len(self.buffer[i]) < offset: - offset -= pos + offset -= len(self.buffer[i]) i += 1 self.position = [i, offset] @@ -64,7 +79,7 @@ class BufferedStream: return self._readStream(bytes) else: return self._readFromBuffer(bytes) - + def _bufferedBytes(self): return sum([len(item) for item in self.buffer]) @@ -83,7 +98,7 @@ class BufferedStream: while bufferIndex < len(self.buffer) and remainingBytes != 0: assert remainingBytes > 0 bufferedData = self.buffer[bufferIndex] - + if remainingBytes <= len(bufferedData) - bufferOffset: bytesToRead = remainingBytes self.position = [bufferIndex, bufferOffset + bytesToRead] @@ -91,20 +106,33 @@ class BufferedStream: bytesToRead = len(bufferedData) - bufferOffset self.position = [bufferIndex, len(bufferedData)] bufferIndex += 1 - data = rv.append(bufferedData[bufferOffset: - bufferOffset + bytesToRead]) + rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead]) remainingBytes -= bytesToRead bufferOffset = 0 if remainingBytes: rv.append(self._readStream(remainingBytes)) - - return "".join(rv) - + + return b"".join(rv) -class HTMLInputStream: +def HTMLInputStream(source, encoding=None, parseMeta=True, chardet=True): + if hasattr(source, "read"): + isUnicode = isinstance(source.read(0), text_type) + else: + isUnicode = isinstance(source, text_type) + + if isUnicode: + if encoding is not None: + raise TypeError("Cannot explicitly set an encoding with a unicode string") + + return HTMLUnicodeInputStream(source) + else: + return HTMLBinaryInputStream(source, encoding, parseMeta, chardet) + + +class HTMLUnicodeInputStream(object): """Provides a unicode stream of characters to the HTMLTokenizer. This class takes care of character encoding and removing or replacing @@ -114,7 +142,7 @@ class HTMLInputStream: _defaultChunkSize = 10240 - def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + def __init__(self, source): """Initialises the HTMLInputStream. HTMLInputStream(source, [encoding]) -> Normalized stream from source @@ -126,49 +154,29 @@ class HTMLInputStream: the encoding. If specified, that encoding will be used, regardless of any BOM or later declaration (such as in a meta element) - + parseMeta - Look for a element containing encoding information """ - #Craziness - if len(u"\U0010FFFF") == 1: + # Craziness + if len("\U0010FFFF") == 1: self.reportCharacterErrors = self.characterErrorsUCS4 - self.replaceCharactersRegexp = re.compile(u"[\uD800-\uDFFF]") + self.replaceCharactersRegexp = re.compile("[\uD800-\uDFFF]") else: self.reportCharacterErrors = self.characterErrorsUCS2 - self.replaceCharactersRegexp = re.compile(u"([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 1: lastv = ord(data[-1]) if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF: self._bufferedCharacter = data[-1] data = data[:-1] - + self.reportCharacterErrors(data) - + # Replace invalid characters # Note U+0000 is dealt with in the tokenizer - data = self.replaceCharactersRegexp.sub(u"\ufffd", data) - - data = data.replace(u"\r\n", u"\n") - data = data.replace(u"\r", u"\n") + data = self.replaceCharactersRegexp.sub("\ufffd", data) + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") self.chunk = data self.chunkSize = len(data) @@ -378,23 +275,22 @@ class HTMLInputStream: return True def characterErrorsUCS4(self, data): - for i in xrange(len(invalid_unicode_re.findall(data))): + for i in range(len(invalid_unicode_re.findall(data))): self.errors.append("invalid-codepoint") def characterErrorsUCS2(self, data): - #Someone picked the wrong compile option - #You lose + # Someone picked the wrong compile option + # You lose skip = False - import sys for match in invalid_unicode_re.finditer(data): if skip: continue codepoint = ord(match.group()) pos = match.start() - #Pretty sure there should be endianness issues here - if utils.isSurrogatePair(data[pos:pos+2]): - #We have a surrogate pair! - char_val = utils.surrogatePairToCodepoint(data[pos:pos+2]) + # Pretty sure there should be endianness issues here + if utils.isSurrogatePair(data[pos:pos + 2]): + # We have a surrogate pair! + char_val = utils.surrogatePairToCodepoint(data[pos:pos + 2]) if char_val in non_bmp_invalid_codepoints: self.errors.append("invalid-codepoint") skip = True @@ -405,7 +301,7 @@ class HTMLInputStream: skip = False self.errors.append("invalid-codepoint") - def charsUntil(self, characters, opposite = False): + def charsUntil(self, characters, opposite=False): """ Returns a string of characters from the stream up to but not including any character in 'characters' or EOF. 'characters' must be a container that supports the 'in' method and iteration over its @@ -417,12 +313,12 @@ class HTMLInputStream: chars = charsUntilRegEx[(characters, opposite)] except KeyError: if __debug__: - for c in characters: + for c in characters: assert(ord(c) < 128) - regex = u"".join([u"\\x%02x" % ord(c) for c in characters]) + regex = "".join(["\\x%02x" % ord(c) for c in characters]) if not opposite: - regex = u"^%s" % regex - chars = charsUntilRegEx[(characters, opposite)] = re.compile(u"[%s]+" % regex) + regex = "^%s" % regex + chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex) rv = [] @@ -449,7 +345,7 @@ class HTMLInputStream: # Reached EOF break - r = u"".join(rv) + r = "".join(rv) return r def unget(self, char): @@ -468,26 +364,210 @@ class HTMLInputStream: self.chunkOffset -= 1 assert self.chunk[self.chunkOffset] == char -class EncodingBytes(str): + +class HTMLBinaryInputStream(HTMLUnicodeInputStream): + """Provides a unicode stream of characters to the HTMLTokenizer. + + This class takes care of character encoding and removing or replacing + incorrect byte-sequences and also provides column and line tracking. + + """ + + def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + """Initialises the HTMLInputStream. + + HTMLInputStream(source, [encoding]) -> Normalized stream from source + for use by html5lib. + + source can be either a file-object, local filename or a string. + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + + parseMeta - Look for a element containing encoding information + + """ + # Raw Stream - for unicode objects this will encode to utf-8 and set + # self.charEncoding as appropriate + self.rawStream = self.openStream(source) + + HTMLUnicodeInputStream.__init__(self, self.rawStream) + + self.charEncoding = (codecName(encoding), "certain") + + # Encoding Information + # Number of bytes to use when looking for a meta element with + # encoding information + self.numBytesMeta = 512 + # Number of bytes to use when using detecting encoding using chardet + self.numBytesChardet = 100 + # Encoding to use if no other information can be found + self.defaultEncoding = "windows-1252" + + # Detect encoding iff no explicit "transport level" encoding is supplied + if (self.charEncoding[0] is None): + self.charEncoding = self.detectEncoding(parseMeta, chardet) + + # Call superclass + self.reset() + + def reset(self): + self.dataStream = codecs.getreader(self.charEncoding[0])(self.rawStream, + 'replace') + HTMLUnicodeInputStream.reset(self) + + def openStream(self, source): + """Produces a file object from source. + + source can be either a file object, local filename or a string. + + """ + # Already a file object + if hasattr(source, 'read'): + stream = source + else: + stream = BytesIO(source) + + try: + stream.seek(stream.tell()) + except: + stream = BufferedStream(stream) + + return stream + + def detectEncoding(self, parseMeta=True, chardet=True): + # First look for a BOM + # This will also read past the BOM if present + encoding = self.detectBOM() + confidence = "certain" + # If there is no BOM need to look for meta elements with encoding + # information + if encoding is None and parseMeta: + encoding = self.detectEncodingMeta() + confidence = "tentative" + # Guess with chardet, if avaliable + if encoding is None and chardet: + confidence = "tentative" + try: + try: + from charade.universaldetector import UniversalDetector + except ImportError: + from chardet.universaldetector import UniversalDetector + buffers = [] + detector = UniversalDetector() + while not detector.done: + buffer = self.rawStream.read(self.numBytesChardet) + assert isinstance(buffer, bytes) + if not buffer: + break + buffers.append(buffer) + detector.feed(buffer) + detector.close() + encoding = detector.result['encoding'] + self.rawStream.seek(0) + except ImportError: + pass + # If all else fails use the default encoding + if encoding is None: + confidence = "tentative" + encoding = self.defaultEncoding + + # Substitute for equivalent encodings: + encodingSub = {"iso-8859-1": "windows-1252"} + + if encoding.lower() in encodingSub: + encoding = encodingSub[encoding.lower()] + + return encoding, confidence + + def changeEncoding(self, newEncoding): + assert self.charEncoding[1] != "certain" + newEncoding = codecName(newEncoding) + if newEncoding in ("utf-16", "utf-16-be", "utf-16-le"): + newEncoding = "utf-8" + if newEncoding is None: + return + elif newEncoding == self.charEncoding[0]: + self.charEncoding = (self.charEncoding[0], "certain") + else: + self.rawStream.seek(0) + self.reset() + self.charEncoding = (newEncoding, "certain") + raise ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding)) + + def detectBOM(self): + """Attempts to detect at BOM at the start of the stream. If + an encoding can be determined from the BOM return the name of the + encoding otherwise return None""" + bomDict = { + codecs.BOM_UTF8: 'utf-8', + codecs.BOM_UTF16_LE: 'utf-16-le', codecs.BOM_UTF16_BE: 'utf-16-be', + codecs.BOM_UTF32_LE: 'utf-32-le', codecs.BOM_UTF32_BE: 'utf-32-be' + } + + # Go to beginning of file and read in 4 bytes + string = self.rawStream.read(4) + assert isinstance(string, bytes) + + # Try detecting the BOM using bytes from the string + encoding = bomDict.get(string[:3]) # UTF-8 + seek = 3 + if not encoding: + # Need to detect UTF-32 before UTF-16 + encoding = bomDict.get(string) # UTF-32 + seek = 4 + if not encoding: + encoding = bomDict.get(string[:2]) # UTF-16 + seek = 2 + + # Set the read position past the BOM if one was found, otherwise + # set it to the start of the stream + self.rawStream.seek(encoding and seek or 0) + + return encoding + + def detectEncodingMeta(self): + """Report the encoding declared by the meta element + """ + buffer = self.rawStream.read(self.numBytesMeta) + assert isinstance(buffer, bytes) + parser = EncodingParser(buffer) + self.rawStream.seek(0) + encoding = parser.getEncoding() + + if encoding in ("utf-16", "utf-16-be", "utf-16-le"): + encoding = "utf-8" + + return encoding + + +class EncodingBytes(bytes): """String-like object with an associated position and various extra methods If the position is ever greater than the string length then an exception is raised""" def __new__(self, value): - return str.__new__(self, value.lower()) + assert isinstance(value, bytes) + return bytes.__new__(self, value.lower()) def __init__(self, value): - self._position=-1 - + self._position = -1 + def __iter__(self): return self - - def next(self): + + def __next__(self): p = self._position = self._position + 1 if p >= len(self): raise StopIteration elif p < 0: raise TypeError - return self[p] + return self[p:p + 1] + + def next(self): + # Py2 compat + return self.__next__() def previous(self): p = self._position @@ -496,13 +576,13 @@ class EncodingBytes(str): elif p < 0: raise TypeError self._position = p = p - 1 - return self[p] - + return self[p:p + 1] + def setPosition(self, position): if self._position >= len(self): raise StopIteration self._position = position - + def getPosition(self): if self._position >= len(self): raise StopIteration @@ -510,19 +590,19 @@ class EncodingBytes(str): return self._position else: return None - + position = property(getPosition, setPosition) def getCurrentByte(self): - return self[self.position] - + return self[self.position:self.position + 1] + currentByte = property(getCurrentByte) def skip(self, chars=spaceCharactersBytes): """Skip past a list of characters""" p = self.position # use property for the error-checking while p < len(self): - c = self[p] + c = self[p:p + 1] if c not in chars: self._position = p return c @@ -533,7 +613,7 @@ class EncodingBytes(str): def skipUntil(self, chars): p = self.position while p < len(self): - c = self[p] + c = self[p:p + 1] if c in chars: self._position = p return c @@ -542,16 +622,16 @@ class EncodingBytes(str): return None def matchBytes(self, bytes): - """Look for a sequence of bytes at the start of a string. If the bytes - are found return True and advance the position to the byte after the + """Look for a sequence of bytes at the start of a string. If the bytes + are found return True and advance the position to the byte after the match. Otherwise return False and leave the position alone""" p = self.position - data = self[p:p+len(bytes)] + data = self[p:p + len(bytes)] rv = data.startswith(bytes) if rv: self.position += len(bytes) return rv - + def jumpTo(self, bytes): """Look for the next sequence of bytes matching a given sequence. If a match is found advance the position to the last byte of the match""" @@ -560,11 +640,12 @@ class EncodingBytes(str): # XXX: This is ugly, but I can't see a nicer way to fix this. if self._position == -1: self._position = 0 - self._position += (newPosition + len(bytes)-1) + self._position += (newPosition + len(bytes) - 1) return True else: raise StopIteration + class EncodingParser(object): """Mini parser for detecting character encoding from meta elements""" @@ -575,147 +656,158 @@ class EncodingParser(object): def getEncoding(self): methodDispatch = ( - ("") + return self.data.jumpTo(b"-->") def handleMeta(self): if self.data.currentByte not in spaceCharactersBytes: - #if we have ") + return self.data.jumpTo(b">") def getAttribute(self): - """Return a name,value pair for the next attribute in the stream, + """Return a name,value pair for the next attribute in the stream, if one is found, or None""" data = self.data # Step 1 (skip chars) - c = data.skip(spaceCharactersBytes | frozenset("/")) + c = data.skip(spaceCharactersBytes | frozenset([b"/"])) + assert c is None or len(c) == 1 # Step 2 - if c in (">", None): + if c in (b">", None): return None # Step 3 attrName = [] attrValue = [] - #Step 4 attribute name + # Step 4 attribute name while True: - if c == "=" and attrName: + if c == b"=" and attrName: break elif c in spaceCharactersBytes: - #Step 6! + # Step 6! c = data.skip() - c = data.next() break - elif c in ("/", ">"): - return "".join(attrName), "" + elif c in (b"/", b">"): + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrName.append(c.lower()) - elif c == None: + elif c is None: return None else: attrName.append(c) - #Step 5 - c = data.next() - #Step 7 - if c != "=": + # Step 5 + c = next(data) + # Step 7 + if c != b"=": data.previous() - return "".join(attrName), "" - #Step 8 - data.next() - #Step 9 + return b"".join(attrName), b"" + # Step 8 + next(data) + # Step 9 c = data.skip() - #Step 10 - if c in ("'", '"'): - #10.1 + # Step 10 + if c in (b"'", b'"'): + # 10.1 quoteChar = c while True: - #10.2 - c = data.next() - #10.3 + # 10.2 + c = next(data) + # 10.3 if c == quoteChar: - data.next() - return "".join(attrName), "".join(attrValue) - #10.4 + next(data) + return b"".join(attrName), b"".join(attrValue) + # 10.4 elif c in asciiUppercaseBytes: attrValue.append(c.lower()) - #10.5 + # 10.5 else: attrValue.append(c) - elif c == ">": - return "".join(attrName), "" + elif c == b">": + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -724,9 +816,9 @@ class EncodingParser(object): attrValue.append(c) # Step 11 while True: - c = data.next() + c = next(data) if c in spacesAngleBrackets: - return "".join(attrName), "".join(attrValue) + return b"".join(attrName), b"".join(attrValue) elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -737,21 +829,23 @@ class EncodingParser(object): class ContentAttrParser(object): def __init__(self, data): + assert isinstance(data, bytes) self.data = data + def parse(self): try: - #Check if the attr name is charset - #otherwise return - self.data.jumpTo("charset") + # Check if the attr name is charset + # otherwise return + self.data.jumpTo(b"charset") self.data.position += 1 self.data.skip() - if not self.data.currentByte == "=": - #If there is no = sign keep looking for attrs + if not self.data.currentByte == b"=": + # If there is no = sign keep looking for attrs return None self.data.position += 1 self.data.skip() - #Look for an encoding between matching quote marks - if self.data.currentByte in ('"', "'"): + # Look for an encoding between matching quote marks + if self.data.currentByte in (b'"', b"'"): quoteMark = self.data.currentByte self.data.position += 1 oldPosition = self.data.position @@ -760,13 +854,13 @@ class ContentAttrParser(object): else: return None else: - #Unquoted value + # Unquoted value oldPosition = self.data.position try: self.data.skipUntil(spaceCharactersBytes) return self.data[oldPosition:self.data.position] except StopIteration: - #Return the whole remaining value + # Return the whole remaining value return self.data[oldPosition:] except StopIteration: return None @@ -775,7 +869,12 @@ class ContentAttrParser(object): def codecName(encoding): """Return the python codec name corresponding to an encoding or None if the string doesn't correspond to a valid encoding.""" - if (encoding is not None and type(encoding) in types.StringTypes): + if isinstance(encoding, bytes): + try: + encoding = encoding.decode("ascii") + except UnicodeDecodeError: + return None + if encoding: canonicalName = ascii_punctuation_re.sub("", encoding).lower() return encodings.get(canonicalName, None) else: diff --git a/libs/html5lib/sanitizer.py b/libs/html5lib/sanitizer.py index ae4c7d83..71dc5212 100644 --- a/libs/html5lib/sanitizer.py +++ b/libs/html5lib/sanitizer.py @@ -1,142 +1,145 @@ +from __future__ import absolute_import, division, unicode_literals + import re from xml.sax.saxutils import escape, unescape -from tokenizer import HTMLTokenizer -from constants import tokenTypes +from .tokenizer import HTMLTokenizer +from .constants import tokenTypes + class HTMLSanitizerMixin(object): """ sanitization of XHTML+MathML+SVG and of inline style attributes.""" acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] - + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi', - 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', - 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', - 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', - 'munderover', 'none'] - + 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', + 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', + 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none'] + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', - 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', - 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', - 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', - 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', - 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] - + 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', + 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', + 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', - 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', - 'background', 'balance', 'bgcolor', 'bgproperties', 'border', - 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', - 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', - 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', - 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', - 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', - 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', - 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', - 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', - 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', - 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', - 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', - 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', - 'optimum', 'pattern', 'ping', 'point-size', 'prompt', 'pqg', - 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', - 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', - 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', - 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', - 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', - 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', - 'width', 'wrap', 'xml:lang'] + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', + 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', + 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', + 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', + 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', + 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', + 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', + 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', + 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', + 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', + 'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload', + 'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', + 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', + 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', + 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', + 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', + 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', + 'width', 'wrap', 'xml:lang'] mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', - 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', - 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', - 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', - 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', - 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', - 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', - 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', - 'xlink:type', 'xmlns', 'xmlns:xlink'] - - svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', - 'arabic-form', 'ascent', 'attributeName', 'attributeType', - 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', - 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', - 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', - 'fill-opacity', 'fill-rule', 'font-family', 'font-size', - 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', - 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', - 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', - 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', - 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', - 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', - 'opacity', 'orient', 'origin', 'overline-position', - 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', - 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', - 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', - 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', - 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', - 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', - 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', - 'transform', 'type', 'u1', 'u2', 'underline-position', - 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', - 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', - 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', - 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', - 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', - 'y1', 'y2', 'zoomAndPan'] + 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', + 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', + 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', + 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', + 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', + 'xlink:type', 'xmlns', 'xmlns:xlink'] - attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', - 'xlink:href', 'xml:base'] + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', + 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', + 'fill-opacity', 'fill-rule', 'font-family', 'font-size', + 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', + 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', + 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', + 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', + 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', + 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', + 'opacity', 'orient', 'origin', 'overline-position', + 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', + 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', + 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', + 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', + 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', + 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', + 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', + 'transform', 'type', 'u1', 'u2', 'underline-position', + 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', + 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', + 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', + 'y1', 'y2', 'zoomAndPan'] + + attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster', + 'xlink:href', 'xml:base'] svg_attr_val_allows_ref = ['clip-path', 'color-profile', 'cursor', 'fill', - 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', - 'mask', 'stroke'] + 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', + 'mask', 'stroke'] svg_allow_local_href = ['altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', - 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', - 'set', 'use'] - + 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', + 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', + 'set', 'use'] + acceptable_css_properties = ['azimuth', 'background-color', - 'border-bottom-color', 'border-collapse', 'border-color', - 'border-left-color', 'border-right-color', 'border-top-color', 'clear', - 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', - 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', - 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', - 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', - 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', - 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', - 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', - 'white-space', 'width'] - + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', - 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', - 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', - 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', - 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', - 'transparent', 'underline', 'white', 'yellow'] - - acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', - 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', - 'stroke-opacity'] - - acceptable_protocols = [ 'ed2k', 'ftp', 'http', 'https', 'irc', - 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', - 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', - 'ssh', 'sftp', 'rtsp', 'afs' ] - + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + acceptable_svg_properties = ['fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] + + acceptable_protocols = ['ed2k', 'ftp', 'http', 'https', 'irc', + 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', + 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', + 'ssh', 'sftp', 'rtsp', 'afs'] + # subclasses may define their own versions of these constants allowed_elements = acceptable_elements + mathml_elements + svg_elements allowed_attributes = acceptable_attributes + mathml_attributes + svg_attributes @@ -160,94 +163,104 @@ class HTMLSanitizerMixin(object): # accommodate filters which use token_type differently token_type = token["type"] - if token_type in tokenTypes.keys(): - token_type = tokenTypes[token_type] + if token_type in list(tokenTypes.keys()): + token_type = tokenTypes[token_type] - if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], - tokenTypes["EmptyTag"]): + if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"]): if token["name"] in self.allowed_elements: - if token.has_key("data"): - attrs = dict([(name,val) for name,val in - token["data"][::-1] - if name in self.allowed_attributes]) - for attr in self.attr_val_is_uri: - if not attrs.has_key(attr): - continue - val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', - unescape(attrs[attr])).lower() - #remove replacement characters from unescaped characters - val_unescaped = val_unescaped.replace(u"\ufffd", "") - if (re.match("^[a-z0-9][-+.a-z0-9]*:",val_unescaped) and - (val_unescaped.split(':')[0] not in - self.allowed_protocols)): - del attrs[attr] - for attr in self.svg_attr_val_allows_ref: - if attr in attrs: - attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', - ' ', - unescape(attrs[attr])) - if (token["name"] in self.svg_allow_local_href and - 'xlink:href' in attrs and re.search('^\s*[^#\s].*', - attrs['xlink:href'])): - del attrs['xlink:href'] - if attrs.has_key('style'): - attrs['style'] = self.sanitize_css(attrs['style']) - token["data"] = [[name,val] for name,val in attrs.items()] - return token + return self.allowed_token(token, token_type) else: - if token_type == tokenTypes["EndTag"]: - token["data"] = "%s>" % token["name"] - elif token["data"]: - attrs = ''.join([' %s="%s"' % (k,escape(v)) for k,v in token["data"]]) - token["data"] = "<%s%s>" % (token["name"],attrs) - else: - token["data"] = "<%s>" % token["name"] - if token.get("selfClosing"): - token["data"]=token["data"][:-1] + "/>" - - if token["type"] in tokenTypes.keys(): - token["type"] = "Characters" - else: - token["type"] = tokenTypes["Characters"] - - del token["name"] - return token + return self.disallowed_token(token, token_type) elif token_type == tokenTypes["Comment"]: pass else: return token + def allowed_token(self, token, token_type): + if "data" in token: + attrs = dict([(name, val) for name, val in + token["data"][::-1] + if name in self.allowed_attributes]) + for attr in self.attr_val_is_uri: + if attr not in attrs: + continue + val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', + unescape(attrs[attr])).lower() + # remove replacement characters from unescaped characters + val_unescaped = val_unescaped.replace("\ufffd", "") + if (re.match("^[a-z0-9][-+.a-z0-9]*:", val_unescaped) and + (val_unescaped.split(':')[0] not in + self.allowed_protocols)): + del attrs[attr] + for attr in self.svg_attr_val_allows_ref: + if attr in attrs: + attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', + ' ', + unescape(attrs[attr])) + if (token["name"] in self.svg_allow_local_href and + 'xlink:href' in attrs and re.search('^\s*[^#\s].*', + attrs['xlink:href'])): + del attrs['xlink:href'] + if 'style' in attrs: + attrs['style'] = self.sanitize_css(attrs['style']) + token["data"] = [[name, val] for name, val in list(attrs.items())] + return token + + def disallowed_token(self, token, token_type): + if token_type == tokenTypes["EndTag"]: + token["data"] = "%s>" % token["name"] + elif token["data"]: + attrs = ''.join([' %s="%s"' % (k, escape(v)) for k, v in token["data"]]) + token["data"] = "<%s%s>" % (token["name"], attrs) + else: + token["data"] = "<%s>" % token["name"] + if token.get("selfClosing"): + token["data"] = token["data"][:-1] + "/>" + + if token["type"] in list(tokenTypes.keys()): + token["type"] = "Characters" + else: + token["type"] = tokenTypes["Characters"] + + del token["name"] + return token + def sanitize_css(self, style): # disallow urls - style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) + style = re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) # gauntlet - if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' - if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): return '' + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): + return '' clean = [] - for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): - if not value: continue - if prop.lower() in self.allowed_css_properties: - clean.append(prop + ': ' + value + ';') - elif prop.split('-')[0].lower() in ['background','border','margin', - 'padding']: - for keyword in value.split(): - if not keyword in self.acceptable_css_keywords and \ - not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$",keyword): - break - else: - clean.append(prop + ': ' + value + ';') - elif prop.lower() in self.allowed_svg_properties: - clean.append(prop + ': ' + value + ';') + for prop, value in re.findall("([-\w]+)\s*:\s*([^:;]*)", style): + if not value: + continue + if prop.lower() in self.allowed_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background', 'border', 'margin', + 'padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif prop.lower() in self.allowed_svg_properties: + clean.append(prop + ': ' + value + ';') return ' '.join(clean) + class HTMLSanitizer(HTMLTokenizer, HTMLSanitizerMixin): def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True, lowercaseElementName=False, lowercaseAttrName=False, parser=None): - #Change case matching defaults as we only output lowercase html anyway - #This solution doesn't seem ideal... + # Change case matching defaults as we only output lowercase html anyway + # This solution doesn't seem ideal... HTMLTokenizer.__init__(self, stream, encoding, parseMeta, useChardet, lowercaseElementName, lowercaseAttrName, parser=parser) diff --git a/libs/html5lib/serializer/__init__.py b/libs/html5lib/serializer/__init__.py index 1b746655..8380839a 100644 --- a/libs/html5lib/serializer/__init__.py +++ b/libs/html5lib/serializer/__init__.py @@ -1,17 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals -from html5lib import treewalkers +from .. import treewalkers -from htmlserializer import HTMLSerializer -from xhtmlserializer import XHTMLSerializer +from .htmlserializer import HTMLSerializer -def serialize(input, tree="simpletree", format="html", encoding=None, + +def serialize(input, tree="etree", format="html", encoding=None, **serializer_opts): # XXX: Should we cache this? - walker = treewalkers.getTreeWalker(tree) + walker = treewalkers.getTreeWalker(tree) if format == "html": s = HTMLSerializer(**serializer_opts) - elif format == "xhtml": - s = XHTMLSerializer(**serializer_opts) else: - raise ValueError, "type must be either html or xhtml" + raise ValueError("type must be html") return s.render(walker(input), encoding) diff --git a/libs/html5lib/serializer/htmlserializer.py b/libs/html5lib/serializer/htmlserializer.py index 8dd0a815..412a5a22 100644 --- a/libs/html5lib/serializer/htmlserializer.py +++ b/libs/html5lib/serializer/htmlserializer.py @@ -1,18 +1,20 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type import gettext _ = gettext.gettext -from html5lib.constants import voidElements, booleanAttributes, spaceCharacters -from html5lib.constants import rcdataElements, entities, xmlEntities -from html5lib import utils +try: + from functools import reduce +except ImportError: + pass + +from ..constants import voidElements, booleanAttributes, spaceCharacters +from ..constants import rcdataElements, entities, xmlEntities +from .. import utils from xml.sax.saxutils import escape -spaceCharacters = u"".join(spaceCharacters) +spaceCharacters = "".join(spaceCharacters) try: from codecs import register_error, xmlcharrefreplace_errors @@ -21,24 +23,18 @@ except ImportError: else: unicode_encode_errors = "htmlentityreplace" - from html5lib.constants import entities - encode_entity_map = {} - is_ucs4 = len(u"\U0010FFFF") == 1 - for k, v in entities.items(): - #skip multi-character entities + is_ucs4 = len("\U0010FFFF") == 1 + for k, v in list(entities.items()): + # skip multi-character entities if ((is_ucs4 and len(v) > 1) or - (not is_ucs4 and len(v) > 2)): + (not is_ucs4 and len(v) > 2)): continue if v != "&": if len(v) == 2: v = utils.surrogatePairToCodepoint(v) else: - try: - v = ord(v) - except: - print v - raise + v = ord(v) if not v in encode_entity_map or k.islower(): # prefer < over < and similarly for &, >, etc. encode_entity_map[v] = k @@ -53,8 +49,8 @@ else: skip = False continue index = i + exc.start - if utils.isSurrogatePair(exc.object[index:min([exc.end, index+2])]): - codepoint = utils.surrogatePairToCodepoint(exc.object[index:index+2]) + if utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]): + codepoint = utils.surrogatePairToCodepoint(exc.object[index:index + 2]) skip = True else: codepoint = ord(c) @@ -67,8 +63,8 @@ else: if not e.endswith(";"): res.append(";") else: - res.append("%s;"%(hex(cp)[2:])) - return (u"".join(res), exc.end) + res.append("%s;" % (hex(cp)[2:])) + return ("".join(res), exc.end) else: return xmlcharrefreplace_errors(exc) @@ -81,7 +77,7 @@ class HTMLSerializer(object): # attribute quoting options quote_attr_values = False - quote_char = u'"' + quote_char = '"' use_best_quote_char = True # tag syntax options @@ -96,15 +92,17 @@ class HTMLSerializer(object): resolve_entities = True # miscellaneous options + alphabetical_attributes = False inject_meta_charset = True strip_whitespace = False sanitize = False options = ("quote_attr_values", "quote_char", "use_best_quote_char", - "minimize_boolean_attributes", "use_trailing_solidus", - "space_before_trailing_solidus", "omit_optional_tags", - "strip_whitespace", "inject_meta_charset", "escape_lt_in_attrs", - "escape_rcdata", "resolve_entities", "sanitize") + "omit_optional_tags", "minimize_boolean_attributes", + "use_trailing_solidus", "space_before_trailing_solidus", + "escape_lt_in_attrs", "escape_rcdata", "resolve_entities", + "alphabetical_attributes", "inject_meta_charset", + "strip_whitespace", "sanitize") def __init__(self, **kwargs): """Initialize HTMLSerializer. @@ -147,10 +145,12 @@ class HTMLSerializer(object): See `html5lib user documentation`_ omit_optional_tags=True|False Omit start/end tags that are optional. + alphabetical_attributes=False|True + Reorder attributes to be in alphabetical order. .. _html5lib user documentation: http://code.google.com/p/html5lib/wiki/UserDocumentation """ - if kwargs.has_key('quote_char'): + if 'quote_char' in kwargs: self.use_best_quote_char = False for attr in self.options: setattr(self, attr, kwargs.get(attr, getattr(self, attr))) @@ -158,14 +158,14 @@ class HTMLSerializer(object): self.strict = False def encode(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, unicode_encode_errors) else: return string def encodeStrict(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, "strict") else: @@ -175,39 +175,46 @@ class HTMLSerializer(object): self.encoding = encoding in_cdata = False self.errors = [] + if encoding and self.inject_meta_charset: - from html5lib.filters.inject_meta_charset import Filter + from ..filters.inject_meta_charset import Filter treewalker = Filter(treewalker, encoding) - # XXX: WhitespaceFilter should be used before OptionalTagFilter + # WhitespaceFilter should be used before OptionalTagFilter # for maximum efficiently of this latter filter if self.strip_whitespace: - from html5lib.filters.whitespace import Filter + from ..filters.whitespace import Filter treewalker = Filter(treewalker) if self.sanitize: - from html5lib.filters.sanitizer import Filter + from ..filters.sanitizer import Filter treewalker = Filter(treewalker) if self.omit_optional_tags: - from html5lib.filters.optionaltags import Filter + from ..filters.optionaltags import Filter treewalker = Filter(treewalker) + # Alphabetical attributes must be last, as other filters + # could add attributes and alter the order + if self.alphabetical_attributes: + from ..filters.alphabeticalattributes import Filter + treewalker = Filter(treewalker) + for token in treewalker: type = token["type"] if type == "Doctype": - doctype = u"= 0: - if token["systemId"].find(u"'") >= 0: + doctype += " SYSTEM" + if token["systemId"]: + if token["systemId"].find('"') >= 0: + if token["systemId"].find("'") >= 0: self.serializeError(_("System identifer contains both single and double quote characters")) - quote_char = u"'" + quote_char = "'" else: - quote_char = u'"' - doctype += u" %s%s%s" % (quote_char, token["systemId"], quote_char) - - doctype += u">" + quote_char = '"' + doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char) + + doctype += ">" yield self.encodeStrict(doctype) elif type in ("Characters", "SpaceCharacters"): @@ -220,41 +227,41 @@ class HTMLSerializer(object): elif type in ("StartTag", "EmptyTag"): name = token["name"] - yield self.encodeStrict(u"<%s" % name) + yield self.encodeStrict("<%s" % name) if name in rcdataElements and not self.escape_rcdata: in_cdata = True elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - attributes = [] - for (attr_namespace,attr_name),attr_value in sorted(token["data"].items()): - #TODO: Add namespace support here + for (attr_namespace, attr_name), attr_value in token["data"].items(): + # TODO: Add namespace support here k = attr_name v = attr_value - yield self.encodeStrict(u' ') + yield self.encodeStrict(' ') yield self.encodeStrict(k) if not self.minimize_boolean_attributes or \ - (k not in booleanAttributes.get(name, tuple()) \ - and k not in booleanAttributes.get("", tuple())): - yield self.encodeStrict(u"=") + (k not in booleanAttributes.get(name, tuple()) + and k not in booleanAttributes.get("", tuple())): + yield self.encodeStrict("=") if self.quote_attr_values or not v: quote_attr = True else: - quote_attr = reduce(lambda x,y: x or (y in v), - spaceCharacters + u">\"'=", False) - v = v.replace(u"&", u"&") - if self.escape_lt_in_attrs: v = v.replace(u"<", u"<") + quote_attr = reduce(lambda x, y: x or (y in v), + spaceCharacters + ">\"'=", False) + v = v.replace("&", "&") + if self.escape_lt_in_attrs: + v = v.replace("<", "<") if quote_attr: quote_char = self.quote_char if self.use_best_quote_char: - if u"'" in v and u'"' not in v: - quote_char = u'"' - elif u'"' in v and u"'" not in v: - quote_char = u"'" - if quote_char == u"'": - v = v.replace(u"'", u"'") + if "'" in v and '"' not in v: + quote_char = '"' + elif '"' in v and "'" not in v: + quote_char = "'" + if quote_char == "'": + v = v.replace("'", "'") else: - v = v.replace(u'"', u""") + v = v.replace('"', """) yield self.encodeStrict(quote_char) yield self.encode(v) yield self.encodeStrict(quote_char) @@ -262,10 +269,10 @@ class HTMLSerializer(object): yield self.encode(v) if name in voidElements and self.use_trailing_solidus: if self.space_before_trailing_solidus: - yield self.encodeStrict(u" /") + yield self.encodeStrict(" /") else: - yield self.encodeStrict(u"/") - yield self.encode(u">") + yield self.encodeStrict("/") + yield self.encode(">") elif type == "EndTag": name = token["name"] @@ -273,13 +280,13 @@ class HTMLSerializer(object): in_cdata = False elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - yield self.encodeStrict(u"%s>" % name) + yield self.encodeStrict("%s>" % name) elif type == "Comment": data = token["data"] if data.find("--") >= 0: self.serializeError(_("Comment contains --")) - yield self.encodeStrict(u"" % token["data"]) + yield self.encodeStrict("" % token["data"]) elif type == "Entity": name = token["name"] @@ -289,7 +296,7 @@ class HTMLSerializer(object): if self.resolve_entities and key not in xmlEntities: data = entities[key] else: - data = u"&%s;" % name + data = "&%s;" % name yield self.encodeStrict(data) else: @@ -297,9 +304,9 @@ class HTMLSerializer(object): def render(self, treewalker, encoding=None): if encoding: - return "".join(list(self.serialize(treewalker, encoding))) + return b"".join(list(self.serialize(treewalker, encoding))) else: - return u"".join(list(self.serialize(treewalker))) + return "".join(list(self.serialize(treewalker))) def serializeError(self, data="XXX ERROR MESSAGE NEEDED"): # XXX The idea is to make data mandatory. @@ -307,6 +314,7 @@ class HTMLSerializer(object): if self.strict: raise SerializeError + def SerializeError(Exception): """Error in serialized tree""" pass diff --git a/libs/html5lib/serializer/xhtmlserializer.py b/libs/html5lib/serializer/xhtmlserializer.py deleted file mode 100644 index 7fdce47b..00000000 --- a/libs/html5lib/serializer/xhtmlserializer.py +++ /dev/null @@ -1,9 +0,0 @@ -from htmlserializer import HTMLSerializer - -class XHTMLSerializer(HTMLSerializer): - quote_attr_values = True - minimize_boolean_attributes = False - use_trailing_solidus = True - escape_lt_in_attrs = True - omit_optional_tags = False - escape_rcdata = True diff --git a/libs/html5lib/tokenizer.py b/libs/html5lib/tokenizer.py index 7e9eca88..79774578 100644 --- a/libs/html5lib/tokenizer.py +++ b/libs/html5lib/tokenizer.py @@ -1,27 +1,25 @@ +from __future__ import absolute_import, division, unicode_literals + try: - frozenset + chr = unichr # flake8: noqa except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset -try: - from collections import deque -except ImportError: - from utils import deque - -from constants import spaceCharacters -from constants import entitiesWindows1252, entities -from constants import asciiLowercase, asciiLetters, asciiUpper2Lower -from constants import digits, hexDigits, EOF -from constants import tokenTypes, tagTokenTypes -from constants import replacementCharacters + pass -from inputstream import HTMLInputStream +from collections import deque + +from .constants import spaceCharacters +from .constants import entities +from .constants import asciiLetters, asciiUpper2Lower +from .constants import digits, hexDigits, EOF +from .constants import tokenTypes, tagTokenTypes +from .constants import replacementCharacters + +from .inputstream import HTMLInputStream + +from .trie import Trie + +entitiesTrie = Trie(entities) -# Group entities by their first character, for faster lookups -entitiesByFirstChar = {} -for e in entities: - entitiesByFirstChar.setdefault(e[0], []).append(e) class HTMLTokenizer(object): """ This class takes care of tokenizing HTML. @@ -42,10 +40,10 @@ class HTMLTokenizer(object): self.stream = HTMLInputStream(stream, encoding, parseMeta, useChardet) self.parser = parser - #Perform case conversions? + # Perform case conversions? self.lowercaseElementName = lowercaseElementName self.lowercaseAttrName = lowercaseAttrName - + # Setup the initial tokenizer state self.escapeFlag = False self.lastFourChars = [] @@ -100,78 +98,79 @@ class HTMLTokenizer(object): if charAsInt in replacementCharacters: char = replacementCharacters[charAsInt] self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) - elif ((0xD800 <= charAsInt <= 0xDFFF) or + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) + elif ((0xD800 <= charAsInt <= 0xDFFF) or (charAsInt > 0x10FFFF)): - char = u"\uFFFD" + char = "\uFFFD" self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) else: - #Should speed up this check somehow (e.g. move the set to a constant) - if ((0x0001 <= charAsInt <= 0x0008) or - (0x000E <= charAsInt <= 0x001F) or - (0x007F <= charAsInt <= 0x009F) or - (0xFDD0 <= charAsInt <= 0xFDEF) or - charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, + # Should speed up this check somehow (e.g. move the set to a constant) + if ((0x0001 <= charAsInt <= 0x0008) or + (0x000E <= charAsInt <= 0x001F) or + (0x007F <= charAsInt <= 0x009F) or + (0xFDD0 <= charAsInt <= 0xFDEF) or + charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, - 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, + 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, - 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, - 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, - 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, + 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, + 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, + 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF])): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", + "illegal-codepoint-for-numeric-entity", "datavars": {"charAsInt": charAsInt}}) try: # Try/except needed as UCS-2 Python builds' unichar only works # within the BMP. - char = unichr(charAsInt) + char = chr(charAsInt) except ValueError: - char = eval("u'\\U%08x'" % charAsInt) + v = charAsInt - 0x10000 + char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF)) # Discard the ; if present. Otherwise, put it back on the queue and # invoke parseError on parser. - if c != u";": + if c != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "numeric-entity-without-semicolon"}) + "numeric-entity-without-semicolon"}) self.stream.unget(c) return char def consumeEntity(self, allowedChar=None, fromAttribute=False): # Initialise to the default output for when no entity is matched - output = u"&" + output = "&" charStack = [self.stream.char()] - if (charStack[0] in spaceCharacters or charStack[0] in (EOF, u"<", u"&") - or (allowedChar is not None and allowedChar == charStack[0])): + if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") + or (allowedChar is not None and allowedChar == charStack[0])): self.stream.unget(charStack[0]) - elif charStack[0] == u"#": + elif charStack[0] == "#": # Read the next character to see if it's hex or decimal hex = False charStack.append(self.stream.char()) - if charStack[-1] in (u"x", u"X"): + if charStack[-1] in ("x", "X"): hex = True charStack.append(self.stream.char()) # charStack[-1] should be the first digit if (hex and charStack[-1] in hexDigits) \ - or (not hex and charStack[-1] in digits): + or (not hex and charStack[-1] in digits): # At least one digit found, so consume the whole number self.stream.unget(charStack[-1]) output = self.consumeNumberEntity(hex) else: # No digits found self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data": "expected-numeric-entity"}) + "data": "expected-numeric-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: # At this point in the process might have named entity. Entities @@ -179,46 +178,40 @@ class HTMLTokenizer(object): # # Consume characters and compare to these to a substring of the # entity names in the list until the substring no longer matches. - filteredEntityList = entitiesByFirstChar.get(charStack[0], []) - - def entitiesStartingWith(name): - return [e for e in filteredEntityList if e.startswith(name)] - - while (charStack[-1] is not EOF and - entitiesStartingWith("".join(charStack))): + while (charStack[-1] is not EOF): + if not entitiesTrie.has_keys_with_prefix("".join(charStack)): + break charStack.append(self.stream.char()) # At this point we have a string that starts with some characters # that may match an entity - entityName = None - # Try to find the longest entity the string will match to take care # of ¬i for instance. - for entityLength in xrange(len(charStack)-1, 1, -1): - possibleEntityName = "".join(charStack[:entityLength]) - if possibleEntityName in entities: - entityName = possibleEntityName - break + try: + entityName = entitiesTrie.longest_prefix("".join(charStack[:-1])) + entityLength = len(entityName) + except KeyError: + entityName = None if entityName is not None: if entityName[-1] != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "named-entity-without-semicolon"}) + "named-entity-without-semicolon"}) if (entityName[-1] != ";" and fromAttribute and (charStack[entityLength] in asciiLetters or charStack[entityLength] in digits or - charStack[entityLength] == "=")): + charStack[entityLength] == "=")): self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: output = entities[entityName] self.stream.unget(charStack.pop()) - output += u"".join(charStack[entityLength:]) + output += "".join(charStack[entityLength:]) else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-named-entity"}) + "expected-named-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) if fromAttribute: self.currentToken["data"][-1][1] += output @@ -246,28 +239,26 @@ class HTMLTokenizer(object): token["name"] = token["name"].translate(asciiUpper2Lower) if token["type"] == tokenTypes["EndTag"]: if token["data"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"attributes-in-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "attributes-in-end-tag"}) if token["selfClosing"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"self-closing-flag-on-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "self-closing-flag-on-end-tag"}) self.tokenQueue.append(token) self.state = self.dataState - # Below are the various tokenizer states worked out. - def dataState(self): data = self.stream.char() if data == "&": self.state = self.entityDataState elif data == "<": self.state = self.tagOpenState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data":"invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\u0000"}) + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "invalid-codepoint"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\u0000"}) elif data is EOF: # Tokenization ends. return False @@ -276,21 +267,21 @@ class HTMLTokenizer(object): # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def entityDataState(self): self.consumeEntity() self.state = self.dataState return True - + def rcdataState(self): data = self.stream.char() if data == "&": @@ -300,113 +291,113 @@ class HTMLTokenizer(object): elif data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data in spaceCharacters: # Directly after emitting a token you switch back to the "data # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def characterReferenceInRcdata(self): self.consumeEntity() self.state = self.rcdataState return True - + def rawtextState(self): data = self.stream.char() if data == "<": self.state = self.rawtextLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataState(self): data = self.stream.char() if data == "<": self.state = self.scriptDataLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def plaintextState(self): data = self.stream.char() if data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + self.stream.charsUntil(u"\u0000")}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + self.stream.charsUntil("\u0000")}) return True def tagOpenState(self): data = self.stream.char() - if data == u"!": + if data == "!": self.state = self.markupDeclarationOpenState - elif data == u"/": + elif data == "/": self.state = self.closeTagOpenState elif data in asciiLetters: - self.currentToken = {"type": tokenTypes["StartTag"], + self.currentToken = {"type": tokenTypes["StartTag"], "name": data, "data": [], "selfClosing": False, "selfClosingAcknowledged": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-right-bracket"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<>"}) + "expected-tag-name-but-got-right-bracket"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"}) self.state = self.dataState - elif data == u"?": + elif data == "?": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-question-mark"}) + "expected-tag-name-but-got-question-mark"}) self.stream.unget(data) self.state = self.bogusCommentState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + "expected-tag-name"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.dataState return True @@ -415,22 +406,22 @@ class HTMLTokenizer(object): data = self.stream.char() if data in asciiLetters: self.currentToken = {"type": tokenTypes["EndTag"], "name": data, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-right-bracket"}) + "expected-closing-tag-but-got-right-bracket"}) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-eof"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + "expected-closing-tag-but-got-eof"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.state = self.dataState else: # XXX data can be _'_... self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-char", - "datavars": {"data": data}}) + "expected-closing-tag-but-got-char", + "datavars": {"data": data}}) self.stream.unget(data) self.state = self.bogusCommentState return True @@ -439,229 +430,229 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-tag-name"}) + "eof-in-tag-name"}) self.state = self.dataState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" else: self.currentToken["name"] += data # (Don't use charsUntil here, because tag names are # very short and it's faster to not do anything fancy) return True - + def rcdataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rcdataEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rcdataEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rcdataState return True - + def rawtextLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rawtextEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rawtextEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rawtextState return True - + def scriptDataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEndTagOpenState elif data == "!": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapeStartDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.state = self.dataState else: - chars = self.stream.charsUntil((u"<", u"-", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "-", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -669,21 +660,21 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": self.state = self.scriptDataEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -691,61 +682,61 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEscapedEndTagOpenState elif data in asciiLetters: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<" + data}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data}) self.temporaryBuffer = data self.state = self.scriptDataDoubleEscapeStartState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer = data self.state = self.scriptDataEscapedEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapeStartState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -761,87 +752,87 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) return True - + def scriptDataDoubleEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - - def scriptDataDoubleEscapedDashState(self): + + def scriptDataDoubleEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapedLessThanSignState(self): data = self.stream.char() if data == "/": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"/"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"}) self.temporaryBuffer = "" self.state = self.scriptDataDoubleEscapeEndState else: self.stream.unget(data) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapeEndState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -865,23 +856,23 @@ class HTMLTokenizer(object): elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data in (u"'", u'"', u"=", u"<"): + elif data in ("'", '"', "=", "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-name-but-got-eof"}) + "expected-attribute-name-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -892,34 +883,34 @@ class HTMLTokenizer(object): data = self.stream.char() leavingThisState = True emitToken = False - if data == u"=": + if data == "=": self.state = self.beforeAttributeValueState elif data in asciiLetters: self.currentToken["data"][-1][0] += data +\ - self.stream.charsUntil(asciiLetters, True) + self.stream.charsUntil(asciiLetters, True) leavingThisState = False - elif data == u">": + elif data == ">": # XXX If we emit here the attributes are converted to a dict # without being checked and when the code below runs we error # because data is a dict not a list emitToken = True elif data in spaceCharacters: self.state = self.afterAttributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][0] += u"\uFFFD" + self.currentToken["data"][-1][0] += "\uFFFD" leavingThisState = False - elif data in (u"'", u'"', u"<"): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data in ("'", '"', "<"): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"][-1][0] += data leavingThisState = False elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-attribute-name"}) self.state = self.dataState else: @@ -936,7 +927,7 @@ class HTMLTokenizer(object): for name, value in self.currentToken["data"][:-1]: if self.currentToken["data"][-1][0] == name: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "duplicate-attribute"}) + "duplicate-attribute"}) break # XXX Fix for above XXX if emitToken: @@ -947,28 +938,28 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"=": + elif data == "=": self.state = self.beforeAttributeValueState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState - elif data in (u"'", u'"', u"<"): + elif data in ("'", '"', "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-after-attribute-name"}) + "invalid-character-after-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-end-of-tag-but-got-eof"}) + "expected-end-of-tag-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -979,30 +970,30 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"\"": + elif data == "\"": self.state = self.attributeValueDoubleQuotedState - elif data == u"&": + elif data == "&": self.state = self.attributeValueUnQuotedState - self.stream.unget(data); - elif data == u"'": + self.stream.unget(data) + elif data == "'": self.state = self.attributeValueSingleQuotedState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-right-bracket"}) + "expected-attribute-value-but-got-right-bracket"}) self.emitCurrentToken() - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" self.state = self.attributeValueUnQuotedState - elif data in (u"=", u"<", u"`"): + elif data in ("=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "equals-in-unquoted-attribute-value"}) + "equals-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data self.state = self.attributeValueUnQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-eof"}) + "expected-attribute-value-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data @@ -1013,81 +1004,81 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u'"') - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute('"') + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-double-quote"}) + "eof-in-attribute-value-double-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("\"", u"&")) + self.stream.charsUntil(("\"", "&", "\u0000")) return True def attributeValueSingleQuotedState(self): data = self.stream.char() if data == "'": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u"'") - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute("'") + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-single-quote"}) + "eof-in-attribute-value-single-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("'", u"&")) + self.stream.charsUntil(("'", "&", "\u0000")) return True def attributeValueUnQuotedState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u"&": + elif data == "&": self.processEntityInAttribute(">") - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data in (u'"', u"'", u"=", u"<", u"`"): + elif data in ('"', "'", "=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-in-unquoted-attribute-value"}) + "unexpected-character-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-no-quotes"}) + "eof-in-attribute-value-no-quotes"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data + self.stream.charsUntil( - frozenset((u"&", u">", u'"', u"'", u"=", u"<", u"`")) | spaceCharacters) + frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters) return True def afterAttributeValueState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-attribute-value"}) + "unexpected-EOF-after-attribute-value"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-attribute-value"}) + "unexpected-character-after-attribute-value"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1098,14 +1089,14 @@ class HTMLTokenizer(object): self.currentToken["selfClosing"] = True self.emitCurrentToken() elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-solidus-in-tag"}) + "unexpected-EOF-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-soldius-in-tag"}) + "unexpected-character-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1114,10 +1105,10 @@ class HTMLTokenizer(object): # Make a new comment token and give it as value all the characters # until the first > or EOF (charsUntil checks for EOF automatically) # and emit it. - data = self.stream.charsUntil(u">") - data = data.replace(u"\u0000", u"\uFFFD") + data = self.stream.charsUntil(">") + data = data.replace("\u0000", "\uFFFD") self.tokenQueue.append( - {"type": tokenTypes["Comment"], "data": data}) + {"type": tokenTypes["Comment"], "data": data}) # Eat the character directly after the bogus comment which is either a # ">" or an EOF. @@ -1127,28 +1118,28 @@ class HTMLTokenizer(object): def markupDeclarationOpenState(self): charStack = [self.stream.char()] - if charStack[-1] == u"-": + if charStack[-1] == "-": charStack.append(self.stream.char()) - if charStack[-1] == u"-": - self.currentToken = {"type": tokenTypes["Comment"], "data": u""} + if charStack[-1] == "-": + self.currentToken = {"type": tokenTypes["Comment"], "data": ""} self.state = self.commentStartState return True - elif charStack[-1] in (u'd', u'D'): + elif charStack[-1] in ('d', 'D'): matched = True - for expected in ((u'o', u'O'), (u'c', u'C'), (u't', u'T'), - (u'y', u'Y'), (u'p', u'P'), (u'e', u'E')): + for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'), + ('y', 'Y'), ('p', 'P'), ('e', 'E')): charStack.append(self.stream.char()) if charStack[-1] not in expected: matched = False break if matched: self.currentToken = {"type": tokenTypes["Doctype"], - "name": u"", - "publicId": None, "systemId": None, + "name": "", + "publicId": None, "systemId": None, "correct": True} self.state = self.doctypeState return True - elif (charStack[-1] == "[" and + elif (charStack[-1] == "[" and self.parser is not None and self.parser.tree.openElements and self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace): @@ -1163,7 +1154,7 @@ class HTMLTokenizer(object): return True self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-dashes-or-doctype"}) + "expected-dashes-or-doctype"}) while charStack: self.stream.unget(charStack.pop()) @@ -1174,41 +1165,41 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "-": self.state = self.commentStartDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data self.state = self.commentState return True - + def commentStartDashState(self): data = self.stream.char() if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: @@ -1216,95 +1207,94 @@ class HTMLTokenizer(object): self.state = self.commentState return True - def commentState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data + \ - self.stream.charsUntil((u"-", u"\u0000")) + self.stream.charsUntil(("-", "\u0000")) return True def commentEndDashState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-dash"}) + "eof-in-comment-end-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"-" + data + self.currentToken["data"] += "-" + data self.state = self.commentState return True def commentEndState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--\uFFFD" + self.currentToken["data"] += "--\uFFFD" self.state = self.commentState elif data == "!": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-bang-after-double-dash-in-comment"}) + "unexpected-bang-after-double-dash-in-comment"}) self.state = self.commentEndBangState - elif data == u"-": + elif data == "-": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-dash-after-double-dash-in-comment"}) + "unexpected-dash-after-double-dash-in-comment"}) self.currentToken["data"] += data elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-double-dash"}) + "eof-in-comment-double-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-comment"}) - self.currentToken["data"] += u"--" + data + "unexpected-char-in-comment"}) + self.currentToken["data"] += "--" + data self.state = self.commentState return True def commentEndBangState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"-": + elif data == "-": self.currentToken["data"] += "--!" self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--!\uFFFD" + self.currentToken["data"] += "--!\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-bang-state"}) + "eof-in-comment-end-bang-state"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"--!" + data + self.currentToken["data"] += "--!" + data self.state = self.commentState return True @@ -1314,13 +1304,13 @@ class HTMLTokenizer(object): self.state = self.beforeDoctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "need-space-after-doctype"}) + "need-space-after-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeNameState return True @@ -1329,20 +1319,20 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-right-bracket"}) + "expected-doctype-name-but-got-right-bracket"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] = u"\uFFFD" + self.currentToken["name"] = "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1356,18 +1346,18 @@ class HTMLTokenizer(object): if data in spaceCharacters: self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.state = self.afterDoctypeNameState - elif data == u">": + elif data == ">": self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype-name"}) + "eof-in-doctype-name"}) self.currentToken["correct"] = False self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) @@ -1380,21 +1370,21 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.currentToken["correct"] = False self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - if data in (u"p", u"P"): + if data in ("p", "P"): matched = True - for expected in ((u"u", u"U"), (u"b", u"B"), (u"l", u"L"), - (u"i", u"I"), (u"c", u"C")): + for expected in (("u", "U"), ("b", "B"), ("l", "L"), + ("i", "I"), ("c", "C")): data = self.stream.char() if data not in expected: matched = False @@ -1402,10 +1392,10 @@ class HTMLTokenizer(object): if matched: self.state = self.afterDoctypePublicKeywordState return True - elif data in (u"s", u"S"): + elif data in ("s", "S"): matched = True - for expected in ((u"y", u"Y"), (u"s", u"S"), (u"t", u"T"), - (u"e", u"E"), (u"m", u"M")): + for expected in (("y", "Y"), ("s", "S"), ("t", "T"), + ("e", "E"), ("m", "M")): data = self.stream.char() if data not in expected: matched = False @@ -1420,25 +1410,25 @@ class HTMLTokenizer(object): # and needs to be ungetted self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-space-or-right-bracket-in-doctype", "datavars": - {"data": data}}) + "expected-space-or-right-bracket-in-doctype", "datavars": + {"data": data}}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypePublicKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypePublicIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypePublicIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1452,26 +1442,26 @@ class HTMLTokenizer(object): if data in spaceCharacters: pass elif data == "\"": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierDoubleQuotedState elif data == "'": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1480,19 +1470,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1504,19 +1494,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1533,27 +1523,27 @@ class HTMLTokenizer(object): self.state = self.dataState elif data == '"': self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def betweenDoctypePublicAndSystemIdentifiersState(self): data = self.stream.char() if data in spaceCharacters: @@ -1562,36 +1552,36 @@ class HTMLTokenizer(object): self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data == '"': - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypeSystemKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypeSystemIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1599,32 +1589,32 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState return True - + def beforeDoctypeSystemIdentifierState(self): data = self.stream.char() if data in spaceCharacters: pass elif data == "\"": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1633,19 +1623,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1657,19 +1647,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1686,19 +1676,19 @@ class HTMLTokenizer(object): self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.state = self.bogusDoctypeState return True def bogusDoctypeState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: @@ -1713,32 +1703,29 @@ class HTMLTokenizer(object): def cdataSectionState(self): data = [] while True: - data.append(self.stream.charsUntil(u"]")) - charStack = [] - - for expected in ["]", "]", ">"]: - charStack.append(self.stream.char()) - matched = True - if charStack[-1] == EOF: - data.extend(charStack[:-1]) - break - elif charStack[-1] != expected: - matched = False - data.extend(charStack) - break - - if matched: + data.append(self.stream.charsUntil("]")) + data.append(self.stream.charsUntil(">")) + char = self.stream.char() + if char == EOF: break + else: + assert char == ">" + if data[-1][-2:] == "]]": + data[-1] = data[-1][:-2] + break + else: + data.append(char) + data = "".join(data) - #Deal with null here rather than in the parser - nullCount = data.count(u"\u0000") + # Deal with null here rather than in the parser + nullCount = data.count("\u0000") if nullCount > 0: - for i in xrange(nullCount): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + for i in range(nullCount): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - data = data.replace(u"\u0000", u"\uFFFD") + data = data.replace("\u0000", "\uFFFD") if data: - self.tokenQueue.append({"type": tokenTypes["Characters"], + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.dataState return True diff --git a/libs/html5lib/treeadapters/__init__.py b/libs/html5lib/treeadapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/html5lib/treeadapters/sax.py b/libs/html5lib/treeadapters/sax.py new file mode 100644 index 00000000..ad47df95 --- /dev/null +++ b/libs/html5lib/treeadapters/sax.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from xml.sax.xmlreader import AttributesNSImpl + +from ..constants import adjustForeignAttributes, unadjustForeignAttributes + +prefix_mapping = {} +for prefix, localName, namespace in adjustForeignAttributes.values(): + if prefix is not None: + prefix_mapping[prefix] = namespace + + +def to_sax(walker, handler): + """Call SAX-like content handler based on treewalker walker""" + handler.startDocument() + for prefix, namespace in prefix_mapping.items(): + handler.startPrefixMapping(prefix, namespace) + + for token in walker: + type = token["type"] + if type == "Doctype": + continue + elif type in ("StartTag", "EmptyTag"): + attrs = AttributesNSImpl(token["data"], + unadjustForeignAttributes) + handler.startElementNS((token["namespace"], token["name"]), + token["name"], + attrs) + if type == "EmptyTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type == "EndTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type in ("Characters", "SpaceCharacters"): + handler.characters(token["data"]) + elif type == "Comment": + pass + else: + assert False, "Unknown token type" + + for prefix, namespace in prefix_mapping.items(): + handler.endPrefixMapping(prefix) + handler.endDocument() diff --git a/libs/html5lib/treebuilders/__init__.py b/libs/html5lib/treebuilders/__init__.py index 14f66d40..6a6b2a4c 100755 --- a/libs/html5lib/treebuilders/__init__.py +++ b/libs/html5lib/treebuilders/__init__.py @@ -7,7 +7,7 @@ implement several things: 1) A set of classes for various types of elements: Document, Doctype, Comment, Element. These must implement the interface of _base.treebuilders.Node (although comment nodes have a different -signature for their constructor, see treebuilders.simpletree.Comment) +signature for their constructor, see treebuilders.etree.Comment) Textual content may also be implemented as another node type, or not, as your tree implementation requires. @@ -24,73 +24,53 @@ getDocument - Returns the root node of the complete document tree testSerializer method on your treebuilder which accepts a node and returns a string containing Node and its children serialized according to the format used in the unittests - -The supplied simpletree module provides a python-only implementation -of a full treebuilder and is a useful reference for the semantics of -the various methods. """ +from __future__ import absolute_import, division, unicode_literals + +from ..utils import default_etree + treeBuilderCache = {} -import sys def getTreeBuilder(treeType, implementation=None, **kwargs): """Get a TreeBuilder class for various types of tree with built-in support - + treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" - - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. - "dom" - A generic builder for DOM implementations, defaulting to - a xml.dom.minidom based implementation for the sake of - backwards compatibility (as releases up until 0.10 had a - builder called "dom" that was a minidom implemenation). - "etree" - A generic builder for tree implementations exposing an - elementtree-like interface (known to work with - ElementTree, cElementTree and lxml.etree). - "beautifulsoup" - Beautiful soup (if installed) - + values are: + + "dom" - A generic builder for DOM implementations, defaulting to + a xml.dom.minidom based implementation. + "etree" - A generic builder for tree implementations exposing an + ElementTree-like interface, defaulting to + xml.etree.cElementTree if available and + xml.etree.ElementTree if not. + "lxml" - A etree-based builder for lxml.etree, handling + limitations of lxml's implementation. + implementation - (Currently applies to the "etree" and "dom" tree types). A module implementing the tree type e.g. - xml.etree.ElementTree or lxml.etree.""" - + xml.etree.ElementTree or xml.etree.cElementTree.""" + treeType = treeType.lower() if treeType not in treeBuilderCache: if treeType == "dom": - import dom - # XXX: Keep backwards compatibility by using minidom if no implementation is given - if implementation == None: + from . import dom + # Come up with a sane default (pref. from the stdlib) + if implementation is None: from xml.dom import minidom implementation = minidom - # XXX: NEVER cache here, caching is done in the dom submodule + # NEVER cache here, caching is done in the dom submodule return dom.getDomModule(implementation, **kwargs).TreeBuilder - elif treeType == "simpletree": - import simpletree - treeBuilderCache[treeType] = simpletree.TreeBuilder - elif treeType == "beautifulsoup": - import soup - treeBuilderCache[treeType] = soup.TreeBuilder elif treeType == "lxml": - import etree_lxml + from . import etree_lxml treeBuilderCache[treeType] = etree_lxml.TreeBuilder elif treeType == "etree": - # Come up with a sane default - if implementation == None: - try: - import xml.etree.cElementTree as ET - except ImportError: - try: - import xml.etree.ElementTree as ET - except ImportError: - try: - import cElementTree as ET - except ImportError: - import elementtree.ElementTree as ET - implementation = ET - import etree + from . import etree + if implementation is None: + implementation = default_etree # NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeBuilder else: - raise ValueError("""Unrecognised treebuilder "%s" """%treeType) + raise ValueError("""Unrecognised treebuilder "%s" """ % treeType) return treeBuilderCache.get(treeType) diff --git a/libs/html5lib/treebuilders/_base.py b/libs/html5lib/treebuilders/_base.py index f3782d28..8b97cc11 100755 --- a/libs/html5lib/treebuilders/_base.py +++ b/libs/html5lib/treebuilders/_base.py @@ -1,25 +1,34 @@ -from html5lib.constants import scopingElements, tableInsertModeElements, namespaces -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from ..constants import scopingElements, tableInsertModeElements, namespaces # The scope markers are inserted when entering object elements, # marquees, table cells, and table captions, and are used to prevent formatting # from "leaking" into tables, object elements, and marquees. Marker = None +listElementsMap = { + None: (frozenset(scopingElements), False), + "button": (frozenset(scopingElements | set([(namespaces["html"], "button")])), False), + "list": (frozenset(scopingElements | set([(namespaces["html"], "ol"), + (namespaces["html"], "ul")])), False), + "table": (frozenset([(namespaces["html"], "html"), + (namespaces["html"], "table")]), False), + "select": (frozenset([(namespaces["html"], "optgroup"), + (namespaces["html"], "option")]), True) +} + + class Node(object): def __init__(self, name): """Node representing an item in the tree. name - The tag name associated with the node parent - The parent of the current node (or None for the document node) - value - The value of the current node (applies to text nodes and + value - The value of the current node (applies to text nodes and comments attributes - a dict holding name, value pairs for attributes of the node - childNodes - a list of child nodes of the current node. This must + childNodes - a list of child nodes of the current node. This must include all elements but not necessarily other node types _flags - A list of miscellaneous flags that can be set on the node """ @@ -30,14 +39,14 @@ class Node(object): self.childNodes = [] self._flags = [] - def __unicode__(self): - attributesStr = " ".join(["%s=\"%s\""%(name, value) - for name, value in - self.attributes.iteritems()]) + def __str__(self): + attributesStr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in + self.attributes.items()]) if attributesStr: - return "<%s %s>"%(self.name,attributesStr) + return "<%s %s>" % (self.name, attributesStr) else: - return "<%s>"%(self.name) + return "<%s>" % (self.name) def __repr__(self): return "<%s>" % (self.name) @@ -48,14 +57,14 @@ class Node(object): raise NotImplementedError def insertText(self, data, insertBefore=None): - """Insert data as text in the current node, positioned before the + """Insert data as text in the current node, positioned before the start of node insertBefore or to the end of the node's text. """ raise NotImplementedError def insertBefore(self, node, refNode): - """Insert node as a child of the current node, before refNode in the - list of child nodes. Raises ValueError if refNode is not a child of + """Insert node as a child of the current node, before refNode in the + list of child nodes. Raises ValueError if refNode is not a child of the current node""" raise NotImplementedError @@ -65,11 +74,11 @@ class Node(object): raise NotImplementedError def reparentChildren(self, newParent): - """Move all the children of the current node to newParent. - This is needed so that trees that don't store text as nodes move the + """Move all the children of the current node to newParent. + This is needed so that trees that don't store text as nodes move the text in the correct way """ - #XXX - should this method be made more general? + # XXX - should this method be made more general? for child in self.childNodes: newParent.appendChild(child) self.childNodes = [] @@ -80,12 +89,12 @@ class Node(object): """ raise NotImplementedError - def hasContent(self): """Return true if the node has children or text, false otherwise """ raise NotImplementedError + class ActiveFormattingElements(list): def append(self, node): equalCount = 0 @@ -103,12 +112,13 @@ class ActiveFormattingElements(list): def nodesEqual(self, node1, node2): if not node1.nameTuple == node2.nameTuple: return False - + if not node1.attributes == node2.attributes: return False - + return True + class TreeBuilder(object): """Base treebuilder implementation documentClass - the class to use for the bottommost node of a document @@ -117,19 +127,19 @@ class TreeBuilder(object): doctypeClass - the class to use for doctypes """ - #Document class + # Document class documentClass = None - #The class to use for creating a node + # The class to use for creating a node elementClass = None - #The class to use for creating comments + # The class to use for creating comments commentClass = None - #The class to use for creating doctypes + # The class to use for creating doctypes doctypeClass = None - - #Fragment class + + # Fragment class fragmentClass = None def __init__(self, namespaceHTMLElements): @@ -138,12 +148,12 @@ class TreeBuilder(object): else: self.defaultNamespace = None self.reset() - + def reset(self): self.openElements = [] self.activeFormattingElements = ActiveFormattingElements() - #XXX - rename these to headElement, formElement + # XXX - rename these to headElement, formElement self.headPointer = None self.formPointer = None @@ -153,30 +163,20 @@ class TreeBuilder(object): def elementInScope(self, target, variant=None): - #If we pass a node in we match that. if we pass a string - #match any node with that name + # If we pass a node in we match that. if we pass a string + # match any node with that name exactNode = hasattr(target, "nameTuple") - listElementsMap = { - None:(scopingElements, False), - "button":(scopingElements | set([(namespaces["html"], "button")]), False), - "list":(scopingElements | set([(namespaces["html"], "ol"), - (namespaces["html"], "ul")]), False), - "table":(set([(namespaces["html"], "html"), - (namespaces["html"], "table")]), False), - "select":(set([(namespaces["html"], "optgroup"), - (namespaces["html"], "option")]), True) - } listElements, invert = listElementsMap[variant] for node in reversed(self.openElements): if (node.name == target and not exactNode or - node == target and exactNode): + node == target and exactNode): return True - elif (invert ^ (node.nameTuple in listElements)): + elif (invert ^ (node.nameTuple in listElements)): return False - assert False # We should never reach this point + assert False # We should never reach this point def reconstructActiveFormattingElements(self): # Within this algorithm the order of steps described in the @@ -196,7 +196,7 @@ class TreeBuilder(object): # Step 6 while entry != Marker and entry not in self.openElements: if i == 0: - #This will be reset to 0 below + # This will be reset to 0 below i = -1 break i -= 1 @@ -209,13 +209,13 @@ class TreeBuilder(object): # Step 8 entry = self.activeFormattingElements[i] - clone = entry.cloneNode() #Mainly to get a new copy of the attributes + clone = entry.cloneNode() # Mainly to get a new copy of the attributes # Step 9 - element = self.insertElement({"type":"StartTag", - "name":clone.name, - "namespace":clone.namespace, - "data":clone.attributes}) + element = self.insertElement({"type": "StartTag", + "name": clone.name, + "namespace": clone.namespace, + "data": clone.attributes}) # Step 10 self.activeFormattingElements[i] = element @@ -260,7 +260,7 @@ class TreeBuilder(object): if parent is None: parent = self.openElements[-1] parent.appendChild(self.commentClass(token["data"])) - + def createElement(self, token): """Create an element but don't insert it anywhere""" name = token["name"] @@ -282,10 +282,10 @@ class TreeBuilder(object): self.insertElement = self.insertElementNormal insertFromTable = property(_getInsertFromTable, _setInsertFromTable) - + def insertElementNormal(self, token): name = token["name"] - assert type(name) == unicode, "Element %s not unicode"%name + assert isinstance(name, text_type), "Element %s not unicode" % name namespace = token.get("namespace", self.defaultNamespace) element = self.elementClass(name, namespace) element.attributes = token["data"] @@ -294,13 +294,13 @@ class TreeBuilder(object): return element def insertElementTable(self, token): - """Create an element and insert it into the tree""" + """Create an element and insert it into the tree""" element = self.createElement(token) if self.openElements[-1].name not in tableInsertModeElements: return self.insertElementNormal(token) else: - #We should be in the InTable mode. This means we want to do - #special magic element rearranging + # We should be in the InTable mode. This means we want to do + # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() if insertBefore is None: parent.appendChild(element) @@ -315,7 +315,7 @@ class TreeBuilder(object): parent = self.openElements[-1] if (not self.insertFromTable or (self.insertFromTable and - self.openElements[-1].name + self.openElements[-1].name not in tableInsertModeElements)): parent.insertText(data) else: @@ -323,14 +323,14 @@ class TreeBuilder(object): # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() parent.insertText(data, insertBefore) - + def getTableMisnestedNodePosition(self): """Get the foster parent element, and sibling to insert before (or None) when inserting a misnested table node""" # The foster parent element is the one which comes before the most # recently opened table element # XXX - this is really inelegant - lastTable=None + lastTable = None fosterParent = None insertBefore = None for elm in self.openElements[::-1]: @@ -354,7 +354,7 @@ class TreeBuilder(object): name = self.openElements[-1].name # XXX td, th and tr are not actually needed if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) - and name != exclude): + and name != exclude): self.openElements.pop() # XXX This is not entirely what the specification says. We should # investigate it more closely. @@ -363,10 +363,10 @@ class TreeBuilder(object): def getDocument(self): "Return the final tree" return self.document - + def getFragment(self): "Return the final fragment" - #assert self.innerHTML + # assert self.innerHTML fragment = self.fragmentClass() self.openElements[0].reparentChildren(fragment) return fragment diff --git a/libs/html5lib/treebuilders/dom.py b/libs/html5lib/treebuilders/dom.py index 9578da2b..61e5ed79 100644 --- a/libs/html5lib/treebuilders/dom.py +++ b/libs/html5lib/treebuilders/dom.py @@ -1,45 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals -from xml.dom import minidom, Node, XML_NAMESPACE, XMLNS_NAMESPACE -try: - from types import ModuleType -except: - from new import module as ModuleType -import re + +from xml.dom import minidom, Node import weakref -import _base -from html5lib import constants, ihatexml -from html5lib.constants import namespaces +from . import _base +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory -moduleCache = {} - -def getDomModule(DomImplementation): - name = "_" + DomImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType(name) - objs = getDomBuilder(DomImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getDomBuilder(DomImplementation): Dom = DomImplementation + class AttrList(object): def __init__(self, element): self.element = element + def __iter__(self): - return self.element.attributes.items().__iter__() + return list(self.element.attributes.items()).__iter__() + def __setitem__(self, name, value): self.element.setAttribute(name, value) + def __len__(self): - return len(self.element.attributes.items()) + return len(list(self.element.attributes.items())) + def items(self): return [(item[0], item[1]) for item in - self.element.attributes.items()] + list(self.element.attributes.items())] + def keys(self): - return self.element.attributes.keys() + return list(self.element.attributes.keys()) + def __getitem__(self, name): return self.element.getAttribute(name) @@ -48,68 +41,68 @@ def getDomBuilder(DomImplementation): raise NotImplementedError else: return self.element.hasAttribute(name) - + class NodeBuilder(_base.Node): def __init__(self, element): _base.Node.__init__(self, element.nodeName) self.element = element - namespace = property(lambda self:hasattr(self.element, "namespaceURI") + namespace = property(lambda self: hasattr(self.element, "namespaceURI") and self.element.namespaceURI or None) def appendChild(self, node): node.parent = self self.element.appendChild(node.element) - + def insertText(self, data, insertBefore=None): text = self.element.ownerDocument.createTextNode(data) if insertBefore: self.element.insertBefore(text, insertBefore.element) else: self.element.appendChild(text) - + def insertBefore(self, node, refNode): self.element.insertBefore(node.element, refNode.element) node.parent = self - + def removeChild(self, node): if node.element.parentNode == self.element: self.element.removeChild(node.element) node.parent = None - + def reparentChildren(self, newParent): while self.element.hasChildNodes(): child = self.element.firstChild self.element.removeChild(child) newParent.element.appendChild(child) self.childNodes = [] - + def getAttributes(self): return AttrList(self.element) - + def setAttributes(self, attributes): if attributes: - for name, value in attributes.items(): + for name, value in list(attributes.items()): if isinstance(name, tuple): if name[0] is not None: qualifiedName = (name[0] + ":" + name[1]) else: qualifiedName = name[1] - self.element.setAttributeNS(name[2], qualifiedName, + self.element.setAttributeNS(name[2], qualifiedName, value) else: self.element.setAttribute( name, value) attributes = property(getAttributes, setAttributes) - + def cloneNode(self): return NodeBuilder(self.element.cloneNode(False)) - + def hasContent(self): return self.element.hasChildNodes() def getNameTuple(self): - if self.namespace == None: + if self.namespace is None: return namespaces["html"], self.name else: return self.namespace, self.name @@ -118,9 +111,9 @@ def getDomBuilder(DomImplementation): class TreeBuilder(_base.TreeBuilder): def documentClass(self): - self.dom = Dom.getDOMImplementation().createDocument(None,None,None) + self.dom = Dom.getDOMImplementation().createDocument(None, None, None) return weakref.proxy(self) - + def insertDoctype(self, token): name = token["name"] publicId = token["publicId"] @@ -131,7 +124,7 @@ def getDomBuilder(DomImplementation): self.document.appendChild(NodeBuilder(doctype)) if Dom == minidom: doctype.ownerDocument = self.dom - + def elementClass(self, name, namespace=None): if namespace is None and self.defaultNamespace is None: node = self.dom.createElement(name) @@ -139,70 +132,72 @@ def getDomBuilder(DomImplementation): node = self.dom.createElementNS(namespace, name) return NodeBuilder(node) - + def commentClass(self, data): return NodeBuilder(self.dom.createComment(data)) - + def fragmentClass(self): return NodeBuilder(self.dom.createDocumentFragment()) - + def appendChild(self, node): self.dom.appendChild(node.element) - + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): return self.dom - + def getFragment(self): return _base.TreeBuilder.getFragment(self).element - + def insertText(self, data, parent=None): - data=data - if parent <> self: + data = data + if parent != self: _base.TreeBuilder.insertText(self, data, parent) else: # HACK: allow text nodes as children of the document node if hasattr(self.dom, '_child_node_types'): if not Node.TEXT_NODE in self.dom._child_node_types: - self.dom._child_node_types=list(self.dom._child_node_types) + self.dom._child_node_types = list(self.dom._child_node_types) self.dom._child_node_types.append(Node.TEXT_NODE) self.dom.appendChild(self.dom.createTextNode(data)) - + + implementation = DomImplementation name = None - + def testSerializer(element): element.normalize() rv = [] + def serializeElement(element, indent=0): if element.nodeType == Node.DOCUMENT_TYPE_NODE: if element.name: if element.publicId or element.systemId: publicId = element.publicId or "" systemId = element.systemId or "" - rv.append( """|%s"""%( - ' '*indent, element.name, publicId, systemId)) + rv.append("""|%s""" % + (' ' * indent, element.name, publicId, systemId)) else: - rv.append("|%s"%(' '*indent, element.name)) + rv.append("|%s" % (' ' * indent, element.name)) else: - rv.append("|%s"%(' '*indent,)) + rv.append("|%s" % (' ' * indent,)) elif element.nodeType == Node.DOCUMENT_NODE: rv.append("#document") elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE: rv.append("#document-fragment") elif element.nodeType == Node.COMMENT_NODE: - rv.append("|%s"%(' '*indent, element.nodeValue)) + rv.append("|%s" % (' ' * indent, element.nodeValue)) elif element.nodeType == Node.TEXT_NODE: - rv.append("|%s\"%s\"" %(' '*indent, element.nodeValue)) + rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue)) else: if (hasattr(element, "namespaceURI") and - element.namespaceURI != None): - name = "%s %s"%(constants.prefixes[element.namespaceURI], - element.nodeName) + element.namespaceURI is not None): + name = "%s %s" % (constants.prefixes[element.namespaceURI], + element.nodeName) else: name = element.nodeName - rv.append("|%s<%s>"%(' '*indent, name)) + rv.append("|%s<%s>" % (' ' * indent, name)) if element.hasAttributes(): attributes = [] for i in range(len(element.attributes)): @@ -211,81 +206,22 @@ def getDomBuilder(DomImplementation): value = attr.value ns = attr.namespaceURI if ns: - name = "%s %s"%(constants.prefixes[ns], attr.localName) + name = "%s %s" % (constants.prefixes[ns], attr.localName) else: name = attr.nodeName attributes.append((name, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) indent += 2 for child in element.childNodes: serializeElement(child, indent) serializeElement(element, 0) - + return "\n".join(rv) - - def dom2sax(node, handler, nsmap={'xml':XML_NAMESPACE}): - if node.nodeType == Node.ELEMENT_NODE: - if not nsmap: - handler.startElement(node.nodeName, node.attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElement(node.nodeName) - else: - attributes = dict(node.attributes.itemsNS()) - - # gather namespace declarations - prefixes = [] - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if (attr.namespaceURI == XMLNS_NAMESPACE or - (attr.namespaceURI == None and attr.nodeName.startswith('xmlns'))): - prefix = (attr.nodeName != 'xmlns' and attr.nodeName or None) - handler.startPrefixMapping(prefix, attr.nodeValue) - prefixes.append(prefix) - nsmap = nsmap.copy() - nsmap[prefix] = attr.nodeValue - del attributes[(attr.namespaceURI, attr.nodeName)] - - # apply namespace declarations - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if attr.namespaceURI == None and ':' in attr.nodeName: - prefix = attr.nodeName.split(':')[0] - if nsmap.has_key(prefix): - del attributes[(attr.namespaceURI, attr.nodeName)] - attributes[(nsmap[prefix],attr.nodeName)]=attr.nodeValue - - # SAX events - ns = node.namespaceURI or nsmap.get(None,None) - handler.startElementNS((ns,node.nodeName), node.nodeName, attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElementNS((ns, node.nodeName), node.nodeName) - for prefix in prefixes: handler.endPrefixMapping(prefix) - - elif node.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: - handler.characters(node.nodeValue) - - elif node.nodeType == Node.DOCUMENT_NODE: - handler.startDocument() - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endDocument() - - elif node.nodeType == Node.DOCUMENT_FRAGMENT_NODE: - for child in node.childNodes: dom2sax(child, handler, nsmap) - - else: - # ATTRIBUTE_NODE - # ENTITY_NODE - # PROCESSING_INSTRUCTION_NODE - # COMMENT_NODE - # DOCUMENT_TYPE_NODE - # NOTATION_NODE - pass - + return locals() -# Keep backwards compatibility with things that directly load -# classes/functions from this module -for key, value in getDomModule(minidom).__dict__.items(): - globals()[key] = value + +# The actual means to get a module! +getDomModule = moduleFactoryFactory(getDomBuilder) diff --git a/libs/html5lib/treebuilders/etree.py b/libs/html5lib/treebuilders/etree.py index 95be4755..2c8ed19f 100755 --- a/libs/html5lib/treebuilders/etree.py +++ b/libs/html5lib/treebuilders/etree.py @@ -1,32 +1,21 @@ -try: - from types import ModuleType -except: - from new import module as ModuleType -import re -import types +from __future__ import absolute_import, division, unicode_literals +from six import text_type -import _base -from html5lib import ihatexml -from html5lib import constants -from html5lib.constants import namespaces +import re + +from . import _base +from .. import ihatexml +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation, fullTree=False): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation, fullTree) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation, fullTree=False): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag + class Element(_base.Node): def __init__(self, name, namespace=None): self._name = name @@ -45,16 +34,16 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) return etree_tag - + def _setName(self, name): self._name = name self._element.tag = self._getETreeTag(self._name, self._namespace) - + def _getName(self): return self._name - + name = property(_getName, _setName) def _setNamespace(self, namespace): @@ -65,81 +54,82 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): return self._namespace namespace = property(_getNamespace, _setNamespace) - + def _getAttributes(self): return self._element.attrib - + def _setAttributes(self, attributes): - #Delete existing attributes first - #XXX - there may be a better way to do this... - for key in self._element.attrib.keys(): + # Delete existing attributes first + # XXX - there may be a better way to do this... + for key in list(self._element.attrib.keys()): del self._element.attrib[key] - for key, value in attributes.iteritems(): + for key, value in attributes.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], key[1]) + name = "{%s}%s" % (key[2], key[1]) else: name = key self._element.set(name, value) - + attributes = property(_getAttributes, _setAttributes) - + def _getChildNodes(self): - return self._childNodes + return self._childNodes + def _setChildNodes(self, value): del self._element[:] self._childNodes = [] for element in value: self.insertChild(element) - + childNodes = property(_getChildNodes, _setChildNodes) - + def hasContent(self): """Return true if the node has children or text""" return bool(self._element.text or len(self._element)) - + def appendChild(self, node): self._childNodes.append(node) self._element.append(node._element) node.parent = self - + def insertBefore(self, node, refNode): index = list(self._element).index(refNode._element) self._element.insert(index, node._element) node.parent = self - + def removeChild(self, node): self._element.remove(node._element) - node.parent=None - + node.parent = None + def insertText(self, data, insertBefore=None): if not(len(self._element)): if not self._element.text: self._element.text = "" self._element.text += data elif insertBefore is None: - #Insert the text as the tail of the last child element + # Insert the text as the tail of the last child element if not self._element[-1].tail: self._element[-1].tail = "" self._element[-1].tail += data else: - #Insert the text before the specified node + # Insert the text before the specified node children = list(self._element) index = children.index(insertBefore._element) if index > 0: - if not self._element[index-1].tail: - self._element[index-1].tail = "" - self._element[index-1].tail += data + if not self._element[index - 1].tail: + self._element[index - 1].tail = "" + self._element[index - 1].tail += data else: if not self._element.text: self._element.text = "" self._element.text += data - + def cloneNode(self): element = type(self)(self.name, self.namespace) - for name, value in self.attributes.iteritems(): + for name, value in self.attributes.items(): element.attributes[name] = value return element - + def reparentChildren(self, newParent): if newParent.childNodes: newParent.childNodes[-1]._element.tail += self._element.text @@ -150,60 +140,60 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): newParent._element.text += self._element.text self._element.text = "" _base.Node.reparentChildren(self, newParent) - + class Comment(Element): def __init__(self, data): - #Use the superclass constructor to set all properties on the - #wrapper element + # Use the superclass constructor to set all properties on the + # wrapper element self._element = ElementTree.Comment(data) self.parent = None self._childNodes = [] self._flags = [] - + def _getData(self): return self._element.text - + def _setData(self, value): self._element.text = value - + data = property(_getData, _setData) - + class DocumentType(Element): def __init__(self, name, publicId, systemId): - Element.__init__(self, "") + Element.__init__(self, "") self._element.text = name self.publicId = publicId self.systemId = systemId def _getPublicId(self): - return self._element.get(u"publicId", "") + return self._element.get("publicId", "") def _setPublicId(self, value): if value is not None: - self._element.set(u"publicId", value) + self._element.set("publicId", value) publicId = property(_getPublicId, _setPublicId) - + def _getSystemId(self): - return self._element.get(u"systemId", "") + return self._element.get("systemId", "") def _setSystemId(self, value): if value is not None: - self._element.set(u"systemId", value) + self._element.set("systemId", value) systemId = property(_getSystemId, _setSystemId) - + class Document(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_ROOT") + class DocumentFragment(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_FRAGMENT") + def testSerializer(element): rv = [] - finalText = None + def serializeElement(element, indent=0): if not(hasattr(element, "tag")): element = element.getroot() @@ -211,20 +201,23 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": + rv.append("""""" % + (element.text, publicId, systemId)) + else: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": rv.append("#document") - if element.text: - rv.append("|%s\"%s\""%(' '*(indent+2), element.text)) - if element.tail: - finalText = element.tail - elif element.tag == ElementTree.Comment: - rv.append("|%s"%(' '*indent, element.text)) + if element.text is not None: + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + elif element.tag == ElementTreeCommentType: + rv.append("|%s" % (' ' * indent, element.text)) else: - assert type(element.tag) in types.StringTypes, "Expected unicode, got %s"%type(element.tag) + assert isinstance(element.tag, text_type), \ + "Expected unicode, got %s, %s" % (type(element.tag), element.tag) nsmatch = tag_regexp.match(element.tag) if nsmatch is None: @@ -232,113 +225,113 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): else: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - name = "%s %s"%(prefix, name) - rv.append("|%s<%s>"%(' '*indent, name)) + name = "%s %s" % (prefix, name) + rv.append("|%s<%s>" % (' ' * indent, name)) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: attr_string = name attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 for child in element: serializeElement(child, indent) if element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) - - if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) - + return "\n".join(rv) - + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] - finalText = None filter = ihatexml.InfosetFilter() + def serializeElement(element): - if type(element) == type(ElementTree.ElementTree): + if isinstance(element, ElementTree.ElementTree): element = element.getroot() - + if element.tag == "": if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": - if element.text: - rv.append(element.text) - if element.tail: - finalText = element.tail - - for child in element: - serializeElement(child) - - elif type(element.tag) == type(ElementTree.Comment): - rv.append(""%(element.text,)) - else: - #This is assumed to be an ordinary element - if not element.attrib: - rv.append("<%s>"%(filter.fromXmlName(element.tag),)) + rv.append("""""" % + (element.text, publicId, systemId)) else: - attr = " ".join(["%s=\"%s\""%( - filter.fromXmlName(name), value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) - if element.text: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": + if element.text is not None: rv.append(element.text) - + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + for child in element: serializeElement(child) - - rv.append("%s>"%(element.tag,)) - + + elif element.tag == ElementTreeCommentType: + rv.append("" % (element.text,)) + else: + # This is assumed to be an ordinary element + if not element.attrib: + rv.append("<%s>" % (filter.fromXmlName(element.tag),)) + else: + attr = " ".join(["%s=\"%s\"" % ( + filter.fromXmlName(name), value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) + if element.text: + rv.append(element.text) + + for child in element: + serializeElement(child) + + rv.append("%s>" % (element.tag,)) + if element.tail: rv.append(element.tail) - + serializeElement(element) - - if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) - + return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = Element commentClass = Comment fragmentClass = DocumentFragment - + implementation = ElementTreeImplementation + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): if fullTree: return self.document._element else: if self.defaultNamespace is not None: return self.document._element.find( - "{%s}html"%self.defaultNamespace) + "{%s}html" % self.defaultNamespace) else: return self.document._element.find("html") - + def getFragment(self): return _base.TreeBuilder.getFragment(self)._element - + return locals() + + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treebuilders/etree_lxml.py b/libs/html5lib/treebuilders/etree_lxml.py index eee1e3b2..35d08efa 100644 --- a/libs/html5lib/treebuilders/etree_lxml.py +++ b/libs/html5lib/treebuilders/etree_lxml.py @@ -1,20 +1,3 @@ -import warnings -import re - -import _base -from html5lib.constants import DataLossWarning -import html5lib.constants as constants -import etree as etree_builders -from html5lib import ihatexml - -try: - import lxml.etree as etree -except ImportError: - pass - -fullTree = True -tag_regexp = re.compile("{([^}]*)}(.*)") - """Module for supporting the lxml.etree library. The idea here is to use as much of the native library as possible, without using fragile hacks like custom element names that break between releases. The downside of this is that we cannot represent @@ -26,12 +9,34 @@ Docypes with no name When any of these things occur, we emit a DataLossWarning """ +from __future__ import absolute_import, division, unicode_literals + +import warnings +import re +import sys + +from . import _base +from ..constants import DataLossWarning +from .. import constants +from . import etree as etree_builders +from .. import ihatexml + +import lxml.etree as etree + + +fullTree = True +tag_regexp = re.compile("{([^}]*)}(.*)") + +comment_type = etree.Comment("asd").tag + + class DocumentType(object): def __init__(self, name, publicId, systemId): - self.name = name + self.name = name self.publicId = publicId self.systemId = systemId + class Document(object): def __init__(self): self._elementTree = None @@ -42,118 +47,126 @@ class Document(object): def _getChildNodes(self): return self._childNodes - + childNodes = property(_getChildNodes) + def testSerializer(element): rv = [] finalText = None - filter = ihatexml.InfosetFilter() + infosetFilter = ihatexml.InfosetFilter() + def serializeElement(element, indent=0): if not hasattr(element, "tag"): - if hasattr(element, "getroot"): - #Full tree case + if hasattr(element, "getroot"): + # Full tree case rv.append("#document") if element.docinfo.internalDTD: - if not (element.docinfo.public_id or + if not (element.docinfo.public_id or element.docinfo.system_url): - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name else: - dtd_str = """"""%( - element.docinfo.root_name, + dtd_str = """""" % ( + element.docinfo.root_name, element.docinfo.public_id, element.docinfo.system_url) - rv.append("|%s%s"%(' '*(indent+2), dtd_str)) + rv.append("|%s%s" % (' ' * (indent + 2), dtd_str)) next_element = element.getroot() while next_element.getprevious() is not None: next_element = next_element.getprevious() while next_element is not None: - serializeElement(next_element, indent+2) + serializeElement(next_element, indent + 2) next_element = next_element.getnext() - elif isinstance(element, basestring): - #Text in a fragment - rv.append("|%s\"%s\""%(' '*indent, element)) + elif isinstance(element, str) or isinstance(element, bytes): + # Text in a fragment + assert isinstance(element, str) or sys.version_info.major == 2 + rv.append("|%s\"%s\"" % (' ' * indent, element)) else: - #Fragment case + # Fragment case rv.append("#document-fragment") for next_element in element: - serializeElement(next_element, indent+2) - elif type(element.tag) == type(etree.Comment): - rv.append("|%s"%(' '*indent, element.text)) + serializeElement(next_element, indent + 2) + elif element.tag == comment_type: + rv.append("|%s" % (' ' * indent, element.text)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * indent, element.tail)) else: + assert isinstance(element, etree._Element) nsmatch = etree_builders.tag_regexp.match(element.tag) if nsmatch is not None: ns = nsmatch.group(1) tag = nsmatch.group(2) prefix = constants.prefixes[ns] - rv.append("|%s<%s %s>"%(' '*indent, prefix, - filter.fromXmlName(tag))) + rv.append("|%s<%s %s>" % (' ' * indent, prefix, + infosetFilter.fromXmlName(tag))) else: - rv.append("|%s<%s>"%(' '*indent, - filter.fromXmlName(element.tag))) + rv.append("|%s<%s>" % (' ' * indent, + infosetFilter.fromXmlName(element.tag))) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() - name = filter.fromXmlName(name) + name = infosetFilter.fromXmlName(name) prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: - attr_string = filter.fromXmlName(name) + attr_string = infosetFilter.fromXmlName(name) attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 - for child in element.getchildren(): + for child in element: serializeElement(child, indent) - if hasattr(element, "tail") and element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) + rv.append("|%s\"%s\"" % (' ' * 2, finalText)) return "\n".join(rv) + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] finalText = None + def serializeElement(element): if not hasattr(element, "tag"): if element.docinfo.internalDTD: if element.docinfo.doctype: dtd_str = element.docinfo.doctype else: - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name rv.append(dtd_str) serializeElement(element.getroot()) - - elif type(element.tag) == type(etree.Comment): - rv.append(""%(element.text,)) - + + elif element.tag == comment_type: + rv.append("" % (element.text,)) + else: - #This is assumed to be an ordinary element + # This is assumed to be an ordinary element if not element.attrib: - rv.append("<%s>"%(element.tag,)) + rv.append("<%s>" % (element.tag,)) else: - attr = " ".join(["%s=\"%s\""%(name, value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) + attr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) if element.text: rv.append(element.text) - for child in element.getchildren(): + for child in element: serializeElement(child) - rv.append("%s>"%(element.tag,)) + rv.append("%s>" % (element.tag,)) if hasattr(element, "tail") and element.tail: rv.append(element.tail) @@ -161,56 +174,57 @@ def tostring(element): serializeElement(element) if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) + rv.append("%s\"" % (' ' * 2, finalText)) return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = None commentClass = None - fragmentClass = Document + fragmentClass = Document + implementation = etree - def __init__(self, namespaceHTMLElements, fullTree = False): + def __init__(self, namespaceHTMLElements, fullTree=False): builder = etree_builders.getETreeModule(etree, fullTree=fullTree) - filter = self.filter = ihatexml.InfosetFilter() + infosetFilter = self.infosetFilter = ihatexml.InfosetFilter() self.namespaceHTMLElements = namespaceHTMLElements class Attributes(dict): def __init__(self, element, value={}): self._element = element dict.__init__(self, value) - for key, value in self.iteritems(): + for key, value in self.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value def __setitem__(self, key, value): dict.__setitem__(self, key, value) if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value class Element(builder.Element): def __init__(self, name, namespace): - name = filter.coerceElement(name) + name = infosetFilter.coerceElement(name) builder.Element.__init__(self, name, namespace=namespace) self._attributes = Attributes(self) def _setName(self, name): - self._name = filter.coerceElement(name) + self._name = infosetFilter.coerceElement(name) self._element.tag = self._getETreeTag( self._name, self._namespace) - + def _getName(self): - return filter.fromXmlName(self._name) - + return infosetFilter.fromXmlName(self._name) + name = property(_getName, _setName) def _getAttributes(self): @@ -218,24 +232,23 @@ class TreeBuilder(_base.TreeBuilder): def _setAttributes(self, attributes): self._attributes = Attributes(self, attributes) - + attributes = property(_getAttributes, _setAttributes) def insertText(self, data, insertBefore=None): - data = filter.coerceCharacters(data) + data = infosetFilter.coerceCharacters(data) builder.Element.insertText(self, data, insertBefore) def appendChild(self, child): builder.Element.appendChild(self, child) - class Comment(builder.Comment): def __init__(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) builder.Comment.__init__(self, data) def _setData(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) self._element.text = data def _getData(self): @@ -245,9 +258,9 @@ class TreeBuilder(_base.TreeBuilder): self.elementClass = Element self.commentClass = builder.Comment - #self.fragmentClass = builder.DocumentFragment + # self.fragmentClass = builder.DocumentFragment _base.TreeBuilder.__init__(self, namespaceHTMLElements) - + def reset(self): _base.TreeBuilder.reset(self) self.insertComment = self.insertCommentInitial @@ -262,13 +275,13 @@ class TreeBuilder(_base.TreeBuilder): return self.document._elementTree else: return self.document._elementTree.getroot() - + def getFragment(self): fragment = [] element = self.openElements[0]._element if element.text: fragment.append(element.text) - fragment.extend(element.getchildren()) + fragment.extend(list(element)) if element.tail: fragment.append(element.tail) return fragment @@ -278,59 +291,79 @@ class TreeBuilder(_base.TreeBuilder): publicId = token["publicId"] systemId = token["systemId"] - if not name or ihatexml.nonXmlNameBMPRegexp.search(name) or name[0] == '"': - warnings.warn("lxml cannot represent null or non-xml doctype", DataLossWarning) + if not name: + warnings.warn("lxml cannot represent empty doctype", DataLossWarning) + self.doctype = None + else: + coercedName = self.infosetFilter.coerceElement(name) + if coercedName != name: + warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning) + + doctype = self.doctypeClass(coercedName, publicId, systemId) + self.doctype = doctype - doctype = self.doctypeClass(name, publicId, systemId) - self.doctype = doctype - def insertCommentInitial(self, data, parent=None): self.initial_comments.append(data) - + + def insertCommentMain(self, data, parent=None): + if (parent == self.document and + self.document._elementTree.getroot()[-1].tag == comment_type): + warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning) + super(TreeBuilder, self).insertComment(data, parent) + def insertRoot(self, token): """Create the document root""" - #Because of the way libxml2 works, it doesn't seem to be possible to - #alter information like the doctype after the tree has been parsed. - #Therefore we need to use the built-in parser to create our iniial - #tree, after which we can add elements like normal + # Because of the way libxml2 works, it doesn't seem to be possible to + # alter information like the doctype after the tree has been parsed. + # Therefore we need to use the built-in parser to create our iniial + # tree, after which we can add elements like normal docStr = "" - if self.doctype and self.doctype.name and not self.doctype.name.startswith('"'): - docStr += "= 0 and sysid.find('"') >= 0: + warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning) + sysid = sysid.replace("'", 'U00027') + if sysid.find("'") >= 0: + docStr += '"%s"' % sysid + else: + docStr += "'%s'" % sysid + else: + docStr += "''" docStr += ">" + if self.doctype.name != token["name"]: + warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning) docStr += "" - - try: - root = etree.fromstring(docStr) - except etree.XMLSyntaxError: - print docStr - raise - - #Append the initial comments: + root = etree.fromstring(docStr) + + # Append the initial comments: for comment_token in self.initial_comments: root.addprevious(etree.Comment(comment_token["data"])) - - #Create the root document and add the ElementTree to it + + # Create the root document and add the ElementTree to it self.document = self.documentClass() self.document._elementTree = root.getroottree() - + # Give the root element the right name name = token["name"] namespace = token.get("namespace", self.defaultNamespace) if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) root.tag = etree_tag - - #Add the root element to the internal child/open data structures + + # Add the root element to the internal child/open data structures root_element = self.elementClass(name, namespace) root_element._element = root self.document._childNodes.append(root_element) self.openElements.append(root_element) - - #Reset to the default insert comment function - self.insertComment = super(TreeBuilder, self).insertComment + + # Reset to the default insert comment function + self.insertComment = self.insertCommentMain diff --git a/libs/html5lib/treebuilders/simpletree.py b/libs/html5lib/treebuilders/simpletree.py deleted file mode 100755 index 67fe7583..00000000 --- a/libs/html5lib/treebuilders/simpletree.py +++ /dev/null @@ -1,256 +0,0 @@ -import _base -from html5lib.constants import voidElements, namespaces, prefixes -from xml.sax.saxutils import escape - -# Really crappy basic implementation of a DOM-core like thing -class Node(_base.Node): - type = -1 - def __init__(self, name): - self.name = name - self.parent = None - self.value = None - self.childNodes = [] - self._flags = [] - - def __iter__(self): - for node in self.childNodes: - yield node - for item in node: - yield item - - def __unicode__(self): - return self.name - - def toxml(self): - raise NotImplementedError - - def printTree(self, indent=0): - tree = '\n|%s%s' % (' '* indent, unicode(self)) - for child in self.childNodes: - tree += child.printTree(indent + 2) - return tree - - def appendChild(self, node): - assert isinstance(node, Node) - if (isinstance(node, TextNode) and self.childNodes and - isinstance(self.childNodes[-1], TextNode)): - self.childNodes[-1].value += node.value - else: - self.childNodes.append(node) - node.parent = self - - def insertText(self, data, insertBefore=None): - assert isinstance(data, unicode), "data %s is of type %s expected unicode"%(repr(data), type(data)) - if insertBefore is None: - self.appendChild(TextNode(data)) - else: - self.insertBefore(TextNode(data), insertBefore) - - def insertBefore(self, node, refNode): - index = self.childNodes.index(refNode) - if (isinstance(node, TextNode) and index > 0 and - isinstance(self.childNodes[index - 1], TextNode)): - self.childNodes[index - 1].value += node.value - else: - self.childNodes.insert(index, node) - node.parent = self - - def removeChild(self, node): - try: - self.childNodes.remove(node) - except: - # XXX - raise - node.parent = None - - def cloneNode(self): - raise NotImplementedError - - def hasContent(self): - """Return true if the node has children or text""" - return bool(self.childNodes) - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class Document(Node): - type = 1 - def __init__(self): - Node.__init__(self, None) - - def __str__(self): - return "#document" - - def __unicode__(self): - return str(self) - - def appendChild(self, child): - Node.appendChild(self, child) - - def toxml(self, encoding="utf=8"): - result = "" - for child in self.childNodes: - result += child.toxml() - return result.encode(encoding) - - def hilite(self, encoding="utf-8"): - result = "" - for child in self.childNodes: - result += child.hilite() - return result.encode(encoding) + "" - - def printTree(self): - tree = unicode(self) - for child in self.childNodes: - tree += child.printTree(2) - return tree - - def cloneNode(self): - return Document() - -class DocumentFragment(Document): - type = 2 - def __str__(self): - return "#document-fragment" - - def __unicode__(self): - return str(self) - - def cloneNode(self): - return DocumentFragment() - -class DocumentType(Node): - type = 3 - def __init__(self, name, publicId, systemId): - Node.__init__(self, name) - self.publicId = publicId - self.systemId = systemId - - def __unicode__(self): - if self.publicId or self.systemId: - publicId = self.publicId or "" - systemId = self.systemId or "" - return """"""%( - self.name, publicId, systemId) - - else: - return u"" % self.name - - - toxml = __unicode__ - - def hilite(self): - return '<!DOCTYPE %s>' % self.name - - def cloneNode(self): - return DocumentType(self.name, self.publicId, self.systemId) - -class TextNode(Node): - type = 4 - def __init__(self, value): - Node.__init__(self, None) - self.value = value - - def __unicode__(self): - return u"\"%s\"" % self.value - - def toxml(self): - return escape(self.value) - - hilite = toxml - - def cloneNode(self): - return TextNode(self.value) - -class Element(Node): - type = 5 - def __init__(self, name, namespace=None): - Node.__init__(self, name) - self.namespace = namespace - self.attributes = {} - - def __unicode__(self): - if self.namespace == None: - return u"<%s>" % self.name - else: - return u"<%s %s>"%(prefixes[self.namespace], self.name) - - def toxml(self): - result = '<' + self.name - if self.attributes: - for name,value in self.attributes.iteritems(): - result += u' %s="%s"' % (name, escape(value,{'"':'"'})) - if self.childNodes: - result += '>' - for child in self.childNodes: - result += child.toxml() - result += u'%s>' % self.name - else: - result += u'/>' - return result - - def hilite(self): - result = '<%s' % self.name - if self.attributes: - for name, value in self.attributes.iteritems(): - result += ' %s="%s"' % (name, escape(value, {'"':'"'})) - if self.childNodes: - result += ">" - for child in self.childNodes: - result += child.hilite() - elif self.name in voidElements: - return result + ">" - return result + '</%s>' % self.name - - def printTree(self, indent): - tree = '\n|%s%s' % (' '*indent, unicode(self)) - indent += 2 - if self.attributes: - for name, value in sorted(self.attributes.iteritems()): - if isinstance(name, tuple): - name = "%s %s"%(name[0], name[1]) - tree += '\n|%s%s="%s"' % (' ' * indent, name, value) - for child in self.childNodes: - tree += child.printTree(indent) - return tree - - def cloneNode(self): - newNode = Element(self.name) - if hasattr(self, 'namespace'): - newNode.namespace = self.namespace - for attr, value in self.attributes.iteritems(): - newNode.attributes[attr] = value - return newNode - -class CommentNode(Node): - type = 6 - def __init__(self, data): - Node.__init__(self, None) - self.data = data - - def __unicode__(self): - return "" % self.data - - def toxml(self): - return "" % self.data - - def hilite(self): - return '<!--%s-->' % escape(self.data) - - def cloneNode(self): - return CommentNode(self.data) - -class TreeBuilder(_base.TreeBuilder): - documentClass = Document - doctypeClass = DocumentType - elementClass = Element - commentClass = CommentNode - fragmentClass = DocumentFragment - - def testSerializer(self, node): - return node.printTree() diff --git a/libs/html5lib/treebuilders/soup.py b/libs/html5lib/treebuilders/soup.py deleted file mode 100644 index 9bc5ff0e..00000000 --- a/libs/html5lib/treebuilders/soup.py +++ /dev/null @@ -1,236 +0,0 @@ -import warnings - -warnings.warn("BeautifulSoup 3.x (as of 3.1) is not fully compatible with html5lib and support will be removed in the future", DeprecationWarning) - -from BeautifulSoup import BeautifulSoup, Tag, NavigableString, Comment, Declaration - -import _base -from html5lib.constants import namespaces, DataLossWarning - -class AttrList(object): - def __init__(self, element): - self.element = element - self.attrs = dict(self.element.attrs) - def __iter__(self): - return self.attrs.items().__iter__() - def __setitem__(self, name, value): - "set attr", name, value - self.element[name] = value - def items(self): - return self.attrs.items() - def keys(self): - return self.attrs.keys() - def __getitem__(self, name): - return self.attrs[name] - def __contains__(self, name): - return name in self.attrs.keys() - def __eq__(self, other): - if len(self.keys()) != len(other.keys()): - return False - for item in self.keys(): - if item not in other: - return False - if self[item] != other[item]: - return False - return True - -class Element(_base.Node): - def __init__(self, element, soup, namespace): - _base.Node.__init__(self, element.name) - self.element = element - self.soup = soup - self.namespace = namespace - - def _nodeIndex(self, node, refNode): - # Finds a node by identity rather than equality - for index in range(len(self.element.contents)): - if id(self.element.contents[index]) == id(refNode.element): - return index - return None - - def appendChild(self, node): - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[-1].__class__ == NavigableString): - # Concatenate new text onto old text node - # (TODO: This has O(n^2) performance, for input like "aaa...") - newStr = NavigableString(self.element.contents[-1]+node.element) - - # Remove the old text node - # (Can't simply use .extract() by itself, because it fails if - # an equal text node exists within the parent node) - oldElement = self.element.contents[-1] - del self.element.contents[-1] - oldElement.parent = None - oldElement.extract() - - self.element.insert(len(self.element.contents), newStr) - else: - self.element.insert(len(self.element.contents), node.element) - node.parent = self - - def getAttributes(self): - return AttrList(self.element) - - def setAttributes(self, attributes): - if attributes: - for name, value in attributes.items(): - self.element[name] = value - - attributes = property(getAttributes, setAttributes) - - def insertText(self, data, insertBefore=None): - text = TextNode(NavigableString(data), self.soup) - if insertBefore: - self.insertBefore(text, insertBefore) - else: - self.appendChild(text) - - def insertBefore(self, node, refNode): - index = self._nodeIndex(node, refNode) - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[index-1].__class__ == NavigableString): - # (See comments in appendChild) - newStr = NavigableString(self.element.contents[index-1]+node.element) - oldNode = self.element.contents[index-1] - del self.element.contents[index-1] - oldNode.parent = None - oldNode.extract() - - self.element.insert(index-1, newStr) - else: - self.element.insert(index, node.element) - node.parent = self - - def removeChild(self, node): - index = self._nodeIndex(node.parent, node) - del node.parent.element.contents[index] - node.element.parent = None - node.element.extract() - node.parent = None - - def reparentChildren(self, newParent): - while self.element.contents: - child = self.element.contents[0] - child.extract() - if isinstance(child, Tag): - newParent.appendChild(Element(child, self.soup, namespaces["html"])) - else: - newParent.appendChild(TextNode(child, self.soup)) - - def cloneNode(self): - node = Element(Tag(self.soup, self.element.name), self.soup, self.namespace) - for key,value in self.attributes: - node.attributes[key] = value - return node - - def hasContent(self): - return self.element.contents - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class TextNode(Element): - def __init__(self, element, soup): - _base.Node.__init__(self, None) - self.element = element - self.soup = soup - - def cloneNode(self): - raise NotImplementedError - -class TreeBuilder(_base.TreeBuilder): - def __init__(self, namespaceHTMLElements): - if namespaceHTMLElements: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - _base.TreeBuilder.__init__(self, namespaceHTMLElements) - - def documentClass(self): - self.soup = BeautifulSoup("") - return Element(self.soup, self.soup, None) - - def insertDoctype(self, token): - name = token["name"] - publicId = token["publicId"] - systemId = token["systemId"] - - if publicId: - self.soup.insert(0, Declaration("DOCTYPE %s PUBLIC \"%s\" \"%s\""%(name, publicId, systemId or ""))) - elif systemId: - self.soup.insert(0, Declaration("DOCTYPE %s SYSTEM \"%s\""% - (name, systemId))) - else: - self.soup.insert(0, Declaration("DOCTYPE %s"%name)) - - def elementClass(self, name, namespace): - if namespace is not None: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - return Element(Tag(self.soup, name), self.soup, namespace) - - def commentClass(self, data): - return TextNode(Comment(data), self.soup) - - def fragmentClass(self): - self.soup = BeautifulSoup("") - self.soup.name = "[document_fragment]" - return Element(self.soup, self.soup, None) - - def appendChild(self, node): - self.soup.insert(len(self.soup.contents), node.element) - - def testSerializer(self, element): - return testSerializer(element) - - def getDocument(self): - return self.soup - - def getFragment(self): - return _base.TreeBuilder.getFragment(self).element - -def testSerializer(element): - import re - rv = [] - def serializeElement(element, indent=0): - if isinstance(element, Declaration): - doctype_regexp = r'DOCTYPE\s+(?P[^\s]*)( PUBLIC "(?P.*)" "(?P.*)"| SYSTEM "(?P.*)")?' - m = re.compile(doctype_regexp).match(element.string) - assert m is not None, "DOCTYPE did not match expected format" - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') or "" - else: - systemId = m.group('systemId2') - - if publicId is not None or systemId is not None: - rv.append("""|%s"""% - (' '*indent, name, publicId or "", systemId or "")) - else: - rv.append("|%s"%(' '*indent, name)) - - elif isinstance(element, BeautifulSoup): - if element.name == "[document_fragment]": - rv.append("#document-fragment") - else: - rv.append("#document") - - elif isinstance(element, Comment): - rv.append("|%s"%(' '*indent, element.string)) - elif isinstance(element, unicode): - rv.append("|%s\"%s\"" %(' '*indent, element)) - else: - rv.append("|%s<%s>"%(' '*indent, element.name)) - if element.attrs: - for name, value in sorted(element.attrs): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) - indent += 2 - if hasattr(element, "contents"): - for child in element.contents: - serializeElement(child, indent) - serializeElement(element, 0) - - return "\n".join(rv) diff --git a/libs/html5lib/treewalkers/__init__.py b/libs/html5lib/treewalkers/__init__.py index 3a606a8b..18124e75 100644 --- a/libs/html5lib/treewalkers/__init__.py +++ b/libs/html5lib/treewalkers/__init__.py @@ -8,23 +8,27 @@ implements a 'serialize' method taking a tree as sole argument and returning an iterator generating tokens. """ +from __future__ import absolute_import, division, unicode_literals + +import sys + +from ..utils import default_etree + treeWalkerCache = {} + def getTreeWalker(treeType, implementation=None, **kwargs): """Get a TreeWalker class for various types of tree with built-in support treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" + values are: - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. "dom" - The xml.dom.minidom DOM implementation "pulldom" - The xml.dom.pulldom event stream "etree" - A generic walker for tree implementations exposing an elementtree-like interface (known to work with ElementTree, cElementTree and lxml.etree). "lxml" - Optimized walker for lxml.etree - "beautifulsoup" - Beautiful soup (if installed) "genshi" - a Genshi stream implementation - (Currently applies to the "etree" tree type only). A module @@ -33,20 +37,21 @@ def getTreeWalker(treeType, implementation=None, **kwargs): treeType = treeType.lower() if treeType not in treeWalkerCache: - if treeType in ("dom", "pulldom", "simpletree"): - mod = __import__(treeType, globals()) + if treeType in ("dom", "pulldom"): + name = "%s.%s" % (__name__, treeType) + __import__(name) + mod = sys.modules[name] treeWalkerCache[treeType] = mod.TreeWalker elif treeType == "genshi": - import genshistream + from . import genshistream treeWalkerCache[treeType] = genshistream.TreeWalker - elif treeType == "beautifulsoup": - import soup - treeWalkerCache[treeType] = soup.TreeWalker elif treeType == "lxml": - import lxmletree + from . import lxmletree treeWalkerCache[treeType] = lxmletree.TreeWalker elif treeType == "etree": - import etree + from . import etree + if implementation is None: + implementation = default_etree # XXX: NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeWalker return treeWalkerCache.get(treeType) diff --git a/libs/html5lib/treewalkers/_base.py b/libs/html5lib/treewalkers/_base.py index 5929ba05..34252e50 100644 --- a/libs/html5lib/treewalkers/_base.py +++ b/libs/html5lib/treewalkers/_base.py @@ -1,94 +1,9 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type, string_types + import gettext _ = gettext.gettext -from html5lib.constants import voidElements, spaceCharacters -spaceCharacters = u"".join(spaceCharacters) - -class TreeWalker(object): - def __init__(self, tree): - self.tree = tree - - def __iter__(self): - raise NotImplementedError - - def error(self, msg): - return {"type": "SerializeError", "data": msg} - - def normalizeAttrs(self, attrs): - newattrs = {} - if attrs: - #TODO: treewalkers should always have attrs - for (namespace,name),value in attrs.iteritems(): - namespace = unicode(namespace) if namespace else None - name = unicode(name) - value = unicode(value) - newattrs[(namespace,name)] = value - return newattrs - - def emptyTag(self, namespace, name, attrs, hasChildren=False): - yield {"type": "EmptyTag", "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - if hasChildren: - yield self.error(_("Void element has children")) - - def startTag(self, namespace, name, attrs): - return {"type": "StartTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - - def endTag(self, namespace, name): - return {"type": "EndTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": {}} - - def text(self, data): - data = unicode(data) - middle = data.lstrip(spaceCharacters) - left = data[:len(data)-len(middle)] - if left: - yield {"type": "SpaceCharacters", "data": left} - data = middle - middle = data.rstrip(spaceCharacters) - right = data[len(middle):] - if middle: - yield {"type": "Characters", "data": middle} - if right: - yield {"type": "SpaceCharacters", "data": right} - - def comment(self, data): - return {"type": "Comment", "data": unicode(data)} - - def doctype(self, name, publicId=None, systemId=None, correct=True): - return {"type": "Doctype", - "name": name is not None and unicode(name) or u"", - "publicId": publicId, - "systemId": systemId, - "correct": correct} - - def entity(self, name): - return {"type": "Entity", "name": unicode(name)} - - def unknown(self, nodeType): - return self.error(_("Unknown node type: ") + nodeType) - -class RecursiveTreeWalker(TreeWalker): - def walkChildren(self, node): - raise NodeImplementedError - - def element(self, node, namespace, name, attrs, hasChildren): - if name in voidElements: - for token in self.emptyTag(namespace, name, attrs, hasChildren): - yield token - else: - yield self.startTag(name, attrs) - if hasChildren: - for token in self.walkChildren(node): - yield token - yield self.endTag(name) - from xml.dom import Node DOCUMENT = Node.DOCUMENT_NODE @@ -99,16 +14,127 @@ COMMENT = Node.COMMENT_NODE ENTITY = Node.ENTITY_NODE UNKNOWN = "<#UNKNOWN#>" +from ..constants import voidElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +def to_text(s, blank_if_none=True): + """Wrapper around six.text_type to convert None to empty string""" + if s is None: + if blank_if_none: + return "" + else: + return None + elif isinstance(s, text_type): + return s + else: + return text_type(s) + + +def is_text_or_none(string): + """Wrapper around isinstance(string_types) or is None""" + return string is None or isinstance(string, string_types) + + +class TreeWalker(object): + def __init__(self, tree): + self.tree = tree + + def __iter__(self): + raise NotImplementedError + + def error(self, msg): + return {"type": "SerializeError", "data": msg} + + def emptyTag(self, namespace, name, attrs, hasChildren=False): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + yield {"type": "EmptyTag", "name": to_text(name, False), + "namespace": to_text(namespace), + "data": attrs} + if hasChildren: + yield self.error(_("Void element has children")) + + def startTag(self, namespace, name, attrs): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + return {"type": "StartTag", + "name": text_type(name), + "namespace": to_text(namespace), + "data": dict(((to_text(namespace, False), to_text(name)), + to_text(value, False)) + for (namespace, name), value in attrs.items())} + + def endTag(self, namespace, name): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(namespace) + + return {"type": "EndTag", + "name": to_text(name, False), + "namespace": to_text(namespace), + "data": {}} + + def text(self, data): + assert isinstance(data, string_types), type(data) + + data = to_text(data) + middle = data.lstrip(spaceCharacters) + left = data[:len(data) - len(middle)] + if left: + yield {"type": "SpaceCharacters", "data": left} + data = middle + middle = data.rstrip(spaceCharacters) + right = data[len(middle):] + if middle: + yield {"type": "Characters", "data": middle} + if right: + yield {"type": "SpaceCharacters", "data": right} + + def comment(self, data): + assert isinstance(data, string_types), type(data) + + return {"type": "Comment", "data": text_type(data)} + + def doctype(self, name, publicId=None, systemId=None, correct=True): + assert is_text_or_none(name), type(name) + assert is_text_or_none(publicId), type(publicId) + assert is_text_or_none(systemId), type(systemId) + + return {"type": "Doctype", + "name": to_text(name), + "publicId": to_text(publicId), + "systemId": to_text(systemId), + "correct": to_text(correct)} + + def entity(self, name): + assert isinstance(name, string_types), type(name) + + return {"type": "Entity", "name": text_type(name)} + + def unknown(self, nodeType): + return self.error(_("Unknown node type: ") + nodeType) + + class NonRecursiveTreeWalker(TreeWalker): def getNodeDetails(self, node): raise NotImplementedError - + def getFirstChild(self, node): raise NotImplementedError - + def getNextSibling(self, node): raise NotImplementedError - + def getParentNode(self, node): raise NotImplementedError @@ -118,7 +144,6 @@ class NonRecursiveTreeWalker(TreeWalker): details = self.getNodeDetails(currentNode) type, details = details[0], details[1:] hasChildren = False - endTag = None if type == DOCTYPE: yield self.doctype(*details) @@ -130,12 +155,11 @@ class NonRecursiveTreeWalker(TreeWalker): elif type == ELEMENT: namespace, name, attributes, hasChildren = details if name in voidElements: - for token in self.emptyTag(namespace, name, attributes, + for token in self.emptyTag(namespace, name, attributes, hasChildren): yield token hasChildren = False else: - endTag = name yield self.startTag(namespace, name, attributes) elif type == COMMENT: @@ -149,12 +173,12 @@ class NonRecursiveTreeWalker(TreeWalker): else: yield self.unknown(details[0]) - + if hasChildren: firstChild = self.getFirstChild(currentNode) else: firstChild = None - + if firstChild is not None: currentNode = firstChild else: diff --git a/libs/html5lib/treewalkers/dom.py b/libs/html5lib/treewalkers/dom.py index 383b46cb..a01287a9 100644 --- a/libs/html5lib/treewalkers/dom.py +++ b/libs/html5lib/treewalkers/dom.py @@ -1,10 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom import Node import gettext _ = gettext.gettext -import _base -from html5lib.constants import voidElements +from . import _base + class TreeWalker(_base.NonRecursiveTreeWalker): def getNodeDetails(self, node): @@ -16,10 +18,13 @@ class TreeWalker(_base.NonRecursiveTreeWalker): elif node.nodeType == Node.ELEMENT_NODE: attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value - return (_base.ELEMENT, node.namespaceURI, node.nodeName, + if attr.namespaceURI: + attrs[(attr.namespaceURI, attr.localName)] = attr.value + else: + attrs[(None, attr.name)] = attr.value + return (_base.ELEMENT, node.namespaceURI, node.nodeName, attrs, node.hasChildNodes()) elif node.nodeType == Node.COMMENT_NODE: diff --git a/libs/html5lib/treewalkers/etree.py b/libs/html5lib/treewalkers/etree.py index 13b03194..fd8a9cc9 100644 --- a/libs/html5lib/treewalkers/etree.py +++ b/libs/html5lib/treewalkers/etree.py @@ -1,33 +1,28 @@ +from __future__ import absolute_import, division, unicode_literals + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + OrderedDict = dict import gettext _ = gettext.gettext -try: - from types import ModuleType -except: - from new import module as ModuleType -import copy import re -import _base -from html5lib.constants import voidElements +from six import text_type + +from . import _base +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag class TreeWalker(_base.NonRecursiveTreeWalker): """Given the particular ElementTree representation, this implementation, @@ -35,16 +30,16 @@ def getETreeBuilder(ElementTreeImplementation): content: 1. The current element - + 2. The index of the element relative to its parent - + 3. A stack of ancestor elements - + 4. A flag "text", "tail" or None to indicate if the current node is a text node; either the text or tail of the current element (1) """ def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Element + if isinstance(node, tuple): # It might be the root Element elt, key, parents, flag = node if flag in ("text", "tail"): return _base.TEXT, getattr(elt, flag) @@ -54,41 +49,41 @@ def getETreeBuilder(ElementTreeImplementation): if not(hasattr(node, "tag")): node = node.getroot() - if node.tag in ("", ""): + if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"): return (_base.DOCUMENT,) elif node.tag == "": - return (_base.DOCTYPE, node.text, + return (_base.DOCTYPE, node.text, node.get("publicId"), node.get("systemId")) - elif node.tag == ElementTree.Comment: + elif node.tag == ElementTreeCommentType: return _base.COMMENT, node.text else: - assert type(node.tag) in (str, unicode), type(node.tag) - #This is assumed to be an ordinary element + assert type(node.tag) == text_type, type(node.tag) + # This is assumed to be an ordinary element match = tag_regexp.match(node.tag) if match: namespace, tag = match.groups() else: namespace = None tag = node.tag - attrs = {} - for name, value in node.attrib.items(): + attrs = OrderedDict() + for name, value in list(node.attrib.items()): match = tag_regexp.match(name) if match: - attrs[(match.group(1),match.group(2))] = value + attrs[(match.group(1), match.group(2))] = value else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, tag, + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, tag, attrs, len(node) or node.text) - + def getFirstChild(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: element, key, parents, flag = node, None, [], None - + if flag in ("text", "tail"): return None else: @@ -99,13 +94,13 @@ def getETreeBuilder(ElementTreeImplementation): return element[0], 0, parents, None else: return None - + def getNextSibling(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if len(element): parents.append(element) @@ -116,16 +111,16 @@ def getETreeBuilder(ElementTreeImplementation): if element.tail and flag != "tail": return element, key, parents, "tail" elif key < len(parents[-1]) - 1: - return parents[-1][key+1], key+1, parents, None + return parents[-1][key + 1], key + 1, parents, None else: return None - + def getParentNode(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if not parents: return element @@ -139,3 +134,5 @@ def getETreeBuilder(ElementTreeImplementation): return parent, list(parents[-1]).index(parent), parents, None return locals() + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treewalkers/genshistream.py b/libs/html5lib/treewalkers/genshistream.py index ef71a83e..f559c45d 100644 --- a/libs/html5lib/treewalkers/genshistream.py +++ b/libs/html5lib/treewalkers/genshistream.py @@ -1,50 +1,49 @@ +from __future__ import absolute_import, division, unicode_literals + +from genshi.core import QName from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT -from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -from genshi.output import NamespaceFlattener +from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -import _base +from . import _base + +from ..constants import voidElements, namespaces -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): - depth = 0 - ignore_until = None + # Buffer the events so we can pass in the following one previous = None for event in self.tree: if previous is not None: - if previous[0] == START: - depth += 1 - if ignore_until <= depth: - ignore_until = None - if ignore_until is None: - for token in self.tokens(previous, event): - yield token - if token["type"] == "EmptyTag": - ignore_until = depth - if previous[0] == END: - depth -= 1 - previous = event - if previous is not None: - if ignore_until is None or ignore_until <= depth: - for token in self.tokens(previous, None): + for token in self.tokens(previous, event): yield token - elif ignore_until is not None: - raise ValueError("Illformed DOM event stream: void element without END_ELEMENT") + previous = event + + # Don't forget the final event! + if previous is not None: + for token in self.tokens(previous, None): + yield token def tokens(self, event, next): kind, data, pos = event if kind == START: - tag, attrib = data + tag, attribs = data name = tag.localname namespace = tag.namespace - if tag in voidElements: - for token in self.emptyTag(namespace, name, list(attrib), - not next or next[0] != END + converted_attribs = {} + for k, v in attribs: + if isinstance(k, QName): + converted_attribs[(k.namespace, k.localname)] = v + else: + converted_attribs[(None, k)] = v + + if namespace == namespaces["html"] and name in voidElements: + for token in self.emptyTag(namespace, name, converted_attribs, + not next or next[0] != END or next[1] != tag): yield token else: - yield self.startTag(namespace, name, list(attrib)) + yield self.startTag(namespace, name, converted_attribs) elif kind == END: name = data.localname @@ -62,8 +61,8 @@ class TreeWalker(_base.TreeWalker): elif kind == DOCTYPE: yield self.doctype(*data) - elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, \ - START_CDATA, END_CDATA, PI): + elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, + START_CDATA, END_CDATA, PI): pass else: diff --git a/libs/html5lib/treewalkers/lxmletree.py b/libs/html5lib/treewalkers/lxmletree.py index 46f4908c..375cc2e8 100644 --- a/libs/html5lib/treewalkers/lxmletree.py +++ b/libs/html5lib/treewalkers/lxmletree.py @@ -1,186 +1,208 @@ -from lxml import etree -from html5lib.treebuilders.etree import tag_regexp - -from gettext import gettext -_ = gettext - -import _base - -from html5lib.constants import voidElements -from html5lib import ihatexml - -class Root(object): - def __init__(self, et): - self.elementtree = et - self.children = [] - if et.docinfo.internalDTD: - self.children.append(Doctype(self, et.docinfo.root_name, - et.docinfo.public_id, - et.docinfo.system_url)) - root = et.getroot() - node = root - - while node.getprevious() is not None: - node = node.getprevious() - while node is not None: - self.children.append(node) - node = node.getnext() - - self.text = None - self.tail = None - - def __getitem__(self, key): - return self.children[key] - - def getnext(self): - return None - - def __len__(self): - return 1 - -class Doctype(object): - def __init__(self, root_node, name, public_id, system_id): - self.root_node = root_node - self.name = name - self.public_id = public_id - self.system_id = system_id - - self.text = None - self.tail = None - - def getnext(self): - return self.root_node.children[1] - -class FragmentRoot(Root): - def __init__(self, children): - self.children = [FragmentWrapper(self, child) for child in children] - self.text = self.tail = None - - def getnext(self): - return None - -class FragmentWrapper(object): - def __init__(self, fragment_root, obj): - self.root_node = fragment_root - self.obj = obj - if hasattr(self.obj, 'text'): - self.text = self.obj.text - else: - self.text = None - if hasattr(self.obj, 'tail'): - self.tail = self.obj.tail - else: - self.tail = None - self.isstring = isinstance(obj, basestring) - - def __getattr__(self, name): - return getattr(self.obj, name) - - def getnext(self): - siblings = self.root_node.children - idx = siblings.index(self) - if idx < len(siblings) - 1: - return siblings[idx + 1] - else: - return None - - def __getitem__(self, key): - return self.obj[key] - - def __nonzero__(self): - return bool(self.obj) - - def getparent(self): - return None - - def __str__(self): - return str(self.obj) - - def __unicode__(self): - return unicode(self.obj) - - def __len__(self): - return len(self.obj) - - -class TreeWalker(_base.NonRecursiveTreeWalker): - def __init__(self, tree): - if hasattr(tree, "getroot"): - tree = Root(tree) - elif isinstance(tree, list): - tree = FragmentRoot(tree) - _base.NonRecursiveTreeWalker.__init__(self, tree) - self.filter = ihatexml.InfosetFilter() - def getNodeDetails(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - return _base.TEXT, getattr(node, key) - - elif isinstance(node, Root): - return (_base.DOCUMENT,) - - elif isinstance(node, Doctype): - return _base.DOCTYPE, node.name, node.public_id, node.system_id - - elif isinstance(node, FragmentWrapper) and node.isstring: - return _base.TEXT, node - - elif node.tag == etree.Comment: - return _base.COMMENT, node.text - - elif node.tag == etree.Entity: - return _base.ENTITY, node.text[1:-1] # strip &; - - else: - #This is assumed to be an ordinary element - match = tag_regexp.match(node.tag) - if match: - namespace, tag = match.groups() - else: - namespace = None - tag = node.tag - attrs = {} - for name, value in node.attrib.items(): - match = tag_regexp.match(name) - if match: - attrs[(match.group(1),match.group(2))] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), - attrs, len(node) > 0 or node.text) - - def getFirstChild(self, node): - assert not isinstance(node, tuple), _("Text nodes have no children") - - assert len(node) or node.text, "Node has no children" - if node.text: - return (node, "text") - else: - return node[0] - - def getNextSibling(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - # XXX: we cannot use a "bool(node) and node[0] or None" construct here - # because node[0] might evaluate to False if it has no child element - if len(node): - return node[0] - else: - return None - else: # tail - return node.getnext() - - return node.tail and (node, "tail") or node.getnext() - - def getParentNode(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - return node - # else: fallback to "normal" processing - - return node.getparent() +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from lxml import etree +from ..treebuilders.etree import tag_regexp + +from gettext import gettext +_ = gettext + +from . import _base + +from .. import ihatexml + + +def ensure_str(s): + if s is None: + return None + elif isinstance(s, text_type): + return s + else: + return s.decode("utf-8", "strict") + + +class Root(object): + def __init__(self, et): + self.elementtree = et + self.children = [] + if et.docinfo.internalDTD: + self.children.append(Doctype(self, + ensure_str(et.docinfo.root_name), + ensure_str(et.docinfo.public_id), + ensure_str(et.docinfo.system_url))) + root = et.getroot() + node = root + + while node.getprevious() is not None: + node = node.getprevious() + while node is not None: + self.children.append(node) + node = node.getnext() + + self.text = None + self.tail = None + + def __getitem__(self, key): + return self.children[key] + + def getnext(self): + return None + + def __len__(self): + return 1 + + +class Doctype(object): + def __init__(self, root_node, name, public_id, system_id): + self.root_node = root_node + self.name = name + self.public_id = public_id + self.system_id = system_id + + self.text = None + self.tail = None + + def getnext(self): + return self.root_node.children[1] + + +class FragmentRoot(Root): + def __init__(self, children): + self.children = [FragmentWrapper(self, child) for child in children] + self.text = self.tail = None + + def getnext(self): + return None + + +class FragmentWrapper(object): + def __init__(self, fragment_root, obj): + self.root_node = fragment_root + self.obj = obj + if hasattr(self.obj, 'text'): + self.text = ensure_str(self.obj.text) + else: + self.text = None + if hasattr(self.obj, 'tail'): + self.tail = ensure_str(self.obj.tail) + else: + self.tail = None + self.isstring = isinstance(obj, str) or isinstance(obj, bytes) + # Support for bytes here is Py2 + if self.isstring: + self.obj = ensure_str(self.obj) + + def __getattr__(self, name): + return getattr(self.obj, name) + + def getnext(self): + siblings = self.root_node.children + idx = siblings.index(self) + if idx < len(siblings) - 1: + return siblings[idx + 1] + else: + return None + + def __getitem__(self, key): + return self.obj[key] + + def __bool__(self): + return bool(self.obj) + + def getparent(self): + return None + + def __str__(self): + return str(self.obj) + + def __unicode__(self): + return str(self.obj) + + def __len__(self): + return len(self.obj) + + +class TreeWalker(_base.NonRecursiveTreeWalker): + def __init__(self, tree): + if hasattr(tree, "getroot"): + tree = Root(tree) + elif isinstance(tree, list): + tree = FragmentRoot(tree) + _base.NonRecursiveTreeWalker.__init__(self, tree) + self.filter = ihatexml.InfosetFilter() + + def getNodeDetails(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + return _base.TEXT, ensure_str(getattr(node, key)) + + elif isinstance(node, Root): + return (_base.DOCUMENT,) + + elif isinstance(node, Doctype): + return _base.DOCTYPE, node.name, node.public_id, node.system_id + + elif isinstance(node, FragmentWrapper) and node.isstring: + return _base.TEXT, node.obj + + elif node.tag == etree.Comment: + return _base.COMMENT, ensure_str(node.text) + + elif node.tag == etree.Entity: + return _base.ENTITY, ensure_str(node.text)[1:-1] # strip &; + + else: + # This is assumed to be an ordinary element + match = tag_regexp.match(ensure_str(node.tag)) + if match: + namespace, tag = match.groups() + else: + namespace = None + tag = ensure_str(node.tag) + attrs = {} + for name, value in list(node.attrib.items()): + name = ensure_str(name) + value = ensure_str(value) + match = tag_regexp.match(name) + if match: + attrs[(match.group(1), match.group(2))] = value + else: + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), + attrs, len(node) > 0 or node.text) + + def getFirstChild(self, node): + assert not isinstance(node, tuple), _("Text nodes have no children") + + assert len(node) or node.text, "Node has no children" + if node.text: + return (node, "text") + else: + return node[0] + + def getNextSibling(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + # XXX: we cannot use a "bool(node) and node[0] or None" construct here + # because node[0] might evaluate to False if it has no child element + if len(node): + return node[0] + else: + return None + else: # tail + return node.getnext() + + return (node, "tail") if node.tail else node.getnext() + + def getParentNode(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + return node + # else: fallback to "normal" processing + + return node.getparent() diff --git a/libs/html5lib/treewalkers/pulldom.py b/libs/html5lib/treewalkers/pulldom.py index 1f8b95b8..0b0f515f 100644 --- a/libs/html5lib/treewalkers/pulldom.py +++ b/libs/html5lib/treewalkers/pulldom.py @@ -1,9 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom.pulldom import START_ELEMENT, END_ELEMENT, \ COMMENT, IGNORABLE_WHITESPACE, CHARACTERS -import _base +from . import _base + +from ..constants import voidElements -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): @@ -11,7 +14,7 @@ class TreeWalker(_base.TreeWalker): previous = None for event in self.tree: if previous is not None and \ - (ignore_until is None or previous[1] is ignore_until): + (ignore_until is None or previous[1] is ignore_until): if previous[1] is ignore_until: ignore_until = None for token in self.tokens(previous, event): @@ -31,9 +34,9 @@ class TreeWalker(_base.TreeWalker): name = node.nodeName namespace = node.namespaceURI attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value + attrs[(attr.namespaceURI, attr.localName)] = attr.value if name in voidElements: for token in self.emptyTag(namespace, name, diff --git a/libs/html5lib/treewalkers/simpletree.py b/libs/html5lib/treewalkers/simpletree.py deleted file mode 100644 index 9e6bd4c5..00000000 --- a/libs/html5lib/treewalkers/simpletree.py +++ /dev/null @@ -1,78 +0,0 @@ -import gettext -_ = gettext.gettext - -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - """Given that simpletree has no performant way of getting a node's - next sibling, this implementation returns "nodes" as tuples with the - following content: - - 1. The parent Node (Element, Document or DocumentFragment) - - 2. The child index of the current node in its parent's children list - - 3. A list used as a stack of all ancestors. It is a pair tuple whose - first item is a parent Node and second item is a child index. - """ - - def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - node = parent.childNodes[idx] - - # testing node.type allows us not to import treebuilders.simpletree - if node.type in (1, 2): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif node.type == 3: # DocumentType - return _base.DOCTYPE, node.name, node.publicId, node.systemId - - elif node.type == 4: # TextNode - return _base.TEXT, node.value - - elif node.type == 5: # Element - attrs = {} - for name, value in node.attributes.items(): - if isinstance(name, tuple): - attrs[(name[2],name[1])] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, node.namespace, node.name, - attrs, node.hasContent()) - - elif node.type == 6: # CommentNode - return _base.COMMENT, node.data - - else: - return _node.UNKNOWN, node.type - - def getFirstChild(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - parents.append((parent, idx)) - node = parent.childNodes[idx] - else: - parents = [] - - assert node.hasContent(), "Node has no children" - return (node, 0, parents) - - def getNextSibling(self, node): - assert isinstance(node, tuple), "Node is not a tuple: " + str(node) - parent, idx, parents = node - idx += 1 - if len(parent.childNodes) > idx: - return (parent, idx, parents) - else: - return None - - def getParentNode(self, node): - assert isinstance(node, tuple) - parent, idx, parents = node - if parents: - parent, idx = parents.pop() - return parent, idx, parents - else: - # HACK: We could return ``parent`` but None will stop the algorithm the same way - return None diff --git a/libs/html5lib/treewalkers/soup.py b/libs/html5lib/treewalkers/soup.py deleted file mode 100644 index fca65ecb..00000000 --- a/libs/html5lib/treewalkers/soup.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -import gettext -_ = gettext.gettext - -from BeautifulSoup import BeautifulSoup, Declaration, Comment, Tag -from html5lib.constants import namespaces -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - doctype_regexp = re.compile( - r'DOCTYPE\s+(?P[^\s]*)(\s*PUBLIC\s*"(?P.*)"\s*"(?P.*)"|\s*SYSTEM\s*"(?P.*)")?') - def getNodeDetails(self, node): - if isinstance(node, BeautifulSoup): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif isinstance(node, Declaration): # DocumentType - string = unicode(node.string) - #Slice needed to remove markup added during unicode conversion, - #but only in some versions of BeautifulSoup/Python - if string.startswith(''): - string = string[2:-1] - m = self.doctype_regexp.match(string) - #This regexp approach seems wrong and fragile - #but beautiful soup stores the doctype as a single thing and we want the seperate bits - #It should work as long as the tree is created by html5lib itself but may be wrong if it's - #been modified at all - #We could just feed to it a html5lib tokenizer, I guess... - assert m is not None, "DOCTYPE did not match expected format" - - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') - else: - systemId = m.group('systemId2') - return _base.DOCTYPE, name, publicId or "", systemId or "" - - elif isinstance(node, Comment): - string = unicode(node.string) - if string.startswith(''): - string = string[4:-3] - return _base.COMMENT, string - - elif isinstance(node, unicode): # TextNode - return _base.TEXT, node - - elif isinstance(node, Tag): # Element - return (_base.ELEMENT, namespaces["html"], node.name, - dict(node.attrs).items(), node.contents) - else: - return _base.UNKNOWN, node.__class__.__name__ - - def getFirstChild(self, node): - return node.contents[0] - - def getNextSibling(self, node): - return node.nextSibling - - def getParentNode(self, node): - return node.parent diff --git a/libs/html5lib/trie/__init__.py b/libs/html5lib/trie/__init__.py new file mode 100644 index 00000000..a8cca8a9 --- /dev/null +++ b/libs/html5lib/trie/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + +from .py import Trie as PyTrie + +Trie = PyTrie + +try: + from .datrie import Trie as DATrie +except ImportError: + pass +else: + Trie = DATrie diff --git a/libs/html5lib/trie/_base.py b/libs/html5lib/trie/_base.py new file mode 100644 index 00000000..724486b1 --- /dev/null +++ b/libs/html5lib/trie/_base.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division, unicode_literals + +from collections import Mapping + + +class Trie(Mapping): + """Abstract base class for tries""" + + def keys(self, prefix=None): + keys = super().keys() + + if prefix is None: + return set(keys) + + # Python 2.6: no set comprehensions + return set([x for x in keys if x.startswith(prefix)]) + + def has_keys_with_prefix(self, prefix): + for key in self.keys(): + if key.startswith(prefix): + return True + + return False + + def longest_prefix(self, prefix): + if prefix in self: + return prefix + + for i in range(1, len(prefix) + 1): + if prefix[:-i] in self: + return prefix[:-i] + + raise KeyError(prefix) + + def longest_prefix_item(self, prefix): + lprefix = self.longest_prefix(prefix) + return (lprefix, self[lprefix]) diff --git a/libs/html5lib/trie/datrie.py b/libs/html5lib/trie/datrie.py new file mode 100644 index 00000000..51f3d046 --- /dev/null +++ b/libs/html5lib/trie/datrie.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from datrie import Trie as DATrie +from six import text_type + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + chars = set() + for key in data.keys(): + if not isinstance(key, text_type): + raise TypeError("All keys must be strings") + for char in key: + chars.add(char) + + self._data = DATrie("".join(chars)) + for key, value in data.items(): + self._data[key] = value + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + raise NotImplementedError() + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + return self._data.keys(prefix) + + def has_keys_with_prefix(self, prefix): + return self._data.has_keys_with_prefix(prefix) + + def longest_prefix(self, prefix): + return self._data.longest_prefix(prefix) + + def longest_prefix_item(self, prefix): + return self._data.longest_prefix_item(prefix) diff --git a/libs/html5lib/trie/py.py b/libs/html5lib/trie/py.py new file mode 100644 index 00000000..c2ba3da7 --- /dev/null +++ b/libs/html5lib/trie/py.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from bisect import bisect_left + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + if not all(isinstance(x, text_type) for x in data.keys()): + raise TypeError("All keys must be strings") + + self._data = data + self._keys = sorted(data.keys()) + self._cachestr = "" + self._cachepoints = (0, len(data)) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + if prefix is None or prefix == "" or not self._keys: + return set(self._keys) + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + start = i = bisect_left(self._keys, prefix, lo, hi) + else: + start = i = bisect_left(self._keys, prefix) + + keys = set() + if start == len(self._keys): + return keys + + while self._keys[i].startswith(prefix): + keys.add(self._keys[i]) + i += 1 + + self._cachestr = prefix + self._cachepoints = (start, i) + + return keys + + def has_keys_with_prefix(self, prefix): + if prefix in self._data: + return True + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + i = bisect_left(self._keys, prefix, lo, hi) + else: + i = bisect_left(self._keys, prefix) + + if i == len(self._keys): + return False + + return self._keys[i].startswith(prefix) diff --git a/libs/html5lib/utils.py b/libs/html5lib/utils.py index d53f6788..2f41f4df 100644 --- a/libs/html5lib/utils.py +++ b/libs/html5lib/utils.py @@ -1,9 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals + +from types import ModuleType + try: - frozenset -except NameError: - #Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset + import xml.etree.cElementTree as default_etree +except ImportError: + import xml.etree.ElementTree as default_etree + + +__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", + "surrogatePairToCodepoint", "moduleFactoryFactory"] + class MethodDispatcher(dict): """Dict with 2 special properties: @@ -23,7 +30,7 @@ class MethodDispatcher(dict): # twice as fast. Please do careful performance testing before changing # anything here. _dictEntries = [] - for name,value in items: + for name, value in items: if type(name) in (list, tuple, frozenset, set): for item in name: _dictEntries.append((item, value)) @@ -35,141 +42,41 @@ class MethodDispatcher(dict): def __getitem__(self, key): return dict.get(self, key, self.default) -#Pure python implementation of deque taken from the ASPN Python Cookbook -#Original code by Raymond Hettinger -class deque(object): +# Some utility functions to dal with weirdness around UCS2 vs UCS4 +# python builds - def __init__(self, iterable=(), maxsize=-1): - if not hasattr(self, 'data'): - self.left = self.right = 0 - self.data = {} - self.maxsize = maxsize - self.extend(iterable) - - def append(self, x): - self.data[self.right] = x - self.right += 1 - if self.maxsize != -1 and len(self) > self.maxsize: - self.popleft() - - def appendleft(self, x): - self.left -= 1 - self.data[self.left] = x - if self.maxsize != -1 and len(self) > self.maxsize: - self.pop() - - def pop(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - self.right -= 1 - elem = self.data[self.right] - del self.data[self.right] - return elem - - def popleft(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - elem = self.data[self.left] - del self.data[self.left] - self.left += 1 - return elem - - def clear(self): - self.data.clear() - self.left = self.right = 0 - - def extend(self, iterable): - for elem in iterable: - self.append(elem) - - def extendleft(self, iterable): - for elem in iterable: - self.appendleft(elem) - - def rotate(self, n=1): - if self: - n %= len(self) - for i in xrange(n): - self.appendleft(self.pop()) - - def __getitem__(self, i): - if i < 0: - i += len(self) - try: - return self.data[i + self.left] - except KeyError: - raise IndexError - - def __setitem__(self, i, value): - if i < 0: - i += len(self) - try: - self.data[i + self.left] = value - except KeyError: - raise IndexError - - def __delitem__(self, i): - size = len(self) - if not (-size <= i < size): - raise IndexError - data = self.data - if i < 0: - i += size - for j in xrange(self.left+i, self.right-1): - data[j] = data[j+1] - self.pop() - - def __len__(self): - return self.right - self.left - - def __cmp__(self, other): - if type(self) != type(other): - return cmp(type(self), type(other)) - return cmp(list(self), list(other)) - - def __repr__(self, _track=[]): - if id(self) in _track: - return '...' - _track.append(id(self)) - r = 'deque(%r)' % (list(self),) - _track.remove(id(self)) - return r - - def __getstate__(self): - return (tuple(self),) - - def __setstate__(self, s): - self.__init__(s[0]) - - def __hash__(self): - raise TypeError - - def __copy__(self): - return self.__class__(self) - - def __deepcopy__(self, memo={}): - from copy import deepcopy - result = self.__class__() - memo[id(self)] = result - result.__init__(deepcopy(tuple(self), memo)) - return result - -#Some utility functions to dal with weirdness around UCS2 vs UCS4 -#python builds - -def encodingType(): - if len() == 2: - return "UCS2" - else: - return "UCS4" - -def isSurrogatePair(data): +def isSurrogatePair(data): return (len(data) == 2 and ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF) + def surrogatePairToCodepoint(data): - char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + + char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + (ord(data[1]) - 0xDC00)) return char_val + +# Module Factory Factory (no, this isn't Java, I know) +# Here to stop this being duplicated all over the place. + + +def moduleFactoryFactory(factory): + moduleCache = {} + + def moduleFactory(baseModule, *args, **kwargs): + if isinstance(ModuleType.__name__, type("")): + name = "_%s_factory" % baseModule.__name__ + else: + name = b"_%s_factory" % baseModule.__name__ + + if name in moduleCache: + return moduleCache[name] + else: + mod = ModuleType(name) + objs = factory(baseModule, *args, **kwargs) + mod.__dict__.update(objs) + moduleCache[name] = mod + return mod + + return moduleFactory diff --git a/libs/httplib2/__init__.py b/libs/httplib2/__init__.py index 01151f7f..9780d4e5 100644 --- a/libs/httplib2/__init__.py +++ b/libs/httplib2/__init__.py @@ -3,7 +3,7 @@ from __future__ import generators httplib2 A caching http interface that supports ETags and gzip -to conserve bandwidth. +to conserve bandwidth. Requires Python 2.3 or later @@ -15,17 +15,17 @@ Changelog: __author__ = "Joe Gregorio (joe@bitworking.org)" __copyright__ = "Copyright 2006, Joe Gregorio" __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", - "James Antill", - "Xavier Verges Farrero", - "Jonathan Feinberg", - "Blair Zajac", - "Sam Ruby", - "Louis Nyffenegger"] + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] __license__ = "MIT" -__version__ = "$Rev$" +__version__ = "0.8" -import re -import sys +import re +import sys import email import email.Utils import email.Message @@ -35,6 +35,7 @@ import gzip import zlib import httplib import urlparse +import urllib import base64 import os import copy @@ -42,10 +43,10 @@ import calendar import time import random import errno -# remove depracated warning in python2.6 try: from hashlib import sha1 as _sha, md5 as _md5 except ImportError: + # prior to Python 2.5, these were separate modules import sha import md5 _sha = sha.new @@ -54,21 +55,38 @@ import hmac from gettext import gettext as _ import socket -# Try using local version, followed by system, and none if neither are found try: - import lib.socks as socks + from httplib2 import socks except ImportError: try: - import socks as socks - except ImportError: + import socks + except (ImportError, AttributeError): socks = None # Build the appropriate socket wrapper for ssl try: import ssl # python 2.6 - _ssl_wrap_socket = ssl.wrap_socket -except ImportError: - def _ssl_wrap_socket(sock, key_file, cert_file): + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") ssl_sock = socket.ssl(sock, key_file, cert_file) return httplib.FakeSocket(sock, ssl_sock) @@ -84,15 +102,19 @@ def has_timeout(timeout): # python 2.6 return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) return (timeout is not None) -__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', - 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', - 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', - 'debuglevel'] +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] # The httplib debug level, set to a non-zero value to get debug output debuglevel = 0 +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 # Python 2.3 support if sys.version_info < (2,4): @@ -113,8 +135,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'): # All exceptions raised here derive from HttpLib2Error class HttpLib2Error(Exception): pass -# Some exceptions can be caught and optionally -# be turned back into responses. +# Some exceptions can be caught and optionally +# be turned back into responses. class HttpLib2ErrorWithResponse(HttpLib2Error): def __init__(self, desc, response, content): self.response = response @@ -127,8 +149,18 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class MalformedHeader(HttpLib2Error): pass class RelativeURIError(HttpLib2Error): pass class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert # Open Items: # ----------- @@ -152,6 +184,16 @@ class ServerNotFoundError(HttpLib2Error): pass # requesting that URI again. DEFAULT_MAX_REDIRECTS = 5 +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + # Which headers are hop-by-hop headers by default HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] @@ -176,7 +218,7 @@ def urlnorm(uri): raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) authority = authority.lower() scheme = scheme.lower() - if not path: + if not path: path = "/" # Could do syntax based normalization of the URI before # computing the digest. See Section 6.2.2 of Std 66. @@ -228,7 +270,7 @@ def _parse_cache_control(headers): parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] retval = dict(parts_with_args + parts_wo_args) - return retval + return retval # Whether to use a strict mode to parse WWW-Authenticate headers # Might lead to bad results in case of ill-formed header value, @@ -249,25 +291,30 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): per auth_scheme.""" retval = {} if headers.has_key(headername): - authenticate = headers[headername].strip() - www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED - while authenticate: - # Break off the scheme at the beginning of the line - if headername == 'authentication-info': - (auth_scheme, the_rest) = ('digest', authenticate) - else: - (auth_scheme, the_rest) = authenticate.split(" ", 1) - # Now loop over all the key value pairs that come after the scheme, - # being careful not to roll into the next scheme - match = www_auth.search(the_rest) - auth_params = {} - while match: - if match and len(match.groups()) == 3: - (key, value, the_rest) = match.groups() - auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + try: + + authenticate = headers[headername].strip() + www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED + while authenticate: + # Break off the scheme at the beginning of the line + if headername == 'authentication-info': + (auth_scheme, the_rest) = ('digest', authenticate) + else: + (auth_scheme, the_rest) = authenticate.split(" ", 1) + # Now loop over all the key value pairs that come after the scheme, + # being careful not to roll into the next scheme match = www_auth.search(the_rest) - retval[auth_scheme.lower()] = auth_params - authenticate = the_rest.strip() + auth_params = {} + while match: + if match and len(match.groups()) == 3: + (key, value, the_rest) = match.groups() + auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + match = www_auth.search(the_rest) + retval[auth_scheme.lower()] = auth_params + authenticate = the_rest.strip() + + except ValueError: + raise MalformedHeader("WWW-Authenticate") return retval @@ -279,17 +326,17 @@ def _entry_disposition(response_headers, request_headers): 1. Cache-Control: max-stale 2. Age: headers are not used in the calculations. - Not that this algorithm is simpler than you might think + Not that this algorithm is simpler than you might think because we are operating as a private (non-shared) cache. This lets us ignore 's-maxage'. We can also ignore 'proxy-invalidate' since we aren't a proxy. - We will never return a stale document as - fresh as a design decision, and thus the non-implementation - of 'max-stale'. This also lets us safely ignore 'must-revalidate' + We will never return a stale document as + fresh as a design decision, and thus the non-implementation + of 'max-stale'. This also lets us safely ignore 'must-revalidate' since we operate as if every server has sent 'must-revalidate'. Since we are private we get to ignore both 'public' and 'private' parameters. We also ignore 'no-transform' since - we don't do any transformations. + we don't do any transformations. The 'no-store' parameter is handled at a higher level. So the only Cache-Control parameters we look at are: @@ -298,7 +345,7 @@ def _entry_disposition(response_headers, request_headers): max-age min-fresh """ - + retval = "STALE" cc = _parse_cache_control(request_headers) cc_response = _parse_cache_control(response_headers) @@ -340,10 +387,10 @@ def _entry_disposition(response_headers, request_headers): min_fresh = int(cc['min-fresh']) except ValueError: min_fresh = 0 - current_age += min_fresh + current_age += min_fresh if freshness_lifetime > current_age: retval = "FRESH" - return retval + return retval def _decompressContent(response, new_content): content = new_content @@ -391,7 +438,7 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey): if status == 304: status = 200 - status_header = 'status: %d\r\n' % response_headers.status + status_header = 'status: %d\r\n' % status header_str = info.as_string() @@ -408,10 +455,10 @@ def _wsse_username_token(cnonce, iso_now, password): return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() -# For credentials we need two things, first +# For credentials we need two things, first # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # Then we also need a list of URIs that have already demanded authentication -# That list is tricky since sub-URIs can take the same auth, or the +# That list is tricky since sub-URIs can take the same auth, or the # auth scheme may change as you descend the tree. # So we also need each Auth instance to be able to tell us # how close to the 'top' it is. @@ -435,7 +482,7 @@ class Authentication(object): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate - Authorization header. Over-rise this in sub-classes.""" + Authorization header. Over-ride this in sub-classes.""" pass def response(self, response, content): @@ -443,7 +490,7 @@ class Authentication(object): or such returned from the last authorized response. Over-rise this in sub-classes if necessary. - Return TRUE is the request is to be retried, for + Return TRUE is the request is to be retried, for example Digest may return stale=true. """ return False @@ -461,7 +508,7 @@ class BasicAuthentication(Authentication): class DigestAuthentication(Authentication): - """Only do qop='auth' and MD5, since that + """Only do qop='auth' and MD5, since that is all Apache currently implements""" def __init__(self, credentials, host, request_uri, headers, response, content, http): Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) @@ -474,7 +521,7 @@ class DigestAuthentication(Authentication): self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() if self.challenge['algorithm'] != 'MD5': raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) - self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) + self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.challenge['nc'] = 1 def request(self, method, request_uri, headers, content, cnonce = None): @@ -482,23 +529,24 @@ class DigestAuthentication(Authentication): H = lambda x: _md5(x).hexdigest() KD = lambda s, d: H("%s:%s" % (s, d)) A2 = "".join([method, ":", request_uri]) - self.challenge['cnonce'] = cnonce or _cnonce() - request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], - '%08x' % self.challenge['nc'], - self.challenge['cnonce'], - self.challenge['qop'], H(A2) - )) - headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( - self.credentials[0], + self.challenge['cnonce'] = cnonce or _cnonce() + request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % ( + self.challenge['nonce'], + '%08x' % self.challenge['nc'], + self.challenge['cnonce'], + self.challenge['qop'], H(A2))) + headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['nonce'], - request_uri, + request_uri, self.challenge['algorithm'], request_digest, self.challenge['qop'], self.challenge['nc'], - self.challenge['cnonce'], - ) + self.challenge['cnonce']) + if self.challenge.get('opaque'): + headers['authorization'] += ', opaque="%s"' % self.challenge['opaque'] self.challenge['nc'] += 1 def response(self, response, content): @@ -506,14 +554,14 @@ class DigestAuthentication(Authentication): challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return True else: updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) if updated_challenge.has_key('nextnonce'): self.challenge['nonce'] = updated_challenge['nextnonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return False @@ -547,9 +595,8 @@ class HmacDigestAuthentication(Authentication): else: self.pwhashmod = _sha self.key = "".join([self.credentials[0], ":", - self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), - ":", self.challenge['realm'] - ]) + self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), + ":", self.challenge['realm']]) self.key = self.pwhashmod.new(self.key).hexdigest().lower() def request(self, method, request_uri, headers, content): @@ -561,16 +608,15 @@ class HmacDigestAuthentication(Authentication): cnonce = _cnonce() request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() - headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( - self.credentials[0], + headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['snonce'], cnonce, - request_uri, + request_uri, created, request_digest, - keylist, - ) + keylist) def response(self, response, content): challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {}) @@ -583,7 +629,7 @@ class WsseAuthentication(Authentication): """This is thinly tested and should not be relied upon. At this time there isn't any third party server to test against. Blogger and TypePad implemented this algorithm at one point - but Blogger has since switched to Basic over HTTPS and + but Blogger has since switched to Basic over HTTPS and TypePad has implemented it wrong, by never issuing a 401 challenge but instead requiring your client to telepathically know that their endpoint is expecting WSSE profile="UsernameToken".""" @@ -593,7 +639,7 @@ class WsseAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['Authorization'] = 'WSSE profile="UsernameToken"' + headers['authorization'] = 'WSSE profile="UsernameToken"' iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) cnonce = _cnonce() password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1]) @@ -629,7 +675,7 @@ class GoogleLoginAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth AUTH_SCHEME_CLASSES = { @@ -644,13 +690,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] class FileCache(object): """Uses a local directory as a store for cached files. - Not really safe to use if multiple threads or processes are going to + Not really safe to use if multiple threads or processes are going to be running on the same cache. """ def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior self.cache = cache self.safe = safe - if not os.path.exists(cache): + if not os.path.exists(cache): os.makedirs(self.cache) def get(self, key): @@ -660,7 +706,7 @@ class FileCache(object): f = file(cacheFullPath, "rb") retval = f.read() f.close() - except IOError, e: + except IOError: pass return retval @@ -688,34 +734,127 @@ class Credentials(object): def iter(self, domain): for (cdomain, name, password) in self.credentials: if cdomain == "" or domain == cdomain: - yield (name, password) + yield (name, password) class KeyCerts(Credentials): """Identical to Credentials except that name/password are mapped to key/cert.""" pass +class AllHosts(object): + pass class ProxyInfo(object): - """Collect information required to use a proxy.""" - def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None): - """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX - constants. For example: + """Collect information required to use a proxy.""" + bypass_hosts = () -p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000) - """ - self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: - def astuple(self): - return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, - self.proxy_user, self.proxy_pass) + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass - def isgood(self): - return socks and (self.proxy_host != None) and (self.proxy_port != None) + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) class HTTPConnectionWithTimeout(httplib.HTTPConnection): - """HTTPConnection subclass that supports timeouts""" + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): httplib.HTTPConnection.__init__(self, host, port, strict) @@ -725,27 +864,46 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): def connect(self): """Connect to the host and port specified in __init__.""" # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') msg = "getaddrinfo returns an empty list" - for res in socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM): + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: - if self.proxy_info and self.proxy_info.isgood(): + if use_proxy: self.sock = socks.socksocket(af, socktype, proto) - self.sock.setproxy(*self.proxy_info.astuple()) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) else: self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Different from httplib: support timeouts. if has_timeout(self.timeout): self.sock.settimeout(self.timeout) # End of difference from httplib. if self.debuglevel > 0: - print "connect: (%s, %s)" % (self.host, self.port) + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) - self.sock.connect(sa) + self.sock.connect((self.host, self.port) + sa[2:]) except socket.error, msg: if self.debuglevel > 0: - print 'connect fail:', (self.host, self.port) + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) if self.sock: self.sock.close() self.sock = None @@ -755,56 +913,265 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): raise socket.error, msg class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): - "This class allows communication via SSL." + """ + This class allows communication via SSL. + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=None, proxy_info=None): - httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file, - cert_file=cert_file, strict=strict) + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) self.timeout = timeout self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False def connect(self): "Connect to a host on a given (SSL) port." + msg = "getaddrinfo returns an empty list" if self.proxy_info and self.proxy_info.isgood(): - sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) - sock.setproxy(*self.proxy_info.astuple()) + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() else: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - if has_timeout(self.timeout): - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) - self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file) + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=5): + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass class Http(object): """An HTTP client that handles: -- all methods -- caching -- ETags -- compression, -- HTTPS -- Basic -- Digest -- WSSE -and more. + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. """ - def __init__(self, cache=None, timeout=None, proxy_info=None): - """The value of proxy_info is a ProxyInfo instance. + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. -If 'cache' is a string then it is used as a directory name -for a disk cache. Otherwise it must be an object that supports -the same interface as FileCache.""" + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + # Map domain name to an httplib connection self.connections = {} # The location of the cache, for now a directory # where cached responses are held. - if cache and isinstance(cache, str): + if cache and isinstance(cache, basestring): self.cache = FileCache(cache) else: self.cache = cache @@ -820,10 +1187,10 @@ the same interface as FileCache.""" # If set to False then no redirects are followed, even safe ones. self.follow_redirects = True - + # Which HTTP methods do we apply optimistic concurrency to, i.e. # which methods get an "if-match:" etag header added to them. - self.optimistic_concurrency_methods = ["PUT"] + self.optimistic_concurrency_methods = ["PUT", "PATCH"] # If 'follow_redirects' is True, and this is set to True then # all redirecs are followed, including unsafe ones. @@ -831,10 +1198,27 @@ the same interface as FileCache.""" self.ignore_etag = False - self.force_exception_to_status_code = False + self.force_exception_to_status_code = False self.timeout = timeout + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + def _auth_from_challenge(self, host, request_uri, headers, response, content): """A generator that creates Authorization objects that can be applied to requests. @@ -862,37 +1246,56 @@ the same interface as FileCache.""" self.authorizations = [] def _conn_request(self, conn, request_uri, method, body, headers): - for i in range(2): + for i in range(RETRIES): try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() conn.request(method, request_uri, body, headers) + except socket.timeout: + raise except socket.gaierror: conn.close() raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise except socket.error, e: - if not hasattr(e, 'errno'): # I don't know what this is so lets raise it if it happens + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused raise - elif e.errno == errno.ECONNREFUSED: # Connection refused - raise - # Just because the server closed the connection doesn't apparently mean - # that the server didn't send a response. - pass except httplib.HTTPException: # Just because the server closed the connection doesn't apparently mean # that the server didn't send a response. - pass + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue try: response = conn.getresponse() except (socket.error, httplib.HTTPException): - if i == 0: + if i < RETRIES-1: conn.close() conn.connect() continue else: + conn.close() raise else: content = "" if method == "HEAD": - response.close() + conn.close() else: content = response.read() response = Response(response) @@ -908,12 +1311,12 @@ the same interface as FileCache.""" auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auth = auths and sorted(auths)[0][1] or None - if auth: + if auth: auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers) - if auth: + if auth: if auth.response(response, body): auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers ) @@ -921,7 +1324,7 @@ the same interface as FileCache.""" if response.status == 401: for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): - authorization.request(method, request_uri, headers, body) + authorization.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) if response.status != 401: self.authorizations.append(authorization) @@ -944,26 +1347,31 @@ the same interface as FileCache.""" if response.status == 301 and method in ["GET", "HEAD"]: response['-x-permanent-redirect-url'] = response['location'] if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) if headers.has_key('if-none-match'): del headers['if-none-match'] if headers.has_key('if-modified-since'): del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] if response.has_key('location'): location = response['location'] old_response = copy.deepcopy(response) if not old_response.has_key('content-location'): - old_response['content-location'] = absolute_uri - redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) response.previous = old_response else: - raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) - elif response.status in [200, 203] and method == "GET": + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: # Don't cache 206's since we aren't going to handle byte range requests if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) return (response, content) @@ -978,24 +1386,25 @@ the same interface as FileCache.""" def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): """ Performs a single HTTP request. -The 'uri' is the URI of the HTTP resource and can begin -with either 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. -There is no restriction on the methods allowed. + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'body' is the entity body to be sent with the request. It is a string -object. + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. -Any extra headers that are to be sent with the request should be provided in the -'headers' dictionary. + The 'body' is the entity body to be sent with the request. It is a + string object. -The maximum number of redirect to follow before raising an -exception is 'redirections. The default is 5. + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. -The return value is a tuple of (response, content), the first -being and instance of the 'Response' class, the second being -a string that contains the response entity body. + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. """ try: if headers is None: @@ -1004,7 +1413,7 @@ a string that contains the response entity body. headers = self._normalize_headers(headers) if not headers.has_key('user-agent'): - headers['user-agent'] = "Python-httplib2/%s" % __version__ + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ uri = iri2uri(uri) @@ -1014,21 +1423,38 @@ a string that contains the response entity body. scheme = 'https' authority = domain_port[0] + proxy_info = self._get_proxy_info(scheme, authority) + conn_key = scheme+":"+authority if conn_key in self.connections: conn = self.connections[conn_key] else: if not connection_type: - connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout + connection_type = SCHEME_TO_CONNECTION[scheme] certs = list(self.certificates.iter(authority)) - if scheme == 'https' and certs: - conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], - cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) else: - conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info) + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) conn.set_debuglevel(debuglevel) - if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers: + if 'range' not in headers and 'accept-encoding' not in headers: headers['accept-encoding'] = 'gzip, deflate' info = email.Message.Message() @@ -1048,7 +1474,7 @@ a string that contains the response entity body. feedparser.feed(info) info = feedparser.close() feedparser._parse = None - except IndexError, ValueError: + except (IndexError, ValueError): self.cache.delete(cachekey) cachekey = None cached_value = None @@ -1071,13 +1497,15 @@ a string that contains the response entity body. for header in vary_headers: key = '-varied-%s' % header value = info[key] - if headers.get(header, '') != value: - cached_value = None - break + if headers.get(header, None) != value: + cached_value = None + break if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: if info.has_key('-x-permanent-redirect-url'): # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) response.previous = Response(info) response.previous.fromcache = True @@ -1085,13 +1513,13 @@ a string that contains the response entity body. # Determine our course of action: # Is the cached entry fresh or stale? # Has the client requested a non-cached response? - # - # There seems to be three possible answers: + # + # There seems to be three possible answers: # 1. [FRESH] Return the cache entry w/o doing a GET # 2. [STALE] Do the GET (but add in cache validators if available) # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request - entry_disposition = _entry_disposition(info, headers) - + entry_disposition = _entry_disposition(info, headers) + if entry_disposition == "FRESH": if not cached_value: info['status'] = '504' @@ -1113,7 +1541,7 @@ a string that contains the response entity body. if response.status == 304 and method == "GET": # Rewrite the cache entry with the new end-to-end headers - # Take all headers that are in response + # Take all headers that are in response # and overwrite their values in info. # unless they are hop-by-hop, or are listed in the connection header. @@ -1125,14 +1553,14 @@ a string that contains the response entity body. _updateCache(headers, merged_response, content, self.cache, cachekey) response = merged_response response.status = 200 - response.fromcache = True + response.fromcache = True elif response.status == 200: content = new_content else: self.cache.delete(cachekey) - content = new_content - else: + content = new_content + else: cc = _parse_cache_control(headers) if cc.has_key('only-if-cached'): info['status'] = '504' @@ -1146,34 +1574,47 @@ a string that contains the response entity body. response = e.response content = e.content response.status = 500 - response.reason = str(e) - elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): + response.reason = str(e) + elif isinstance(e, socket.timeout): content = "Request Timeout" - response = Response( { - "content-type": "text/plain", - "status": "408", - "content-length": len(content) - }) + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) response.reason = "Request Timeout" else: - content = str(e) - response = Response( { - "content-type": "text/plain", - "status": "400", - "content-length": len(content) - }) - response.reason = "Bad Request" + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" else: raise - + return (response, content) - + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + class Response(dict): """An object more like email.Message than httplib.HTTPResponse.""" - + """Is this response from our local cache""" fromcache = False @@ -1189,27 +1630,28 @@ class Response(dict): previous = None def __init__(self, info): - # info is either an email.Message or + # info is either an email.Message or # an httplib.HTTPResponse object. if isinstance(info, httplib.HTTPResponse): - for key, value in info.getheaders(): - self[key.lower()] = value + for key, value in info.getheaders(): + self[key.lower()] = value self.status = info.status self['status'] = str(self.status) self.reason = info.reason self.version = info.version elif isinstance(info, email.Message.Message): - for key, value in info.items(): - self[key] = value + for key, value in info.items(): + self[key.lower()] = value self.status = int(self['status']) else: - for key, value in info.iteritems(): - self[key] = value + for key, value in info.iteritems(): + self[key.lower()] = value self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) def __getattr__(self, name): if name == 'dict': - return self - else: - raise AttributeError, name + return self + else: + raise AttributeError, name diff --git a/libs/httplib2/cacerts.txt b/libs/httplib2/cacerts.txt new file mode 100644 index 00000000..d8a0027c --- /dev/null +++ b/libs/httplib2/cacerts.txt @@ -0,0 +1,739 @@ +# Certifcate Authority certificates for validating SSL connections. +# +# This file contains PEM format certificates generated from +# http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Netscape security libraries. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1994-2000 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +Verisign/RSA Secure Server CA +============================= + +-----BEGIN CERTIFICATE----- +MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD +VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0 +MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV +BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy +dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ +ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII +0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI +uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI +hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3 +YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc +1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA== +-----END CERTIFICATE----- + +Thawte Personal Basic CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDITCCAoqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFBlcnNvbmFsIEJhc2lj +IENBMSgwJgYJKoZIhvcNAQkBFhlwZXJzb25hbC1iYXNpY0B0aGF3dGUuY29tMB4X +DTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgcsxCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQZXJzb25hbCBCYXNpYyBD +QTEoMCYGCSqGSIb3DQEJARYZcGVyc29uYWwtYmFzaWNAdGhhd3RlLmNvbTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53 +dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK +wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7 +G1sY0b8jkyECAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQF +AAOBgQAt4plrsD16iddZopQBHyvdEktTwq1/qqcAXJFAVyVKOKqEcLnZgA+le1z7 +c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P +9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ== +-----END CERTIFICATE----- + +Thawte Personal Premium CA +========================== + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBzzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEjMCEGA1UEAxMaVGhhd3RlIFBlcnNvbmFsIFByZW1p +dW0gQ0ExKjAoBgkqhkiG9w0BCQEWG3BlcnNvbmFsLXByZW1pdW1AdGhhd3RlLmNv +bTAeFw05NjAxMDEwMDAwMDBaFw0yMDEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlDYXBlIFRvd24xGjAY +BgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9u +IFNlcnZpY2VzIERpdmlzaW9uMSMwIQYDVQQDExpUaGF3dGUgUGVyc29uYWwgUHJl +bWl1bSBDQTEqMCgGCSqGSIb3DQEJARYbcGVyc29uYWwtcHJlbWl1bUB0aGF3dGUu +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJZtn4B0TPuYwu8KHvE0Vs +Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI +Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD +ZicRFTuqW/KY3TZCstqIdQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH +b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh +KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGJ +-----END CERTIFICATE----- + +Thawte Personal Freemail CA +=========================== + +-----BEGIN CERTIFICATE----- +MIIDLTCCApagAwIBAgIBADANBgkqhkiG9w0BAQQFADCB0TELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEkMCIGA1UEAxMbVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIENBMSswKQYJKoZIhvcNAQkBFhxwZXJzb25hbC1mcmVlbWFpbEB0aGF3dGUu +Y29tMB4XDTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgdExCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEa +MBgGA1UEChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBG +cmVlbWFpbCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhh +d3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1GnX1LCUZFtx6UfY +DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E +rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq +uzgkCGqYx7Hao5iR/Xnb5VrEHLkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zAN +BgkqhkiG9w0BAQQFAAOBgQDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP +MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa +/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei +gQ== +-----END CERTIFICATE----- + +Thawte Server CA +================ + +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +Thawte Premium Server CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +Equifax Secure CA +================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05 +NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD +VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp +bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N +H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR +4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN +BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo +EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5 +FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx +lA== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh +YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7 +FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg +J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc +r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM +HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK +qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj +cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y +cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP +T8qAkbYp +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Equifax Secure Global eBusiness CA +================================== + +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 1 +============================= + +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 2 +============================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +Thawte Time Stamping CA +======================= + +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBizELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzAN +BgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAd +BgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNOTcwMTAxMDAwMDAwWhcN +MjAxMjMxMjM1OTU5WjCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4g +Q2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsG +A1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1l +c3RhbXBpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYrWHhhRYZT +6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa +Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL +8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEEBQADgYEAZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC +9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ +pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnRUEzGLhQOEkbdYATAUOK8oyvyxUBkZ +CayJSdM= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== + +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +Entrust.net Secure Server Certification Authority +================================================= + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +Go Daddy Certification Authority Root Certificate Bundle +======================================================== + +-----BEGIN CERTIFICATE----- +MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx +ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw +MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH +QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j +b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j +b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H +KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm +VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR +SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT +cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ +6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu +MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS +kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB +BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f +BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv +c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH +AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO +BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG +OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU +A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o +0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX +RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH +qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV +U+4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh +bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu +Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g +QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe +BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX +DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE +YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC +ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q +N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO +r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN +f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH +U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU +TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb +VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg +SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv +biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg +MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw +AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv +ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu +Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd +IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv +bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1 +QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O +WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf +SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +GeoTrust Global CA +================== + +-----BEGIN CERTIFICATE----- +MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 +aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw +WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE +AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m +OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu +T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c +JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR +Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz +PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm +aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM +TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g +LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO +BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv +dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB +AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL +NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W +b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S +-----END CERTIFICATE----- + diff --git a/libs/httplib2/iri2uri.py b/libs/httplib2/iri2uri.py index 70667edf..d88c91fd 100644 --- a/libs/httplib2/iri2uri.py +++ b/libs/httplib2/iri2uri.py @@ -16,7 +16,7 @@ import urlparse # Convert an IRI to a URI following the rules in RFC 3987 -# +# # The characters we need to enocde and escape are defined in the spec: # # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD @@ -28,28 +28,28 @@ import urlparse # / %xD0000-DFFFD / %xE1000-EFFFD escape_range = [ - (0xA0, 0xD7FF ), - (0xE000, 0xF8FF ), - (0xF900, 0xFDCF ), - (0xFDF0, 0xFFEF), - (0x10000, 0x1FFFD ), - (0x20000, 0x2FFFD ), - (0x30000, 0x3FFFD), - (0x40000, 0x4FFFD ), - (0x50000, 0x5FFFD ), - (0x60000, 0x6FFFD), - (0x70000, 0x7FFFD ), - (0x80000, 0x8FFFD ), - (0x90000, 0x9FFFD), - (0xA0000, 0xAFFFD ), - (0xB0000, 0xBFFFD ), - (0xC0000, 0xCFFFD), - (0xD0000, 0xDFFFD ), - (0xE1000, 0xEFFFD), - (0xF0000, 0xFFFFD ), - (0x100000, 0x10FFFD) + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), ] - + def encode(c): retval = c i = ord(c) @@ -63,19 +63,19 @@ def encode(c): def iri2uri(uri): - """Convert an IRI to a URI. Note that IRIs must be + """Convert an IRI to a URI. Note that IRIs must be passed in a unicode strings. That is, do not utf-8 encode - the IRI before passing it into the function.""" + the IRI before passing it into the function.""" if isinstance(uri ,unicode): (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) authority = authority.encode('idna') # For each character in 'ucschar' or 'iprivate' # 1. encode as utf-8 - # 2. then %-encode each octet of that utf-8 + # 2. then %-encode each octet of that utf-8 uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = "".join([encode(c) for c in uri]) return uri - + if __name__ == "__main__": import unittest @@ -83,7 +83,7 @@ if __name__ == "__main__": def test_uris(self): """Test that URIs are invariant under the transformation.""" - invariant = [ + invariant = [ u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"http://www.ietf.org/rfc/rfc2396.txt", u"ldap://[2001:db8::7]/c=GB?objectClass?one", @@ -94,7 +94,7 @@ if __name__ == "__main__": u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] for uri in invariant: self.assertEqual(uri, iri2uri(uri)) - + def test_iri(self): """ Test that the right type of escaping is done for each part of the URI.""" self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) @@ -107,4 +107,4 @@ if __name__ == "__main__": unittest.main() - + diff --git a/libs/httplib2/socks.py b/libs/httplib2/socks.py new file mode 100644 index 00000000..0991f4cf --- /dev/null +++ b/libs/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/libs/pyasn1/__init__.py b/libs/pyasn1/__init__.py index 7de39fe5..88aff79c 100644 --- a/libs/pyasn1/__init__.py +++ b/libs/pyasn1/__init__.py @@ -1 +1,8 @@ -majorVersionId = '1' +import sys + +# http://www.python.org/dev/peps/pep-0396/ +__version__ = '0.1.7' + +if sys.version_info[:2] < (2, 4): + raise RuntimeError('PyASN1 requires Python 2.4 or later') + diff --git a/libs/pyasn1/codec/__init__.py b/libs/pyasn1/codec/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/__init__.py +++ b/libs/pyasn1/codec/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/__init__.py b/libs/pyasn1/codec/ber/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/ber/__init__.py +++ b/libs/pyasn1/codec/ber/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/decoder.py b/libs/pyasn1/codec/ber/decoder.py index ae9311cb..be0cf490 100644 --- a/libs/pyasn1/codec/ber/decoder.py +++ b/libs/pyasn1/codec/ber/decoder.py @@ -1,21 +1,24 @@ # BER decoder from pyasn1.type import tag, base, univ, char, useful, tagmap from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import oct2int, octs2ints -from pyasn1 import error +from pyasn1.compat.octets import oct2int, octs2ints, isOctetsType +from pyasn1 import debug, error class AbstractDecoder: protoComponent = None def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Decoder not implemented for %s' % (tagSet,)) def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % (tagSet,)) class AbstractSimpleDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatSimple,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(value, tagSet) elif value is None: @@ -24,7 +27,10 @@ class AbstractSimpleDecoder(AbstractDecoder): return asn1Spec.clone(value) class AbstractConstructedDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatConstructed,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(tagSet) else: @@ -32,19 +38,34 @@ class AbstractConstructedDecoder(AbstractDecoder): class EndOfOctetsDecoder(AbstractSimpleDecoder): def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return eoo.endOfOctets, substrate[:length] + length, state, decodeFun, substrateFun): + return eoo.endOfOctets, substrate[length:] class ExplicitTagDecoder(AbstractSimpleDecoder): + protoComponent = univ.Any('') + tagFormats = (tag.tagFormatConstructed,) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return decodeFun(substrate[:length], asn1Spec, tagSet, length) + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) + head, tail = substrate[:length], substrate[length:] + value, _ = decodeFun(head, asn1Spec, tagSet, length) + return value, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) value, substrate = decodeFun(substrate, asn1Spec, tagSet, length) terminator, substrate = decodeFun(substrate) - if terminator == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(terminator) and \ + terminator == eoo.endOfOctets: return value, substrate else: raise error.PyAsn1Error('Missing end-of-octets terminator') @@ -71,79 +92,71 @@ class IntegerDecoder(AbstractSimpleDecoder): '\xfb': -5 } - def _valueFilter(self, value): - try: - return int(value) - except OverflowError: - return value - def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: - raise error.PyAsn1Error('Empty substrate') - if substrate in self.precomputedValues: - value = self.precomputedValues[substrate] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0), tail + if head in self.precomputedValues: + value = self.precomputedValues[head] else: - firstOctet = oct2int(substrate[0]) + firstOctet = oct2int(head[0]) if firstOctet & 0x80: value = -1 else: value = 0 - for octet in substrate: + for octet in head: value = value << 8 | oct2int(octet) - value = self._valueFilter(value) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class BooleanDecoder(IntegerDecoder): protoComponent = univ.Boolean(0) - def _valueFilter(self, value): - if value: - return 1 - else: - return 0 + def _createComponent(self, asn1Spec, tagSet, value=None): + return IntegerDecoder._createComponent(self, asn1Spec, tagSet, value and 1 or 0) class BitStringDecoder(AbstractSimpleDecoder): protoComponent = univ.BitString(()) + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - if not substrate: - raise error.PyAsn1Error('Missing initial octet') - trailingBits = oct2int(substrate[0]) + if not head: + raise error.PyAsn1Error('Empty substrate') + trailingBits = oct2int(head[0]) if trailingBits > 7: raise error.PyAsn1Error( 'Trailing bits overflow %s' % trailingBits ) - substrate = substrate[1:] - lsb = p = 0; l = len(substrate)-1; b = () + head = head[1:] + lsb = p = 0; l = len(head)-1; b = () while p <= l: if p == l: lsb = trailingBits j = 7 - o = oct2int(substrate[p]) + o = oct2int(head[p]) while j >= lsb: b = b + ((o>>j)&0x01,) j = j - 1 p = p + 1 - return self._createComponent(asn1Spec, tagSet, b), '' + return self._createComponent(asn1Spec, tagSet, b), tail r = self._createComponent(asn1Spec, tagSet, ()) - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -154,27 +167,29 @@ class BitStringDecoder(AbstractSimpleDecoder): class OctetStringDecoder(AbstractSimpleDecoder): protoComponent = univ.OctetString('') + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - return self._createComponent(asn1Spec, tagSet, substrate), '' + return self._createComponent(asn1Spec, tagSet, head), tail r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -186,93 +201,89 @@ class OctetStringDecoder(AbstractSimpleDecoder): class NullDecoder(AbstractSimpleDecoder): protoComponent = univ.Null('') def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if substrate: - raise error.PyAsn1Error('Unexpected substrate for Null') - return r, substrate + if head: + raise error.PyAsn1Error('Unexpected %d-octet substrate for Null' % length) + return r, tail class ObjectIdentifierDecoder(AbstractSimpleDecoder): protoComponent = univ.ObjectIdentifier(()) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - oid = (); index = 0 - # Get the first subid - subId = oct2int(substrate[index]) - oid = oid + divmod(subId, 40) - index = index + 1 - substrateLen = len(substrate) - + # Get the first subid + subId = oct2int(head[0]) + oid = divmod(subId, 40) + + index = 1 + substrateLen = len(head) while index < substrateLen: - subId = oct2int(substrate[index]) - if subId < 128: - oid = oid + (subId,) - index = index + 1 - else: + subId = oct2int(head[index]) + index = index + 1 + if subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in sub-ID OID + # encoding, tolerating it opens a vulnerability. + # See http://www.cosic.esat.kuleuven.be/publications/article-1432.pdf page 7 + raise error.PyAsn1Error('Invalid leading 0x80 in sub-OID') + elif subId > 128: # Construct subid from a number of octets nextSubId = subId subId = 0 - while nextSubId >= 128 and index < substrateLen: + while nextSubId >= 128: subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (oid,) + ) + nextSubId = oct2int(head[index]) index = index + 1 - nextSubId = oct2int(substrate[index]) - if index == substrateLen: - raise error.SubstrateUnderrunError( - 'Short substrate for OID %s' % oid - ) subId = (subId << 7) + nextSubId - oid = oid + (subId,) - index = index + 1 - return self._createComponent(asn1Spec, tagSet, oid), substrate[index:] + oid = oid + (subId,) + return self._createComponent(asn1Spec, tagSet, oid), tail class RealDecoder(AbstractSimpleDecoder): protoComponent = univ.Real() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] - if not length: - raise error.SubstrateUnderrunError('Short substrate for Real') - fo = oct2int(substrate[0]); substrate = substrate[1:] - if fo & 0x40: # infinite value - value = fo & 0x01 and '-inf' or 'inf' - elif fo & 0x80: # binary enoding - if fo & 0x11 == 0: - n = 1 - elif fo & 0x01: - n = 2 - elif fo & 0x02: - n = 3 - else: - n = oct2int(substrate[0]) - eo, substrate = substrate[:n], substrate[n:] - if not eo or not substrate: + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0.0), tail + fo = oct2int(head[0]); head = head[1:] + if fo & 0x80: # binary enoding + n = (fo & 0x03) + 1 + if n == 4: + n = oct2int(head[0]) + eo, head = head[:n], head[n:] + if not eo or not head: raise error.PyAsn1Error('Real exponent screwed') - e = 0 + e = oct2int(eo[0]) & 0x80 and -1 or 0 while eo: # exponent e <<= 8 e |= oct2int(eo[0]) eo = eo[1:] p = 0 - while substrate: # value + while head: # value p <<= 8 - p |= oct2int(substrate[0]) - substrate = substrate[1:] + p |= oct2int(head[0]) + head = head[1:] if fo & 0x40: # sign bit p = -p value = (p, 2, e) + elif fo & 0x40: # infinite value + value = fo & 0x01 and '-inf' or 'inf' elif fo & 0xc0 == 0: # character encoding try: if fo & 0x3 == 0x1: # NR1 - value = (int(substrate), 10, 0) + value = (int(head), 10, 0) elif fo & 0x3 == 0x2: # NR2 - value = float(substrate) + value = float(head) elif fo & 0x3 == 0x3: # NR3 - value = float(substrate) + value = float(head) else: raise error.SubstrateUnderrunError( 'Unknown NR (tag %s)' % fo @@ -281,13 +292,11 @@ class RealDecoder(AbstractSimpleDecoder): raise error.SubstrateUnderrunError( 'Bad character Real syntax' ) - elif fo & 0xc0 == 0x40: # special real value - pass else: raise error.SubstrateUnderrunError( 'Unknown encoding (tag %s)' % fo ) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class SequenceDecoder(AbstractConstructedDecoder): protoComponent = univ.Sequence() @@ -301,17 +310,15 @@ class SequenceDecoder(AbstractConstructedDecoder): return r.getComponentPositionNearType(t, idx) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) idx = 0 - if not decodeFun: - return r, substrate - while substrate: + if substrateFun: + return substrateFun(r, substrate, length) + while head: asn1Spec = self._getComponentTagMap(r, idx) - component, substrate = decodeFun( - substrate, asn1Spec - ) + component, head = decodeFun(head, asn1Spec) idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx ) @@ -319,18 +326,19 @@ class SequenceDecoder(AbstractConstructedDecoder): idx = idx + 1 r.setDefaultComponents() r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) idx = 0 while substrate: asn1Spec = self._getComponentTagMap(r, idx) - if not decodeFun: - return r, substrate component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx @@ -348,32 +356,31 @@ class SequenceDecoder(AbstractConstructedDecoder): class SequenceOfDecoder(AbstractConstructedDecoder): protoComponent = univ.SequenceOf() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun( - substrate, asn1Spec - ) + while head: + component, head = decodeFun(head, asn1Spec) r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 @@ -401,43 +408,68 @@ class SetOfDecoder(SequenceOfDecoder): class ChoiceDecoder(AbstractConstructedDecoder): protoComponent = univ.Choice() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) if r.getTagSet() == tagSet: # explicitly tagged Choice - component, substrate = decodeFun( - substrate, r.getComponentTagMap() + component, head = decodeFun( + head, r.getComponentTagMap() ) else: - component, substrate = decodeFun( - substrate, r.getComponentTagMap(), tagSet, length, state + component, head = decodeFun( + head, r.getComponentTagMap(), tagSet, length, state ) if isinstance(component, univ.Choice): effectiveTagSet = component.getEffectiveTagSet() else: effectiveTagSet = component.getTagSet() r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) + return r, tail + + def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, + length, state, decodeFun, substrateFun): + r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) + if r.getTagSet() == tagSet: # explicitly tagged Choice + component, substrate = decodeFun(substrate, r.getComponentTagMap()) + eooMarker, substrate = decodeFun(substrate) # eat up EOO marker + if not eoo.endOfOctets.isSameTypeWith(eooMarker) or \ + eooMarker != eoo.endOfOctets: + raise error.PyAsn1Error('No EOO seen before substrate ends') + else: + component, substrate= decodeFun( + substrate, r.getComponentTagMap(), tagSet, length, state + ) + if isinstance(component, univ.Choice): + effectiveTagSet = component.getEffectiveTagSet() + else: + effectiveTagSet = component.getTagSet() + r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) return r, substrate - indefLenValueDecoder = valueDecoder - class AnyDecoder(AbstractSimpleDecoder): protoComponent = univ.Any() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is None or \ asn1Spec is not None and tagSet != asn1Spec.getTagSet(): # untagged Any container, recover inner header substrate length = length + len(fullSubstrate) - len(substrate) substrate = fullSubstrate - substrate = substrate[:length] - return self._createComponent(asn1Spec, tagSet, value=substrate), '' + if substrateFun: + return substrateFun(self._createComponent(asn1Spec, tagSet), + substrate, length) + head, tail = substrate[:length], substrate[length:] + return self._createComponent(asn1Spec, tagSet, value=head), tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is not None and tagSet == asn1Spec.getTagSet(): # tagged Any type -- consume header substrate header = '' @@ -450,11 +482,12 @@ class AnyDecoder(AbstractSimpleDecoder): # Any components do not inherit initial tag asn1Spec = self.protoComponent - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -550,7 +583,10 @@ class Decoder: self.__tagSetCache = {} def __call__(self, substrate, asn1Spec=None, tagSet=None, - length=None, state=stDecodeTag, recursiveFlag=1): + length=None, state=stDecodeTag, recursiveFlag=1, + substrateFun=None): + if debug.logger & debug.flagDecoder: + debug.logger('decoder called at scope %s with state %d, working with up to %d octets of substrate: %s' % (debug.scope, state, len(substrate), debug.hexdump(substrate))) fullSubstrate = substrate while state != stStop: if state == stDecodeTag: @@ -559,6 +595,9 @@ class Decoder: raise error.SubstrateUnderrunError( 'Short octet stream on tag decoding' ) + if not isOctetsType(substrate) and \ + not isinstance(substrate, univ.OctetString): + raise error.PyAsn1Error('Bad octet stream type') firstOctet = substrate[0] substrate = substrate[1:] @@ -598,6 +637,7 @@ class Decoder: else: tagSet = lastTag + tagSet state = stDecodeLength + debug.logger and debug.logger & debug.flagDecoder and debug.logger('tag decoded into %r, decoding length' % tagSet) if state == stDecodeLength: # Decode length if not substrate: @@ -625,12 +665,13 @@ class Decoder: for char in lengthString: length = (length << 8) | oct2int(char) size = size + 1 - state = stGetValueDecoder substrate = substrate[size:] if length != -1 and len(substrate) < length: raise error.SubstrateUnderrunError( '%d-octet short' % (length - len(substrate)) ) + state = stGetValueDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value length decoded into %d, payload substrate is: %s' % (length, debug.hexdump(length == -1 and substrate or substrate[:length]))) if state == stGetValueDecoder: if asn1Spec is None: state = stGetValueDecoderByTag @@ -669,14 +710,27 @@ class Decoder: state = stDecodeValue else: state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by a built-in type, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(concreteDecoder is None and '?' or concreteDecoder.protoComponent.__class__.__name__) if state == stGetValueDecoderByAsn1Spec: if isinstance(asn1Spec, (dict, tagmap.TagMap)): if tagSet in asn1Spec: __chosenSpec = asn1Spec[tagSet] else: __chosenSpec = None + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('candidate ASN.1 spec is a map of:') + for t, v in asn1Spec.getPosMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + if asn1Spec.getNegMap(): + debug.logger('but neither of: ') + for i in asn1Spec.getNegMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + debug.logger('new candidate ASN.1 spec is %s, chosen by %r' % (__chosenSpec is None and '' or __chosenSpec.__class__.__name__, tagSet)) else: __chosenSpec = asn1Spec + debug.logger and debug.logger & debug.flagDecoder and debug.logger('candidate ASN.1 spec is %s' % asn1Spec.__class__.__name__) if __chosenSpec is not None and ( tagSet == __chosenSpec.getTagSet() or \ tagSet in __chosenSpec.getTagMap() @@ -687,9 +741,11 @@ class Decoder: __chosenSpec.typeId in self.__typeMap: # ambiguous type concreteDecoder = self.__typeMap[__chosenSpec.typeId] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen for an ambiguous type by type ID %s' % (__chosenSpec.typeId,)) elif baseTagSet in self.__tagMap: # base type or tagged subtype concreteDecoder = self.__tagMap[baseTagSet] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen by base %r' % (baseTagSet,)) else: concreteDecoder = None if concreteDecoder: @@ -700,8 +756,13 @@ class Decoder: elif tagSet == self.__endOfOctetsTagSet: concreteDecoder = self.__tagMap[tagSet] state = stDecodeValue + debug.logger and debug.logger & debug.flagDecoder and debug.logger('end-of-octets found') else: + concreteDecoder = None state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by ASN.1 spec, decoding %s' % (state == stDecodeValue and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(__chosenSpec is None and '?' or __chosenSpec.__class__.__name__) if state == stTryAsExplicitTag: if tagSet and \ tagSet[0][1] == tag.tagFormatConstructed and \ @@ -710,34 +771,35 @@ class Decoder: concreteDecoder = explicitTagDecoder state = stDecodeValue else: + concreteDecoder = None state = self.defaultErrorState + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as failure')) if state == stDumpRawValue: concreteDecoder = self.defaultRawDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding value' % concreteDecoder.__class__.__name__) state = stDecodeValue if state == stDecodeValue: - if recursiveFlag: - decodeFun = self - else: - decodeFun = None + if recursiveFlag == 0 and not substrateFun: # legacy + substrateFun = lambda a,b,c: (a,b[:c]) if length == -1: # indef length value, substrate = concreteDecoder.indefLenValueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) else: - value, _substrate = concreteDecoder.valueDecoder( + value, substrate = concreteDecoder.valueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) - if recursiveFlag: - substrate = substrate[length:] - else: - substrate = _substrate state = stStop + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s yields type %s, value:\n%s\n...remaining substrate is: %s' % (concreteDecoder.__class__.__name__, value.__class__.__name__, value.prettyPrint(), substrate and debug.hexdump(substrate) or '')) if state == stErrorCondition: raise error.PyAsn1Error( '%r not in asn1Spec: %r' % (tagSet, asn1Spec) ) + if debug.logger and debug.logger & debug.flagDecoder: + debug.scope.pop() + debug.logger('decoder left scope %s, call completed' % debug.scope) return value, substrate decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/ber/encoder.py b/libs/pyasn1/codec/ber/encoder.py index 2149b0ba..173949d0 100644 --- a/libs/pyasn1/codec/ber/encoder.py +++ b/libs/pyasn1/codec/ber/encoder.py @@ -1,8 +1,8 @@ # BER encoder from pyasn1.type import base, tag, univ, char, useful from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import int2oct, ints2octs, null, str2octs -from pyasn1 import error +from pyasn1.compat.octets import int2oct, oct2int, ints2octs, null, str2octs +from pyasn1 import debug, error class Error(Exception): pass @@ -78,9 +78,24 @@ class ExplicitlyTaggedItemEncoder(AbstractItemEncoder): explicitlyTaggedItemEncoder = ExplicitlyTaggedItemEncoder() +class BooleanEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 + _true = ints2octs((1,)) + _false = ints2octs((0,)) + def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + return value and self._true or self._false, 0 + class IntegerEncoder(AbstractItemEncoder): supportIndefLenMode = 0 + supportCompactZero = False def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + if value == 0: # shortcut for zero value + if self.supportCompactZero: + # this seems to be a correct way for encoding zeros + return null, 0 + else: + # this seems to be a widespread way for encoding zeros + return ints2octs((0,)), 0 octets = [] value = int(value) # to save on ops on asn1 type while 1: @@ -149,18 +164,15 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): index = 5 else: if len(oid) < 2: - raise error.PyAsn1Error('Short OID %s' % value) + raise error.PyAsn1Error('Short OID %s' % (value,)) # Build the first twos - index = 0 - subid = oid[index] * 40 - subid = subid + oid[index+1] - if subid < 0 or subid > 0xff: + if oid[0] > 6 or oid[1] > 39 or oid[0] == 6 and oid[1] > 15: raise error.PyAsn1Error( - 'Initial sub-ID overflow %s in OID %s' % (oid[index:], value) + 'Initial sub-ID overflow %s in OID %s' % (oid[:2], value) ) - octets = (subid,) - index = index + 2 + octets = (oid[0] * 40 + oid[1],) + index = 2 # Cycle through subids for subid in oid[index:]: @@ -184,6 +196,7 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): return ints2octs(octets), 0 class RealEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 def encodeValue(self, encodeFun, value, defMode, maxChunkSize): if value.isPlusInfinity(): return int2oct(0x40), 0 @@ -206,9 +219,11 @@ class RealEncoder(AbstractItemEncoder): m >>= 1 e += 1 eo = null - while e: + while e not in (0, -1): eo = int2oct(e&0xff) + eo e >>= 8 + if e == 0 and eo and oct2int(eo[0]) & 0x80: + eo = int2oct(0) + eo n = len(eo) if n > 0xff: raise error.PyAsn1Error('Real exponent overflow') @@ -268,7 +283,7 @@ class AnyEncoder(OctetStringEncoder): tagMap = { eoo.endOfOctets.tagSet: EndOfOctetsEncoder(), - univ.Boolean.tagSet: IntegerEncoder(), + univ.Boolean.tagSet: BooleanEncoder(), univ.Integer.tagSet: IntegerEncoder(), univ.BitString.tagSet: BitStringEncoder(), univ.OctetString.tagSet: OctetStringEncoder(), @@ -313,6 +328,7 @@ class Encoder: self.__typeMap = typeMap def __call__(self, value, defMode=1, maxChunkSize=0): + debug.logger & debug.flagEncoder and debug.logger('encoder called in %sdef mode, chunk size %s for type %s, value:\n%s' % (not defMode and 'in' or '', maxChunkSize, value.__class__.__name__, value.prettyPrint())) tagSet = value.getTagSet() if len(tagSet) > 1: concreteEncoder = explicitlyTaggedItemEncoder @@ -322,13 +338,16 @@ class Encoder: elif tagSet in self.__tagMap: concreteEncoder = self.__tagMap[tagSet] else: - baseTagSet = value.baseTagSet - if baseTagSet in self.__tagMap: - concreteEncoder = self.__tagMap[baseTagSet] + tagSet = value.baseTagSet + if tagSet in self.__tagMap: + concreteEncoder = self.__tagMap[tagSet] else: - raise Error('No encoder for %s' % value) - return concreteEncoder.encode( + raise Error('No encoder for %s' % (value,)) + debug.logger & debug.flagEncoder and debug.logger('using value codec %s chosen by %r' % (concreteEncoder.__class__.__name__, tagSet)) + substrate = concreteEncoder.encode( self, value, defMode, maxChunkSize ) + debug.logger & debug.flagEncoder and debug.logger('built %s octets of substrate: %s\nencoder completed' % (len(substrate), debug.hexdump(substrate))) + return substrate encode = Encoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/cer/__init__.py b/libs/pyasn1/codec/cer/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/cer/__init__.py +++ b/libs/pyasn1/codec/cer/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/cer/decoder.py b/libs/pyasn1/codec/cer/decoder.py index 71395d22..9fd37c13 100644 --- a/libs/pyasn1/codec/cer/decoder.py +++ b/libs/pyasn1/codec/cer/decoder.py @@ -7,22 +7,25 @@ from pyasn1 import error class BooleanDecoder(decoder.AbstractSimpleDecoder): protoComponent = univ.Boolean(0) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - byte = oct2int(substrate[0]) + byte = oct2int(head[0]) + # CER/DER specifies encoding of TRUE as 0xFF and FALSE as 0x0, while + # BER allows any non-zero value as TRUE; cf. sections 8.2.2. and 11.1 + # in http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf if byte == 0xff: value = 1 elif byte == 0x00: value = 0 else: raise error.PyAsn1Error('Boolean CER violation: %s' % byte) - return self._createComponent(asn1Spec, tagSet, value), substrate[1:] + return self._createComponent(asn1Spec, tagSet, value), tail tagMap = decoder.tagMap.copy() tagMap.update({ - univ.Boolean.tagSet: BooleanDecoder(), + univ.Boolean.tagSet: BooleanDecoder() }) typeMap = decoder.typeMap diff --git a/libs/pyasn1/codec/der/__init__.py b/libs/pyasn1/codec/der/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/der/__init__.py +++ b/libs/pyasn1/codec/der/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/der/decoder.py b/libs/pyasn1/codec/der/decoder.py index 0f5a24ca..604abec2 100644 --- a/libs/pyasn1/codec/der/decoder.py +++ b/libs/pyasn1/codec/der/decoder.py @@ -2,4 +2,8 @@ from pyasn1.type import univ from pyasn1.codec.cer import decoder -decode = decoder.Decoder(decoder.tagMap, decoder.typeMap) +tagMap = decoder.tagMap +typeMap = decoder.typeMap +Decoder = decoder.Decoder + +decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/compat/__init__.py b/libs/pyasn1/compat/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/compat/__init__.py +++ b/libs/pyasn1/compat/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/compat/octets.py b/libs/pyasn1/compat/octets.py index d0303eaa..f7f2a29b 100644 --- a/libs/pyasn1/compat/octets.py +++ b/libs/pyasn1/compat/octets.py @@ -8,6 +8,7 @@ if version_info[0] <= 2: octs2ints = lambda s: [ oct2int(x) for x in s ] str2octs = lambda x: x octs2str = lambda x: x + isOctetsType = lambda s: isinstance(s, str) else: ints2octs = bytes int2oct = lambda x: ints2octs((x,)) @@ -16,3 +17,4 @@ else: octs2ints = lambda s: [ x for x in s ] str2octs = lambda x: x.encode() octs2str = lambda x: x.decode() + isOctetsType = lambda s: isinstance(s, bytes) diff --git a/libs/pyasn1/debug.py b/libs/pyasn1/debug.py new file mode 100644 index 00000000..c27cb1d4 --- /dev/null +++ b/libs/pyasn1/debug.py @@ -0,0 +1,65 @@ +import sys +from pyasn1.compat.octets import octs2ints +from pyasn1 import error +from pyasn1 import __version__ + +flagNone = 0x0000 +flagEncoder = 0x0001 +flagDecoder = 0x0002 +flagAll = 0xffff + +flagMap = { + 'encoder': flagEncoder, + 'decoder': flagDecoder, + 'all': flagAll + } + +class Debug: + defaultPrinter = sys.stderr.write + def __init__(self, *flags): + self._flags = flagNone + self._printer = self.defaultPrinter + self('running pyasn1 version %s' % __version__) + for f in flags: + if f not in flagMap: + raise error.PyAsn1Error('bad debug flag %s' % (f,)) + self._flags = self._flags | flagMap[f] + self('debug category \'%s\' enabled' % f) + + def __str__(self): + return 'logger %s, flags %x' % (self._printer, self._flags) + + def __call__(self, msg): + self._printer('DBG: %s\n' % msg) + + def __and__(self, flag): + return self._flags & flag + + def __rand__(self, flag): + return flag & self._flags + +logger = 0 + +def setLogger(l): + global logger + logger = l + +def hexdump(octets): + return ' '.join( + [ '%s%.2X' % (n%16 == 0 and ('\n%.5d: ' % n) or '', x) + for n,x in zip(range(len(octets)), octs2ints(octets)) ] + ) + +class Scope: + def __init__(self): + self._list = [] + + def __str__(self): return '.'.join(self._list) + + def push(self, token): + self._list.append(token) + + def pop(self): + return self._list.pop() + +scope = Scope() diff --git a/libs/pyasn1/type/__init__.py b/libs/pyasn1/type/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/type/__init__.py +++ b/libs/pyasn1/type/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/type/base.py b/libs/pyasn1/type/base.py index db31671e..40873719 100644 --- a/libs/pyasn1/type/base.py +++ b/libs/pyasn1/type/base.py @@ -120,7 +120,12 @@ class AbstractSimpleAsn1Item(Asn1ItemBase): def prettyIn(self, value): return value def prettyOut(self, value): return str(value) - def prettyPrint(self, scope=0): return self.prettyOut(self._value) + def prettyPrint(self, scope=0): + if self._value is noValue: + return '' + else: + return self.prettyOut(self._value) + # XXX Compatibility stub def prettyPrinter(self, scope=0): return self.prettyPrint(scope) diff --git a/libs/pyasn1/type/namedtype.py b/libs/pyasn1/type/namedtype.py index aa9c5678..48967a5f 100644 --- a/libs/pyasn1/type/namedtype.py +++ b/libs/pyasn1/type/namedtype.py @@ -60,12 +60,12 @@ class NamedTypes: tagMap = self.__namedTypes[idx].getType().getTagMap() for t in tagMap.getPosMap(): if t in self.__tagToPosIdx: - raise error.PyAsn1Error('Duplicate type %s' % t) + raise error.PyAsn1Error('Duplicate type %s' % (t,)) self.__tagToPosIdx[t] = idx try: return self.__tagToPosIdx[tagSet] except KeyError: - raise error.PyAsn1Error('Type %s not found' % tagSet) + raise error.PyAsn1Error('Type %s not found' % (tagSet,)) def getNameByPosition(self, idx): try: @@ -79,12 +79,12 @@ class NamedTypes: idx = idx - 1 n = self.__namedTypes[idx].getName() if n in self.__nameToPosIdx: - raise error.PyAsn1Error('Duplicate name %s' % n) + raise error.PyAsn1Error('Duplicate name %s' % (n,)) self.__nameToPosIdx[n] = idx try: return self.__nameToPosIdx[name] except KeyError: - raise error.PyAsn1Error('Name %s not found' % name) + raise error.PyAsn1Error('Name %s not found' % (name,)) def __buildAmbigiousTagMap(self): ambigiousTypes = () diff --git a/libs/pyasn1/type/namedval.py b/libs/pyasn1/type/namedval.py index 815e2d42..d0fea7cc 100644 --- a/libs/pyasn1/type/namedval.py +++ b/libs/pyasn1/type/namedval.py @@ -15,10 +15,10 @@ class NamedValues: name = namedValue val = automaticVal if name in self.nameToValIdx: - raise error.PyAsn1Error('Duplicate name %s' % name) + raise error.PyAsn1Error('Duplicate name %s' % (name,)) self.nameToValIdx[name] = val if val in self.valToNameIdx: - raise error.PyAsn1Error('Duplicate value %s' % name) + raise error.PyAsn1Error('Duplicate value %s=%s' % (name, val)) self.valToNameIdx[val] = name self.namedValues = self.namedValues + ((name, val),) automaticVal = automaticVal + 1 diff --git a/libs/pyasn1/type/tag.py b/libs/pyasn1/type/tag.py index 0cf67ebd..1144907f 100644 --- a/libs/pyasn1/type/tag.py +++ b/libs/pyasn1/type/tag.py @@ -18,7 +18,7 @@ class Tag: def __init__(self, tagClass, tagFormat, tagId): if tagId < 0: raise error.PyAsn1Error( - 'Negative tag ID (%s) not allowed' % tagId + 'Negative tag ID (%s) not allowed' % (tagId,) ) self.__tag = (tagClass, tagFormat, tagId) self.uniq = (tagClass, tagId) diff --git a/libs/pyasn1/type/tagmap.py b/libs/pyasn1/type/tagmap.py index 53e1791a..7cec3a10 100644 --- a/libs/pyasn1/type/tagmap.py +++ b/libs/pyasn1/type/tagmap.py @@ -28,7 +28,7 @@ class TagMap: def clone(self, parentType, tagMap, uniq=False): if self.__defType is not None and tagMap.getDef() is not None: - raise error.PyAsn1Error('Duplicate default value at %s' % self) + raise error.PyAsn1Error('Duplicate default value at %s' % (self,)) if tagMap.getDef() is not None: defType = tagMap.getDef() else: @@ -37,7 +37,7 @@ class TagMap: posMap = self.__posMap.copy() for k in tagMap.getPosMap(): if uniq and k in posMap: - raise error.PyAsn1Error('Duplicate positive key %s' % k) + raise error.PyAsn1Error('Duplicate positive key %s' % (k,)) posMap[k] = parentType negMap = self.__negMap.copy() diff --git a/libs/pyasn1/type/univ.py b/libs/pyasn1/type/univ.py index cb4f49b7..9cd16f8a 100644 --- a/libs/pyasn1/type/univ.py +++ b/libs/pyasn1/type/univ.py @@ -69,13 +69,18 @@ class Integer(base.AbstractSimpleAsn1Item): def prettyIn(self, value): if not isinstance(value, str): - return int(value) + try: + return int(value) + except: + raise error.PyAsn1Error( + 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) + ) r = self.__namedValues.getValue(value) if r is not None: return r try: return int(value) - except ValueError: + except: raise error.PyAsn1Error( 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) ) @@ -224,14 +229,14 @@ class BitString(base.AbstractSimpleAsn1Item): return tuple(r) else: raise error.PyAsn1Error( - 'Bad BIT STRING value notation %s' % value + 'Bad BIT STRING value notation %s' % (value,) ) else: for i in value.split(','): j = self.__namedValues.getValue(i) if j is None: raise error.PyAsn1Error( - 'Unknown bit identifier \'%s\'' % i + 'Unknown bit identifier \'%s\'' % (i,) ) if j >= len(r): r.extend([0]*(j-len(r)+1)) @@ -528,7 +533,7 @@ class Real(base.AbstractSimpleAsn1Item): ) if value[1] not in (2, 10): raise error.PyAsn1Error( - 'Prohibited base for Real value: %s' % value[1] + 'Prohibited base for Real value: %s' % (value[1],) ) if value[1] == 10: value = self.__normalizeBase10(value) @@ -648,7 +653,7 @@ class SetOf(base.AbstractConstructedAsn1Item): def _verifyComponent(self, idx, value): if self._componentType is not None and \ not self._componentType.isSuperTypeOf(value): - raise error.PyAsn1Error('Component type error %s' % value) + raise error.PyAsn1Error('Component type error %s' % (value,)) def getComponentByPosition(self, idx): return self._componentValues[idx] def setComponentByPosition(self, idx, value=None, verifyConstraints=True): @@ -924,9 +929,9 @@ class Choice(Set): return self._componentValues[self._currentIdx] >= other return NotImplemented if sys.version_info[0] <= 2: - def __nonzero__(self, other): return bool(self._componentValues) + def __nonzero__(self): return bool(self._componentValues) else: - def __bool__(self, other): return bool(self._componentValues) + def __bool__(self): return bool(self._componentValues) def __len__(self): return self._currentIdx is not None and 1 or 0 diff --git a/libs/pyutil/_version.py b/libs/pyutil/_version.py index 617d2205..376b2b9b 100644 --- a/libs/pyutil/_version.py +++ b/libs/pyutil/_version.py @@ -6,7 +6,7 @@ # pyutil.version_class for a description of what the different fields mean. __pkgname__ = "pyutil" -verstr = "1.9.3" +verstr = "1.9.7" try: from pyutil.version_class import Version as pyutil_Version __version__ = pyutil_Version(verstr) diff --git a/libs/pyutil/benchutil.py b/libs/pyutil/benchutil.py index 3e773a63..6c286346 100644 --- a/libs/pyutil/benchutil.py +++ b/libs/pyutil/benchutil.py @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -21,10 +21,10 @@ the second, e.g.: >>> rep_bench(fib, 25, UNITS_PER_SECOND=1000) best: 1.968e+00, 3th-best: 1.987e+00, mean: 2.118e+00, 3th-worst: 2.175e+00, worst: 2.503e+00 (of 10) -The output is reporting the number of milliseconds that executing the function -took, divided by N, from ten different invocations of fib(). It reports the -best, worst, M-th best, M-th worst, and mean, where "M" is the natural log of -the number of invocations (in this case 10). +The output is reporting the number of milliseconds that executing the +function took, divided by N, from ten different invocations of +fib(). It reports the best, worst, M-th best, M-th worst, and mean, +where "M" is 1/4 of the number of invocations (in this case 10). 2. Now run it with different values of N and look for patterns: @@ -74,10 +74,12 @@ and the main function is to make them be methods of the same object, e.g.: 4. Things to fix: - a. I used to have it hooked up to use the "hotshot" profiler on the code being - measured. I recently tried to change it to use the newer cProfile profiler - instead, but I don't understand the interface to cProfiler so it just gives an - exception if you pass profile=True. Please fix this and send me a patch. + a. I used to have it hooked up to use the "hotshot" profiler on the + code being measured. I recently tried to change it to use the newer + cProfile profiler instead, but I don't understand the interface to + cProfiler so it just gives an exception if you pass + profile=True. Please fix this and send me a patch. xxx change it to + statprof b. Wouldn't it be great if this script emitted results in a json format that was understood by a tool to make pretty interactive explorable graphs? The @@ -122,7 +124,7 @@ def mult(a, b): except TypeError: return to_decimal(a) * to_decimal(b) -def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): +def rep_bench(func, n, runtime=1.0, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME (wall-clock time) has already elapsed (unless MAXTIME is None). @@ -130,33 +132,43 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p @param quiet Don't print anything--just return the results dict. """ assert isinstance(n, int), (n, type(n)) + global worstemptymeasure + emsta = clock() + do_nothing(2**32) + emstop = clock() + empty = emstop - emsta + if empty > worstemptymeasure: + worstemptymeasure = empty + if (worstemptymeasure*2) >= runtime: + raise BadMeasure("Apparently simply invoking an empty Python function can take as long as %0.10f seconds, and we were running iterations for only about %0.10f seconds. So the measurement of the runtime of the code under benchmark is not reliable. Please pass a higher number for the 'runtime' argument to bench_it().") + startwallclocktime = time.time() - tls = [] # elapsed time in seconds + tls = [] # (elapsed time per iter in seconds, iters) bmes = [] while ((len(tls) < MAXREPS) or (MAXREPS is None)) and ((MAXTIME is None) or ((time.time() - startwallclocktime) < MAXTIME)): if initfunc: initfunc(n) try: - tl = bench_it(func, n, profile=profile, profresults=profresults) + tl, iters = bench_it(func, n, runtime=runtime, profile=profile, profresults=profresults) except BadMeasure, bme: bmes.append(bme) else: - tls.append(tl) + tls.append((tl, iters)) if len(tls) == 0: raise Exception("Couldn't get any measurements within time limits or number-of-attempts limits. Maybe something is wrong with your clock? %s" % (bmes,)) - sumtls = reduce(operator.__add__, tls) + sumtls = sum([tl for (tl, iters) in tls]) mean = sumtls / len(tls) tls.sort() - worst = tls[-1] - best = tls[0] - _assert(best > worstemptymeasure*MARGINOFERROR, "%s(n=%s) took %0.10f seconds, but we cannot measure times much less than about %0.10f seconds. Try a more time-consuming variant (such as higher n)." % (func, n, best, worstemptymeasure*MARGINOFERROR,)) + worst = tls[-1][0] + best = tls[0][0] + m = len(tls)/4 if m > 0: - mthbest = tls[m-1] - mthworst = tls[-m] + mthbest = tls[m-1][0] + mthworst = tls[-m][0] else: - mthbest = tls[0] - mthworst = tls[-1] + mthbest = tls[0][0] + mthworst = tls[-1][0] # The +/-0 index is the best/worst, the +/-1 index is the 2nd-best/worst, # etc, so we use mp1 to name it. @@ -196,26 +208,22 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=1.0, profile=False, profresults="pyutil-benchutil.prof"): if profile: - st = clock() - cProfile.run('func(n)', profresults) - sto = clock() + raise NotImplementedException() else: + iters = 0 st = clock() - func(n) + deadline = st + runtime sto = clock() + while sto < deadline: + func(n) + iters += 1 + sto = clock() timeelapsed = sto - st - if timeelapsed <= 0: - raise BadMeasure(timeelapsed) - global worstemptymeasure - emsta = clock() - do_nothing(2**32) - emstop = clock() - empty = emstop - emsta - if empty > worstemptymeasure: - worstemptymeasure = empty - return timeelapsed + if (timeelapsed <= 0) or (iters == 0): + raise BadMeasure((timeelapsed, iters)) + return (timeelapsed / iters, iters) def bench(func, initfunc=None, TOPXP=21, MAXREPS=5, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", outputjson=False, jsonresultsfname="pyutil-benchutil-results.json", UNITS_PER_SECOND=1): BSIZES = [] diff --git a/libs/pyutil/benchutil.py~ b/libs/pyutil/benchutil.py~ index 3ec323eb..a33111e8 100644 --- a/libs/pyutil/benchutil.py~ +++ b/libs/pyutil/benchutil.py~ @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -104,6 +104,24 @@ def makeg(func): func() return blah +def to_decimal(x): + """ + See if D(x) returns something. If instead it raises TypeError, x must have been a float, so convert it to Decimal by way of string. (In Python >= 2.7, D(x) does this automatically. + """ + try: + return D(x) + except TypeError: + return D("%0.54f" % (x,)) + +def mult(a, b): + """ + If we get TypeError from * (possibly because one is float and the other is Decimal), then promote them both to Decimal. + """ + try: + return a * b + except TypeError: + return to_decimal(a) * to_decimal(b) + def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME @@ -144,12 +162,12 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p # etc, so we use mp1 to name it. mp1 = m+1 res = { - 'worst': (worst*UNITS_PER_SECOND)/n, - 'best': (best*UNITS_PER_SECOND)/n, + 'worst': mult(worst, UNITS_PER_SECOND)/n, + 'best': mult(best, UNITS_PER_SECOND)/n, 'mp1': mp1, - 'mth-best': (mthbest*UNITS_PER_SECOND)/n, - 'mth-worst': (mthworst*UNITS_PER_SECOND)/n, - 'mean': (mean*UNITS_PER_SECOND)/n, + 'mth-best': mult(mthbest, UNITS_PER_SECOND)/n, + 'mth-worst': mult(mthworst, UNITS_PER_SECOND)/n, + 'mean': mult(mean, UNITS_PER_SECOND)/n, 'num': len(tls), } @@ -178,7 +196,10 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=0.1, profile=False, profresults="pyutil-benchutil.prof"): + """ + runtime is how many seconds to + """ if profile: st = clock() cProfile.run('func(n)', profresults) diff --git a/libs/pyutil/data/wordlist.txt b/libs/pyutil/data/wordlist.txt new file mode 100644 index 00000000..e1048b99 --- /dev/null +++ b/libs/pyutil/data/wordlist.txt @@ -0,0 +1,7248 @@ +fawn +yellow +four +prefix +payoff +scold +outwit +lore +lord +swivel +deli +pigment +foul +fur +disturb +prize +broiler +wooden +satchel +crotch +fritter +charter +tired +miller +bacon +second +tether +ruthless +thunder +fossil +succumb +cull +specialist +hero +avert +herb +splinter +here +herd +china +dogwood +cult +shriek +chink +pancreas +robin +neurologist +climber +diplomat +golden +gridiron +lengthen +summons +remnant +stern +unit +spoke +exhort +statesmanship +music +bedrock +passport +strike +teaspoon +relay +relax +hurt +meteorologist +glass +hurl +hole +hold +unpack +sweeten +blade +locker +locket +plunger +wand +wane +unjust +household +digit +malign +caution +want +rayon +hog +hoe +travel +copious +cutback +revisit +how +hot +hop +cheetah +diagram +possum +modest +antonym +pigtail +revolt +alias +decoy +wing +squint +wine +feedback +misdemeanor +kickoff +foodstuff +butcher +dreamer +fir +bowlder +fix +fib +fig +fin +undercut +enrich +slate +interrupt +sixteen +silver +scholar +thyme +seamstress +debut +arrow +debug +volcano +burial +whim +concord +knockout +garment +allah +spider +crocus +turnip +yiddish +fortnight +allay +whir +whip +diction +smirk +mason +semiconductor +re +adapt +outburst +knit +scruff +silicon +miaow +thumbtack +shopper +wasp +wash +instruct +rhododendron +tango +master +architect +bitter +listen +wisdom +swish +sulphur +crawl +trek +peril +outlay +coward +tree +shower +pneumonia +sheen +acclaim +entail +girder +runner +spectrum +headland +increment +quay +dozen +kidnap +gripe +hum +greenback +tipi +matriarch +stirrup +object +toil +microsecond +mouth +addict +letter +fluster +drought +thriller +expound +singer +upend +grove +professor +camp +detriment +nineteenth +scream +marvel +bomb +reactor +heckler +ulcer +caper +layout +menu +bust +cougar +bush +bliss +rich +mend +rice +plate +pocket +cushion +fetish +relish +jaguar +boarder +pretzel +patch +hasten +respond +fair +heirloom +radium +radius +result +fail +crouch +clef +best +irk +yogurt +ire +wage +extend +vestment +souvenir +extent +wheelbarrow +carbon +debt +roller +accident +trickster +veer +disdain +cup +logic +genus +rehash +gopher +canyon +bewilder +chrome +onomatopoeia +advert +grapefruit +stadium +jackass +counterattack +life +retrospect +spit +worker +wish +lift +toboggan +chile +child +chili +spin +wildcat +dissect +employ +calcium +delicatessen +locksmith +letdown +player +elicit +eighteen +violin +doorman +specter +hone +toaster +honk +rebellion +split +bid +european +typhoid +boiler +ownership +supper +tuna +tune +furlough +noblewoman +unhook +abound +bellow +beset +plight +brandish +previous +ham +hag +hay +prison +falter +east +hat +quirk +birth +shadow +gangplank +remind +pavement +battlefield +attorney +right +old +creek +crowd +creed +crown +glove +billboard +creep +chorus +okra +bottom +circumvent +inhuman +fox +foe +fog +binder +yoke +slither +recollect +despair +rebut +eightieth +sob +sod +overshadow +honeymoon +overgrow +sop +sow +wrap +fabric +panorama +support +tame +avail +width +hothead +call +overhand +overhang +telegraph +offer +thesaurus +beech +squalid +safeguard +otter +duel +misinform +paprika +vanguard +pest +duet +proud +tournament +proven +exist +quintuplet +dealer +leer +floor +glacier +actor +flood +role +entomologist +sunset +smell +leek +intend +glutton +ointment +asterisk +taurus +intent +cleaver +entrust +windscreen +puss +lowdown +time +push +gown +chain +viaduct +skate +chair +midst +millisecond +ballet +uneven +vex +crater +oversight +jerk +ameba +embark +flora +mourn +knapsack +southpaw +exact +epic +judaism +tear +teas +teat +crustacean +subway +team +skewer +prevent +meadow +gremlin +attic +sigh +milligram +heavyweight +crescent +playpen +crackpot +melt +current +boost +abscond +gnaw +splice +address +brilliant +endow +queue +influx +love +radish +prefer +piranha +fake +instal +forefront +sky +homesick +turret +wicker +wicket +scope +prosecutor +wicked +afford +refrain +visual +appendix +behalf +mascot +lumberjack +pretend +descriptor +dispossess +stole +winter +savor +sputter +meddler +slush +spot +textual +date +suck +dove +pulley +stress +conscious +bluster +wheelchair +quadrant +mango +so +skirmish +truce +drunken +archeologist +footstep +yearn +jig +disconnect +thumb +accordion +nearsighted +councillor +hubbub +suspicion +thump +apron +civilian +insomnia +nation +amulet +twilight +ketchup +handiwork +revert +fisherman +quarter +quartet +receipt +fireproof +breakthrough +sponsor +troll +naked +canvas +onrush +trauma +formula +dumbfound +million +envelop +vicious +disrespect +mime +plea +byte +workmanship +punk +wrong +ostrich +punt +footwear +neglect +gunshot +potter +one +reopen +chide +conifer +vote +paleontologist +languish +boulevard +wrath +convent +bite +extortion +shiver +draft +cite +starfish +shawl +artifact +snatch +antic +boyfriend +iceberg +rival +stammer +counselor +janitor +prospect +sac +greyhound +argument +alley +sad +say +borough +saw +handicraft +tulip +general +knead +zoo +note +take +destroy +printer +buffer +squalor +compress +buffet +crochet +knee +byway +lawn +enamel +blockhead +sale +cocoanut +wind +axe +salt +cobra +homespun +lotus +friction +bright +slot +slow +slop +unkind +gourd +transact +cloak +debunk +slog +hockey +slob +robe +clank +dissimilar +psychiatrist +clang +outlet +prime +artist +saliva +borrow +soloist +carrion +handcuff +primp +landlord +tortilla +where +xmas +vision +gout +gangster +cheesecloth +diver +bugler +mutton +plummet +bootleg +teacup +bureau +mope +vender +jumper +spars +screen +dome +supermarket +adept +jovial +spare +spark +quack +oust +fit +madcap +mane +flipper +backpack +twin +boar +supervisor +extinct +twig +boat +companionship +stretch +west +breath +reflex +gist +thousand +photon +cloudburst +turtleneck +former +jute +scarlet +straighten +spotlight +girth +brow +canon +dubious +monk +blab +fame +spunk +breakdown +hideaway +deft +barber +disown +booster +driftwood +veal +pewter +dimension +scholarship +summer +manifold +poach +disconcert +slime +rest +invalid +alarmist +mandolin +instrument +overthrow +stopwatch +haystack +joyful +sportsmanship +rejoin +dart +dark +brazier +snarl +traffic +cranium +vacuum +world +snare +dare +clan +clam +stranger +shutter +glamor +clay +claw +inter +kennel +clap +auditorium +obstruct +grub +potion +lobster +racial +endeavor +tote +tube +moslem +tuba +nook +exit +refer +zest +ration +leadership +standpoint +stone +ace +slender +meal +tumor +neighbor +act +mean +invert +braggart +homeless +wade +hypnotist +racquet +hew +burglar +her +gleam +glean +mindless +harpsichord +italic +hem +hen +defrost +epilog +pull +regimen +darken +wafer +rage +hooray +tripe +ruse +flirt +reprimand +whiz +torso +pulp +rust +ohm +gong +ad +fright +certain +epaulet +catchup +hoodlum +ay +ax +tranquil +jargon +slobber +cream +yoga +collector +abolish +tight +backgammon +congress +annex +slant +midget +brotherhood +slang +rostrum +neuter +thorn +groom +mask +kilogram +mash +mimic +mast +mass +ringworm +waiver +retch +gingham +influenza +consider +neigh +upkeep +taxicab +tinsel +to +tail +smile +norm +debit +baton +candid +salesperson +cobalt +strand +laud +pedant +sand +adjust +small +mammal +peon +ninetieth +plaid +past +burnish +gossip +canvass +healer +hick +offbeat +clock +section +succinct +method +contrast +full +hash +lobbyist +saleswoman +dramatist +backlash +brutal +prior +hamster +skyrocket +social +action +welder +raze +depart +vie +sherbet +regiment +captor +coercion +entrap +select +casket +enliven +petroleum +maltreat +pearl +sitter +morn +ballad +more +teen +teem +door +tester +signpost +nomad +doom +cunning +fatal +malt +chisel +patriarch +knocker +midstream +mall +learn +grope +male +stewardess +prompt +taunt +gallop +scab +accept +autumn +gallon +scar +rustler +condemn +huge +speedboat +fruition +cling +clink +plant +anoint +blotter +variant +unsound +plane +waver +flutter +pucker +wrench +trellis +patio +pant +instep +trade +paper +pang +brim +mislay +hearsay +buttercup +epoch +coarsen +bypass +motley +sucker +gadget +consign +imperil +skipper +harrow +nugget +fount +found +lantern +status +eyelash +clockwork +scribe +penicillin +lipstick +research +highway +bungler +belief +porcelain +bedlam +cockpit +loafer +suntan +acorn +riser +reproach +prefab +drivel +sicken +bumper +testament +clump +major +purport +limerick +number +feeder +slipper +footprint +florist +glitter +guess +guest +jet +swipe +vocalist +saint +gnash +relationship +tightwad +typhoon +mural +consult +grace +frock +getaway +vocal +video +defect +waft +pedestrian +graffito +caress +blond +gasket +sell +ballerina +ragamuffin +tarnish +spaghetti +self +trowel +poplar +brace +bobbin +kneecap +hypochondriac +blackboard +nasal +twine +raucous +virus +plan +wive +foyer +oyster +unequal +arson +covet +cover +barren +barrel +bulletin +chowder +golf +cruiser +affix +session +freight +impact +condor +writer +peculiar +condom +factor +downpour +dandelion +streamer +resent +actress +compass +banner +tumult +sojourn +caramel +enema +weaver +river +outlaw +prospectus +manger +set +creator +overwhelm +jade +sex +see +sea +contour +analog +project +urchin +fission +crossword +pickup +crosswalk +kneel +candor +mildew +hardship +disallow +incident +dividend +pagan +scatterbrain +lass +last +thou +opal +feminist +amoeba +lash +whole +load +loaf +electrician +pendulum +bell +loam +loan +hollow +scallop +church +psychoanalyst +underlay +napalm +airfield +devil +filth +imbed +proprietor +veneer +firm +sweetheart +champion +fire +infect +upstart +fund +deport +hostess +straight +budget +error +outskirt +real +pound +moth +vow +chasm +vanish +chase +starlight +seek +shorten +wasteland +specimen +commune +snail +teeter +cigar +epithet +alert +opinion +stack +recent +expend +clime +person +sixtieth +crayfish +telegram +aroma +belabor +amp +demerit +sandal +goblet +chest +eager +horseradish +homeland +wrongdoer +input +limp +cordon +format +bureaucrat +quest +cataclysm +blackjack +falcon +abduct +flannel +spine +consensus +crescendo +spring +beckon +palm +pall +sight +curious +sprint +battalion +pale +gruel +benefit +religion +be +odor +agreement +carol +by +scepter +coexist +hatchet +sacrament +ambush +biennial +repair +contributor +next +span +sock +submit +custom +spay +suit +spar +spat +blueprint +perplex +poster +lint +slump +pastor +overbear +link +atom +line +up +slander +foist +hornet +insignia +genial +aerial +nationalist +haunt +char +chap +chat +parsec +breather +phantom +paradox +tuft +uranium +scrape +parakeet +swirl +freighter +tart +tedium +scapegoat +trouser +scrub +gardenia +hackney +lane +land +fighter +algorithm +scotch +age +feud +summit +walker +fresh +crowbar +rescuer +hello +essay +code +partial +serviceman +scratch +broaden +totem +soften +leggin +renown +prim +flashback +young +send +moor +tremor +garret +armament +garden +quadruplet +llama +precinct +wipe +magic +harbor +eve +anxious +race +rack +mishap +crook +croon +odd +ode +victor +index +yock +sauerkraut +apparatus +indian +proffer +bird +inspect +leg +punch +acquit +let +fifteen +vinegar +great +casino +screech +scatter +survey +insulin +grandchild +buss +popcorn +mussel +maker +grower +sire +disobey +causal +zip +archbishop +theme +aeon +eleven +doubt +yardstick +midday +pencil +babe +shipwreck +patrol +rubbish +central +hoard +pout +pour +thin +drill +coffin +cherubim +bent +pawn +process +lock +slim +high +slit +bend +slip +pelvis +martyr +trumpet +weaken +rhubarb +delay +blackhead +luster +stow +halter +singular +await +wristwatch +notebook +tier +marrow +hawk +autograph +tomato +counter +robot +element +writ +allot +allow +alloy +thigh +mute +insight +spatula +comma +mutt +warren +perfect +decay +shudder +garnet +derelict +prosper +python +belch +bat +launder +dock +snake +kiss +bar +cage +wrangler +truth +scorch +subset +bump +static +thirteen +mete +jagged +disco +tenth +wander +matrix +bag +fatherland +venom +czar +oblong +lob +shut +perish +tempo +graze +tempt +shun +embarrass +minstrel +chilli +mainland +spill +length +stickler +scare +scarf +manuscript +scene +cobweb +owner +scent +prank +lop +opossum +sergeant +spaceship +painful +stomach +chagrin +vouch +rotunda +haven +steel +wet +bother +aggressor +psalm +disband +unman +steep +torrent +misunderstand +beggar +viewer +partnership +correspond +tonight +receptionist +fourteenth +mischief +depict +soak +bacterium +bassoon +hammer +adjunct +lilt +soap +soar +calculus +manor +raindrop +cipher +vise +segment +fervent +instil +locust +enlist +soprano +fiasco +brew +fact +bring +brine +bedroom +rough +asylum +trivial +brink +redirect +disillusion +planter +jay +jaw +jar +jam +tape +bourbon +flinch +hope +jackpot +move +familiar +scorn +sinus +wring +antagonist +smash +shaver +summon +stuff +rein +withstand +pronoun +packer +frame +packet +bellhop +airmail +dungeon +wire +mien +partisan +unravel +piston +pistol +email +browbeat +fetich +physicist +courtyard +lawsuit +tantrum +drum +quitter +ramp +drug +doorway +puff +roughen +medallion +revamp +migrant +distil +javelin +indict +chromium +lectern +mailman +gondola +quaver +blatant +feather +ballast +sheepish +crisscross +federalist +mannequin +altruism +banish +laser +runway +bathtub +maul +groin +ripe +lush +site +lust +mockingbird +tenor +passbook +ransom +tattoo +inquest +terrorist +buffoon +outbreak +android +balm +ball +balk +dusk +fiesta +bale +bald +dust +broccoli +mosaic +audit +off +shotgun +polyp +command +diphtheria +audio +maggot +compel +glut +glue +rambler +web +generous +clergyman +wee +wed +arrest +crack +scoundrel +government +chancellor +crux +haul +cedar +desk +password +recurs +placenta +crisp +onion +resin +alkali +stagger +imprison +nymph +sprain +overcast +foray +habitat +thief +daylight +flush +wisecrack +ballot +transport +henchman +disbelief +hoarder +avoid +disk +doer +passion +saucepan +stairway +putt +drift +ornithologist +stage +iris +sister +adverb +peal +ingest +union +artefact +parsley +assess +lung +mere +muck +commission +caviar +watchman +stamina +much +function +funnel +cosmopolitan +frisk +shyness +grate +rectum +count +congresswoman +smooth +monument +problem +baptism +cordial +kowtow +sirloin +retina +inn +replica +ink +anesthesia +furl +sexual +saviour +behold +reckless +chum +monday +repeal +veil +vein +ghost +eon +rule +dynamo +torrid +pension +tryout +abhor +buoy +inning +dote +rapid +mansion +defraud +voter +spew +bludgeon +bike +daze +regal +chill +regalia +whack +ale +compassion +blanket +distort +mania +chauvinist +chapel +whisk +daydream +pinch +scalar +handout +roadblock +unblock +math +triumph +chew +paperback +phoney +speck +heliport +sabbath +horn +chef +aristocrat +panda +stardom +lizard +walkout +toll +crunch +dustpan +pursuit +paraffin +sorceress +hairdo +daughter +envoy +adopt +tankard +smoke +loincloth +bunker +anarchist +envious +sanatorium +infield +spigot +thrust +hindsight +total +bra +plot +plow +plop +sweater +gloss +ploy +insult +plod +knoll +beeswax +solicit +award +yard +tariff +overrun +word +err +crest +work +grovel +tinder +era +elbow +spendthrift +quiver +serpent +flunk +impair +liter +chameleon +sever +moron +disappoint +beach +pizza +fever +lad +ladder +lag +lab +lay +law +arch +cosmonaut +retort +greet +greek +green +south +worst +order +greed +salon +gumption +devote +muffin +misconduct +mayor +sheaf +avocado +valor +carton +shear +then +fragment +safe +break +band +bang +coffer +overprint +tzar +bank +bread +crock +gallows +lisp +iguana +schemer +transient +prawn +sled +flock +slew +hostel +burlap +network +diesel +fellowship +amethyst +marigold +barrier +veto +standard +stencil +lollipop +morass +drench +ticket +maniac +raisin +flawless +renew +sprig +regress +vanquish +kin +render +system +hamstring +chopper +disembark +comic +overs +neck +upshot +tourniquet +kiwi +emblem +luncheon +cereal +rebuff +minibus +guild +target +tavern +hike +medley +iota +guilt +iron +minus +pessimist +lull +ponder +strength +realm +widen +silversmith +latter +hamper +transmit +curfew +maiden +boxcar +sue +negro +phase +proverb +grave +deacon +swamp +bracket +aunt +rickshaw +oppress +mitten +crust +boyhood +nephew +toast +geyser +layman +geologist +predecessor +do +ardor +ecologist +roundabout +slack +rebirth +runt +rune +rung +crucifixion +steak +steal +steam +ghoul +reread +misdirect +christian +goulash +pastel +gentleman +cellist +contraband +drawl +accord +unfold +kitchen +cop +cot +cow +brat +excrement +ill +cob +brag +cod +cog +bran +coo +con +emporium +eyesight +tone +spear +royal +trunk +nonconformist +infirm +speak +charisma +scarecrow +warmth +leech +baud +hacksaw +millionth +hoist +spellbind +gracious +physician +inhibit +gnu +launcher +air +aim +ail +abrupt +thrash +aid +stink +have +sticker +sting +throat +brake +cone +hebrew +uplift +stint +descent +perform +descend +decibel +wheel +raid +fuss +nil +swell +hang +evil +hand +fuse +nip +nit +scenario +drip +ragged +client +mamma +kinship +indigo +photo +victim +extol +thyroid +exalt +shout +cognac +board +zillion +righteous +plasma +intercom +fusion +boxer +cape +retreat +cooler +night +flatter +born +rile +flatten +bore +orchid +cede +humor +peek +peel +pose +confer +peer +peep +chafe +foreskin +chaff +diner +coral +visa +banker +horizon +cherish +gingerbread +octopus +croak +faint +dilemma +tetanus +float +profession +bound +curios +sedan +loin +beet +piggyback +wag +bookend +wad +frill +sovereign +fight +gybe +way +wax +burro +war +fizz +peninsula +holdup +boon +true +reset +absent +nursemaid +smidgeon +maximum +crystal +veterinarian +emir +emit +aorta +flat +abstract +molt +flaw +postscript +subsist +prayer +cacao +face +mold +mole +stake +shrine +test +upholster +unwilling +frolic +shrink +heyday +chairperson +hemophilia +faze +affidavit +loyal +longshoreman +igloo +concept +matron +consul +fulcrum +datum +horseback +supplement +toothpick +varnish +grape +zone +mallet +flask +graph +hump +flash +manicurist +glad +rhythm +tusk +terror +idealist +southwest +brown +congest +kitten +blast +brows +ophthalmologist +gun +gum +gut +guy +diarrhoea +upper +brave +regret +bravo +thinker +cost +helpless +tempest +cargo +appear +economist +menthol +medal +havoc +uniform +tarantula +appeal +caveat +genes +gawk +jester +disclaim +goldfish +teacher +buck +merriment +fogey +precursor +plotter +eke +disavow +trial +convertor +pillow +bolt +extra +paragon +keeper +marker +firearm +market +streetcar +prove +subvert +live +matador +club +cluck +clue +logarithm +prepay +graphic +slogan +car +cap +caw +cat +meow +can +cab +heart +hears +chip +sake +bridesmaid +abort +chin +chic +serum +bankrupt +freezer +write +lobe +storeroom +criterion +entrench +afternoon +product +dive +southern +bawl +motorway +pave +drastic +flourish +crepe +grandson +explicit +offend +barnyard +forfeit +haircut +ledger +brain +nitrogen +cold +braid +ethic +willow +theorem +window +artisan +factual +tiara +gizzard +nought +halt +fling +nod +rake +overcrowd +dishearten +hale +half +recap +courtship +taillight +provision +discuss +halo +wont +concerto +servant +drop +domain +supplant +year +operand +wavelength +happen +album +accomplish +space +thirst +rational +thong +carp +cart +virtuoso +quart +rebel +marina +prospector +card +care +fungus +tomahawk +british +honest +nonprofit +profess +blind +madam +blink +rink +rind +ring +drove +tomorrow +size +sheep +sheer +sheet +silent +bookmark +breed +callous +traction +checker +tragic +heartburn +friend +pomp +courier +that +peck +scalpel +rugged +recruit +magnesium +optimist +extinguish +angel +slay +slat +premier +slap +racetrack +slam +anger +breakfast +recover +slab +upbeat +veteran +shore +snout +siesta +begin +prick +halibut +price +foothold +dream +tooth +aerosol +washcloth +fifth +ground +gnat +snack +ratio +stair +proportion +jolt +stain +juror +shrill +pumpkin +cannon +loath +stroll +leather +thermal +husband +druggist +concert +burst +spore +whitewash +unfit +staunch +sport +incisor +concern +crawfish +glaze +complexion +import +clench +pluck +blame +broil +impromptu +whisker +guffaw +pertain +priestess +temper +aura +comet +evict +adroit +dispatch +exploit +semicolon +lioness +harmless +rebuild +toss +sisterhood +textbook +bloodhound +crumb +these +trick +scum +cherub +fool +marksman +zenith +mucus +soil +agnostic +laggard +bias +eras +bestial +beaver +waterway +petunia +helium +develop +media +pester +poetic +document +sweeper +finish +thunderbolt +foal +foam +cymbal +fruit +volley +trawler +smelt +quartz +theater +framework +patchwork +demean +taxi +livestock +fester +battleship +typhus +neon +touch +speed +death +refurbish +treatment +baloney +momentum +lade +ream +hover +frown +spectacular +larva +read +ruler +swig +leapfrog +earnest +detract +stunt +execution +reap +hovel +rear +postcard +incest +roll +engross +oblivion +output +downward +falsehood +laugh +verbal +landslid +squirm +garland +putter +cleanser +deficit +squirt +wheat +sadden +throb +sixth +tuition +strainer +bazaar +throw +comparison +placard +hiatus +chop +fell +wolf +parson +chow +ruff +assassin +processor +heater +outlook +earring +watchdog +your +restless +stare +grater +log +area +start +stealth +low +lot +wigwam +groan +pitcher +rump +posterior +recoil +omelet +lymph +thaw +corduroy +wholes +hire +fraud +default +bucket +draftsman +cornmeal +gibberish +charlatan +scanner +pickax +sheath +mover +antenna +housework +valid +colt +you +houseboat +poor +polar +poop +peat +pear +peas +overreact +podium +peak +pool +fiscal +assert +moonlight +repay +forswear +mourner +skeleton +breadth +groundwork +angler +month +unrest +bequeath +carpet +corps +gymnast +foster +spearhead +fervor +fountain +washroom +horror +verb +minded +heaven +ceaseless +saboteur +tendril +blackout +smock +homonym +resound +exodus +casual +bomber +protractor +lurch +milk +turmoil +vet +excess +strong +arena +divisor +noticeboard +outgrowth +vegetarian +whine +soldier +amount +base +trainer +put +haemoglobin +seventeenth +taker +helper +pup +titbit +assessor +chestnut +yuck +suction +grill +nine +parasol +transcend +pusher +boycott +archipelago +tract +phrase +magenta +frostbit +sheikh +warhead +spreadsheet +lyre +snippet +reject +gash +circuit +rude +sneak +denial +gasp +reelect +undergo +figment +overnight +ego +dread +egg +lynch +earthworm +help +reservoir +slouch +auburn +reclaim +preempt +soot +helm +hell +clarinet +limelight +prowler +lateral +heron +astonish +forbear +dim +food +musket +terrain +vomit +foot +stopper +holiday +payer +twelfth +bless +radial +trailer +pamper +thirteenth +talisman +event +magnet +vertigo +wedlock +teak +publish +eardrum +sustain +shrivel +outrun +ass +pun +drink +bass +dirt +dung +dune +cornstarch +reason +heroin +ask +ash +turnout +bask +bash +pus +launch +curtsey +round +bridegroom +caption +liaison +heartbeat +blush +assign +arsenal +demagog +elder +effortless +mist +miss +blossom +minion +station +expand +fingernail +scheme +banana +merciless +slosh +behind +bowel +trapper +boredom +sign +leotard +tunic +lament +ouster +hyaena +bride +bobsled +currant +toward +weapon +phobia +chipmunk +yowl +null +sensual +lid +lie +koala +cave +lip +useless +honeycomb +popular +quota +plunder +mace +watt +smoulder +clear +cleat +succor +clean +skein +blend +humid +phenomenon +cowgirl +flicker +sheik +crayon +copyright +paranoid +scooter +less +ikon +ampersand +waterproof +custodian +outlast +strut +strum +basement +chimney +monogram +fluff +chasten +geld +courteous +cramp +backtrack +grey +close +despatch +bandwagon +haddock +aqueduct +wow +grocer +won +woodchuck +wok +woe +stalk +bettor +wreath +philanthropist +spray +distinguish +zipper +garrison +delimit +eggplant +buzz +vault +protector +mausoleum +onward +oversleep +liken +proton +header +badminton +vessel +catwalk +stamp +damp +nape +damn +threaten +dame +alto +liven +exempt +deter +liver +hobnob +furrow +pact +loom +utmost +look +socialist +governor +rope +pace +while +smart +fleet +loot +loop +pack +petal +hoax +grant +belong +makeshift +discredit +grand +conflict +sham +hallway +optic +dime +bonus +banter +overweight +user +boa +grind +auditor +five +ambassador +chore +abstain +bearer +morsel +tick +botch +pier +carat +march +albino +game +jibe +banjo +optician +signal +manifest +eel +sleigh +sketch +creation +undress +yolk +urgent +impoverish +mustang +clothespin +fundamentalist +gild +simmer +slash +slapstick +run +rum +rub +booklet +benefactor +rug +stem +step +stew +taboo +subtract +rut +discus +shine +faith +pigpen +letup +portico +reappear +aye +congressman +block +foreswear +misbehavior +dude +within +gilt +pentagon +connector +syllabus +palomino +harem +frost +reed +womankind +reef +reek +reel +dull +skulk +swagger +chiropractor +ringlet +foresight +similar +psychopath +kidney +straitjacket +nab +sullen +nag +objection +obelisk +nap +department +nay +draw +resign +drag +tundra +drab +formal +horseplay +outing +orbit +depth +bribe +pinion +underbrush +cheerful +go +emboss +dunk +ammonia +compact +aquarium +baron +aria +stave +ameer +shack +geranium +warpath +epitaph +velour +schooner +virtuous +fond +wave +trough +cellular +tenet +nausea +stiff +asparagus +gender +button +hive +verdict +cloister +pilfer +picket +blitz +jump +booth +ardent +languor +cartel +click +poke +wallet +colonel +valet +cell +rotten +experiment +stanch +brooch +fifteenth +quell +weirdo +convert +chant +gent +repel +behead +ricksha +wig +daybreak +danger +win +clout +wit +ligament +infest +gimmick +wiz +cloud +metaphor +snoop +copperhead +crag +crab +cram +expressway +compatriot +mismatch +starter +salad +consort +ride +donut +archer +meet +control +wharf +halloween +glade +skirt +bandana +filament +circular +fare +farm +thunderstorm +canker +foment +corral +scoop +encyclopedia +scoot +agenda +american +cadet +sperm +gunman +hock +brood +broom +brook +walnut +youngster +skater +frond +auto +dike +relentless +snorkel +placid +stout +hands +front +refuel +muff +perfectionist +mode +upward +commonwealth +unwind +chunk +mollusc +seesaw +apartheid +mollusk +special +gallant +armor +confess +fathom +remiss +activist +playground +wick +obsess +umbrella +hopscotch +watermark +jilt +undo +advisor +sneer +princess +shrew +timer +keep +counterpart +elector +keel +mad +blogger +seventeen +bitch +drinker +equinox +dump +wrapper +chintz +attach +attack +jellyfish +final +beard +introvert +punish +feint +noun +plough +piecework +waist +photograph +spurn +cartoonist +beg +bed +bee +discolor +swindler +firework +spurt +bet +are +exhibit +fume +tabu +torment +sundown +portrait +need +border +rotor +bastard +sprinkler +gunner +jaunt +tactic +truck +detector +visor +brand +african +camper +rigor +awe +plumber +eject +spleen +urn +upset +snapshot +businessman +constrain +skunk +affair +indoor +crate +molest +cavort +sheriff +fiberglass +winner +wreak +rash +earner +rasp +gradual +fuel +sulfur +joint +fallout +buyer +endless +gray +tobacco +gust +ordain +topaz +nutmeg +she +contain +recoup +grab +conduct +widow +hardwood +shake +orphan +portend +computer +driveway +equip +portent +unearth +southward +tend +state +lug +tens +antler +tent +bleed +castoff +blinker +keg +bemoan +key +overhaul +thank +sniff +career +admit +spatter +plankton +jersey +christen +tuesday +poem +sari +tread +shaikh +yap +cent +quiz +yam +treat +yak +whisper +poet +fibber +spaniel +nuptial +brunch +debtor +novel +ripen +pandemonium +harden +neuron +hearten +steer +generic +balloon +speaker +northwest +blight +fireman +flesh +absorb +powwow +inbreed +spree +magician +rift +weld +surfboard +lunch +glower +well +drone +welt +underdog +discord +mistaken +dose +distant +laurel +skill +cinder +jackal +dais +ovum +snicker +stratum +possess +warrant +homework +canter +rafter +fate +burden +loss +clown +tablespoons +lose +divest +satirist +rote +page +backlog +shed +glare +twitter +hush +redress +home +peter +competitor +pinpoint +overlay +broad +overlap +hinder +individualist +journal +offset +instinct +smidgen +refuge +freedom +cleans +nightclub +rodeo +dominion +wallop +buzzard +cocoa +pointless +gerbil +snowplow +mastermind +museum +poinsettia +drumstick +mohair +jinx +backhand +cricket +north +gait +admonish +neutral +ho +technician +overflow +ear +eat +he +leper +limit +cello +display +wringer +twist +entreat +contest +meteor +finch +chemist +fodder +star +stay +stag +foil +stab +phosphorus +appoint +sphinx +shunt +broncho +atheist +atheism +portion +pardon +mackerel +demand +unfurl +protest +asian +captain +gamut +swab +swan +sinew +swat +swap +anticlimax +sway +loon +appal +void +vase +smack +govern +affect +hitch +vast +pilgrim +naturalist +vector +washout +whirlwind +quilt +crave +yack +cactus +quill +pander +wreck +orchestra +bikini +spokesman +haze +new +net +maverick +seventh +mew +onus +cardboard +interpret +taper +credit +harass +jamb +permit +prolog +menial +hunch +campaign +bayonet +moral +handlebar +overhear +ore +overheat +calk +overhead +calm +intrust +recommend +type +tell +calf +demon +wart +warp +warn +dogma +warm +pecan +adult +qualm +flotilla +ward +blindfold +confound +rook +room +candlestick +worth +bungalow +headway +root +defer +vodka +give +climax +assent +honey +surveyor +quail +freshen +polio +rib +stockyard +answer +abdomen +plank +coup +fracas +passageway +waterfront +lesbian +guerilla +attempt +third +maintain +capitalist +fetus +deck +keyboard +windshield +furlong +harmonica +crew +better +persist +pass +workout +microfilm +caterpillar +grammar +meat +mistrust +roast +side +bone +luck +caustic +aids +dawn +extract +jell +contend +velvet +gradient +open +crucial +content +reader +linear +whiff +bestow +mistress +needlework +steward +fleck +loud +skinflint +playwright +grade +hoop +hoot +buttock +hook +ditch +hoof +hood +hydrant +acquaint +spinach +historian +enthral +woodwind +brainwash +dwell +inferno +twister +gym +somewhat +gambler +symptom +preacher +affront +keyword +matter +loiter +mink +seep +quench +modern +mind +mine +ginger +seed +seem +churn +mint +unfasten +alibi +desist +chess +sleeper +quarterback +phonograph +chatterbox +regular +condominium +blacklist +don +alarm +impostor +dog +doe +solemn +digress +constrict +consumer +dot +hunger +visitor +probe +syntax +chord +sundial +northeast +explain +jailer +sugar +folder +inventor +edict +patter +smut +stop +coast +pincushion +watermelon +smug +earl +earn +peacock +bay +reload +bad +troop +cower +ban +stinger +linguist +enchant +attest +subject +snuff +scrap +sail +causeway +scram +baboon +warrior +triplet +vitamin +lotion +orthodontist +beautician +cousin +motto +sprinter +pate +typist +height +gusher +aftermath +arsenic +ether +accustom +tint +recur +three +erect +ting +chrysanthemum +trigger +interest +basic +basil +basin +idol +chug +mushroom +suppress +dismiss +deepen +encyclopaedia +unpopular +tank +affirm +tang +near +moratorium +neat +motorist +anchor +spawn +seven +cane +diaphragm +it +shame +jest +in +id +disappear +if +abscess +growl +sap +parish +make +kit +delight +squat +garlic +warden +unicorn +jealous +overt +bequest +kid +butter +romp +smoker +inherit +bedspread +diphthong +left +protocol +just +yen +unfair +psych +human +yes +cretin +yew +legion +character +wretch +save +opt +discreet +background +shoulder +nude +manual +pillar +dean +squander +deal +deaf +maxim +dead +revel +intern +dear +strife +sprawl +pail +collect +normal +councilor +flounder +bullfight +tartar +bold +statistician +burn +blackmail +cottontail +sift +protagonist +burp +burr +tartan +super +innuendo +crucifix +craftsman +commit +marshal +unsay +paunch +chimp +down +lieu +chime +initial +lampoon +editor +fraction +unseat +fork +undergrowth +form +fore +ford +diaper +overburden +analyst +fort +pavilion +whiskey +boomerang +cosmos +propaganda +shin +disciplinarian +classic +covert +sidestep +drive +gland +scrawl +fatherhood +ship +graft +vista +marxist +smidgin +excel +handed +venison +congeal +marxism +sling +faction +handicap +slink +felt +diet +parenthood +journey +reign +stoke +weekend +derail +billion +potato +jacket +gorilla +almanac +teeth +meander +befriend +proletariat +woodwork +skip +skit +invent +adjourn +mild +mile +skim +skin +mill +primer +proletarian +skid +surplus +seasick +misread +depend +swoon +father +countdown +deject +swoop +regatta +unburden +string +yeast +pathologist +merit +join +jettison +stiffen +hoorah +din +stapler +nectar +die +dig +democrat +noiseless +item +dip +blur +shave +thresher +villa +worm +slake +sunup +talker +fillet +suspect +drunkard +shoo +dwarf +dweller +wail +guardian +clerk +makeup +stallion +waif +detest +tangent +deceit +rue +wait +box +boy +cuckoo +shift +bow +dither +boo +raccoon +cyst +bob +nylon +bog +elect +plumb +kayak +surmount +transplant +saki +wealth +perk +visit +vineyard +somersault +sharpen +yoghurt +aspirin +labyrinth +curriculum +downtown +tandem +rigid +savior +effort +gnome +demolish +pageant +moccasin +melodrama +flu +soul +impel +soup +sour +claim +plaza +reflector +predict +agent +drawer +council +craze +pink +purr +arbor +tilt +clever +parch +pine +till +sunday +sword +tile +pathway +pint +map +mar +mat +may +gelatin +membership +mankind +tablecloth +grow +man +relinquish +aimless +hemlock +omen +tale +switch +jail +deposit +talc +unleash +basket +longhand +talk +shield +rabbi +moralist +lyric +pitch +solder +pointer +group +monitor +bedbug +maid +drummer +maim +mail +main +tonic +killer +shatter +minuet +safari +teller +rattler +outweigh +feverish +peasant +careless +rock +hijack +eyelid +latin +bookshop +poker +gavel +unlock +manifesto +girl +stitch +monolog +priest +dutch +blubber +sensor +correct +monster +zombi +vinyl +jaywalk +cough +underwear +waiter +buzzer +thing +registrar +blacksmith +think +frequent +first +lone +crib +long +extrovert +thoughtless +lap +autocrat +escort +daunt +mermaid +anus +yoghourt +memo +broadcast +butt +proofread +tractor +coconut +lick +piccolo +marijuana +dash +comedian +sulk +nazi +sherbert +stopgap +daredevil +acumen +squad +interior +channel +pain +trace +roster +track +acrid +zigzag +whizz +assault +billow +pair +synonym +napkin +typeset +scowl +voodoo +toucan +amir +shop +lexicon +shot +show +cornea +veranda +therapist +shoe +threshold +corner +label +cornet +enthusiast +fend +objector +flapjack +dice +plume +enough +syphon +black +consent +enthusiasm +fiendish +plump +get +straggler +stomp +midriff +slyness +gee +gibber +neckerchief +gem +disinherit +beseech +skull +businesswoman +yield +stupid +nostril +tallow +kernel +sear +eighteenth +seat +seam +seal +stigma +calendar +wonder +puma +parent +limber +ornament +forecast +gage +pump +august +foreword +slingshot +tacit +wednesday +gauntlet +childbirth +tug +tuck +trader +tour +tout +delirium +stretcher +cancer +spank +cancel +tub +mare +underworld +imp +undershirt +mark +mart +workshop +rancher +fiftieth +chalet +graveyard +squash +wake +sound +litterbug +epidermis +slumber +cock +strait +strain +sudden +protein +par +pat +harsh +paw +pay +woodland +same +heartbreak +pad +cotton +pal +pan +exhaust +oil +chloroform +munch +companion +foghorn +polygon +drain +vertebra +soundproof +outdoor +suitor +money +imprint +leeway +aspect +flavor +asthma +godchild +comradeship +forgo +pile +pill +grip +grit +mop +mow +moo +mom +mob +railroad +grim +grin +oxygen +server +chamber +nose +hallelujah +fulfil +sneaker +afflict +witchcraft +ascend +dole +ascent +spasm +gross +confirm +pioneer +inject +gladden +highbrow +linoleum +intravenous +knife +raincoat +broker +squall +bravado +racoon +opium +contagion +roar +island +insect +mixer +thrive +partizan +road +checkup +dagger +coupon +splint +empress +whiten +strip +uptown +skillet +paraphernalia +jigsaw +totalitarian +madden +tycoon +tripod +striker +shroud +hiccup +gore +spice +ember +magnolia +grouch +conqueror +embed +deadlock +affection +deer +deep +fellow +planetarium +deem +file +girlfriend +deed +hound +film +fill +tedious +selfish +personnel +hybrid +repent +drouth +field +prism +astronaut +fruitless +lapel +shelter +gander +unload +burrow +god +gangway +oral +motel +represent +forger +pheasant +forget +founder +suburban +dollar +rebind +zinc +implement +crimson +hideous +premium +parcel +straightforward +scout +scour +fall +bottleneck +pueblo +hinterland +dampen +dictatorship +flyover +neighborhood +clinch +gnarl +burger +zero +cottonwood +lawyer +further +misrepresent +ribbon +dial +skeptic +stool +trinket +stoop +plush +movement +girlhood +malaria +intrench +twang +mule +ranger +beacon +bigamist +capacitor +search +stupor +margin +airport +chipper +chieftain +narrow +fatten +quotient +wizard +caravan +transit +sadist +sadism +establish +dachshund +hobgoblin +eye +score +distinct +two +splash +libel +furor +wiper +diamond +brisk +opportunist +particular +disfavor +nineteen +town +hour +cluster +fast +dew +remain +paragraph +den +abandon +stubborn +shark +buttress +onslaught +share +sphere +minimum +rainstorm +attain +junket +sharp +botanist +siren +awkward +comfort +rapport +stir +bleat +whopper +blacken +blood +bloom +chute +coax +orchard +coat +doctor +spiteful +electron +blunder +mislead +coal +sect +infant +setback +radar +dough +lava +suffer +hundredth +sodium +bosom +late +speech +clamor +lath +lookout +goof +good +goon +detour +frigid +compound +detach +complain +bombard +headroom +countersign +token +monsoon +clamp +harm +hark +mental +hare +hard +beret +banquet +connect +fist +callus +hart +orient +harp +flower +creditor +trooper +pigeon +seaport +granola +print +foreground +assist +cockroach +pleasant +gig +faucet +prophet +omit +wither +pure +corkscrew +copper +perturb +barbarian +shoal +cups +jabber +razor +construct +paint +leash +statement +mama +hummingbird +catapult +pare +park +selector +glycerin +dentist +part +pars +youth +totter +plead +hangout +cistern +blanch +mountain +cardigan +couch +onset +build +zucchini +flute +salmon +chart +most +charm +moss +eskimo +organist +humanitarian +mammoth +pennant +squelch +weigh +standoff +sector +sparrow +fine +find +giant +merger +nervous +ruin +fiend +boulder +prowess +paperweight +cholera +express +ferret +cheapen +batter +breast +theft +silk +pellet +restart +silo +huff +common +archaeologist +printout +vine +lion +overeat +tender +expert +burner +myriad +stowaway +subscript +hypochondria +premiss +egotist +complement +figurehead +mailbox +pagoda +aircraft +sultan +archway +annual +foreign +point +smother +newborn +pamphlet +dancer +esophagus +platinum +pocketbook +secret +amnesia +reformat +finalist +ram +gay +gas +gap +holler +gal +understand +gag +chatter +gab +bile +politician +metro +solid +bill +holocaust +crutch +fun +lingo +manner +mystic +astound +rancor +eczema +ingrain +anaesthesia +sociologist +dishonor +ewe +seminar +corridor +neutron +itch +leopard +yesterday +moment +stripe +unveil +timid +task +werewolf +withdraw +landmark +grid +recant +spend +howl +darn +shape +snot +timber +rundown +impetus +cut +cur +pollster +snag +forbid +cue +punter +cub +snap +bridal +easter +brainstorm +bin +squawk +rebound +bib +judgement +redeem +bit +knock +disrepair +blemish +flue +fagot +flux +bamboo +foolish +walrus +sequin +transgress +often +back +impeach +extremist +mirror +lightning +scald +scale +culprit +pet +pelt +pew +pep +pen +scalp +lard +lark +peg +pea +patient +fed +megaton +constraint +oatmeal +drama +catnip +pediatrician +offshoot +obstetrician +gambit +maelstrom +tiff +clack +lesson +jockey +few +doll +errand +camera +handbook +forward +nougat +sideshow +showman +switchboard +calico +lifeguard +planet +jumbo +azalea +constant +flow +possessor +lye +curd +cure +curb +curl +prevail +stagecoach +leaflet +crypt +underweight +cellar +lend +tablespoon +papa +lens +cater +desert +statesman +mantel +notion +uterus +anguish +caribou +stroller +seaman +golfer +strew +parrakeet +peanut +welter +mower +rudder +compost +blaze +atlas +gravel +queen +dessert +rhyme +claustrophobia +surgeon +molar +verandah +knight +shock +crow +queer +crop +append +power +junior +anthem +access +clipboard +bachelor +intercept +sink +sing +roof +bode +implicit +remark +talent +conceit +resurrect +weekday +climb +honor +blizzard +liqueur +talon +oval +scandal +gateway +sermon +lime +patron +asteroid +butler +charcoal +trait +kiosk +thatch +trail +train +armadillo +harvest +fan +account +tunnel +carrot +obvious +smear +parkway +unread +fetch +employe +truism +sanitarium +teamster +boney +spruce +serial +contempt +hangar +lamb +lame +lamp +forest +goner +stock +roam +leukemia +bluff +terrier +fray +drape +bind +guru +liner +linen +chief +poacher +furious +furnish +disarm +meter +bunch +marshmallow +decorum +labor +kindergarten +heroism +willing +marsh +dad +junction +dab +dam +spell +swordfish +mention +courtroom +sonata +day +strive +flail +snowdrift +thrill +slacken +cider +memoir +sawdust +disregard +flair +thwart +jailor +jugular +pivot +cupboard +lentil +salesman +hippopotamus +matt +defend +rev +repress +stub +mate +barley +stud +smog +stun +red +franc +frank +hanker +fourteen +salami +likelihood +afterward +squadron +indent +mortar +skateboard +yarn +mortal +workbook +retain +retail +waitress +suffix +overshoot +ethnic +sack +brute +whoop +puppet +guidebook +vandal +pauper +ancient +monkey +bologna +laps +vulgar +vagina +hexagon +scant +liquor +cabin +sixteenth +gear +eavesdrop +bulldog +smolder +forethought +springboard +nun +bodyguard +prune +shrapnel +shampoo +linchpin +lover +anthropologist +tide +cavern +pedlar +countryman +waken +optimum +mix +parka +spinster +meek +dryness +hazel +eight +clamber +handbag +hoodwink +transcript +payment +gullet +gulley +gather +request +absurd +rendezvous +occasion +thicken +recess +kite +text +hamlet +traitor +industrialist +sidetrack +portfolio +floodlight +thicket +staff +communism +scorpion +madman +prolong +resubmit +satan +oppressor +communist +inferior +equilibrium +gumdrop +starch +beat +rush +bear +beam +bean +october +beak +bead +organ +ashtray +nutriment +eyebrow +motherhood +mascara +conform +showdown +infidel +racket +interview +reform +pattern +nebula +brunt +hammock +progress +tailspin +sorrow +stratagem +deliver +blackbird +boloney +exclaim +instant +joke +equal +kosher +swim +swallow +highland +guerrilla +glorious +wear +comment +vent +denim +overcoat +commend +vend +harpoon +manhood +citizenship +copier +newscast +gaze +teamwork +gulch +curtain +curtail +hyacinth +juggler +censor +goddess +bulk +bull +bulb +skew +carburetor +cypher +plain +homey +bray +kinfolk +bicker +dissent +squid +blimp +creak +prose +partner +inspector +lynchpin +portray +whirl +grinder +matchbook +defiant +anorak +tumbler +infer +whirr +tighten +pockmark +sauna +ion +grandstand +sunburn +judgment +retard +center +builder +pickpocket +thought +starboard +usual +coaster +humdrum +fingerprint +storey +clinic +interim +surpass +tough +earshot +flashlight +tong +flee +lupin +lake +bench +add +citizen +ado +crossroad +ravel +match +raven +cantaloup +punctual +newsstand +dryer +insert +flamingo +like +success +sofa +journalist +heed +arraign +chick +soft +heel +outfield +propel +fuze +hail +hair +convey +proper +paddock +novelist +shrug +shrub +slide +tureen +regain +pepper +hose +slight +host +panel +beaker +actual +socket +flake +preen +toadstool +pickaback +discard +tomb +tome +snitch +chronic +guard +esteem +custard +underpass +glimmer +gene +maze +globe +buy +bus +coke +sequel +but +bun +bum +bug +bud +embargo +woodsman +wise +ecosystem +debrief +flip +wisp +wist +trapezoid +condiment +plutonium +pin +garter +domino +circus +pie +pig +pit +campus +gush +oaf +cashew +oak +detail +virtual +detain +sewer +oar +redden +dresser +wallow +nutrient +godsend +yelp +baker +jab +hiker +pupil +yell +cookbook +vermin +sleek +sleet +sleep +liar +hate +trolley +sallow +tweet +glider +under +tweed +pride +merchant +lure +risk +rise +lurk +jack +confetti +anemia +school +parrot +enjoy +overdo +cracker +almond +direct +nail +street +monorail +ransack +blue +hide +worsen +poison +beater +supplier +dashboard +wink +even +pontoon +studio +path +crossbow +enrol +connoisseur +forum +ravish +auction +settler +mentor +midway +blowtorch +stray +straw +strap +cassino +would +phlegm +bayou +asset +spike +preview +musk +mush +saber +muse +grief +phone +muss +pouch +must +shoot +hutch +ma +ms +mr +machinist +fortress +quarrel +loosen +joyous +hemoglobin +dolphin +mayhem +attract +end +trill +keen +bunk +vagrant +rhinoceros +shred +toxin +gate +ancestor +dialect +moisten +kilowatt +mess +lump +mesh +sparkler +parallel +stronghold +splendid +spout +patent +enter +vapor +hedgehog +fetter +deform +clapper +sprout +over +bleach +mallard +oven +caster +digest +forehead +theologian +womanhood +comprehend +tramp +drawback +fade +croquet +tourist +plaster +roost +knighthood +monarch +rental +gloom +chuck +choir +prohibit +hanger +unscrew +gymnasium +poncho +truant +saturday +depress +goo +lair +dinosaur +nitwit +psychic +tonsil +gob +emphysema +nite +washer +resistor +carcass +rail +free +rain +acrobat +fret +harpist +ritual +filter +hopeless +soda +rang +accent +puck +rank +restrict +rant +sober +toy +their +sarcasm +top +tow +tot +fiction +ton +duress +toe +urban +murder +overdraw +tool +hearth +embellish +solicitor +toot +incur +western +nonpartisan +lather +prong +flame +mirth +countess +rag +donkey +fashion +handkerchief +ruckus +taint +raw +rat +rap +protract +spade +ray +snow +thorough +contact +hatch +snob +cleft +extravert +quicken +rider +evangelist +shallow +milkman +coil +coin +glow +interject +flop +metal +freeway +policewoman +flog +yank +chariot +bait +endear +saga +alight +random +sage +dupe +radio +rector +earth +bail +shellfish +spite +stanza +disgust +axiom +waltz +gees +watch +fluid +ultimatum +report +reconstruct +noon +spokesperson +egoism +public +erupt +pacifist +pummel +habit +wrest +nut +resist +corrupt +hourglass +mull +mud +mug +finger +mum +approach +wean +weak +contort +boss +toothbrush +southeast +larynx +devour +devout +censorship +newt +protect +irregular +fault +papyrus +facet +elf +smuggler +trust +bingo +bathroom +beef +legend +beer +spread +communion +loft +bladder +uncommon +craft +spearmint +catch +snipe +teapot +misfit +lessen +thousandth +referendum +pyramid +handrail +broth +lollypop +exterior +suggest +wound +overstep +utilitarian +complex +papaya +screw +pick +deflect +suburb +portal +postmark +tassel +ocean +mother +bough +bugger +rodent +shorthand +enlighten +elk +elm +moonbeam +flutist +kelp +misprint +teetotal +upturn +ramrod +dismount +quicksand +spanner +authorship +roach +befit +rumor +apart +ditto +gift +zeal +contradict +hunt +dishonest +zoom +mongrel +hunk +mosquito +hunchback +sanction +excerpt +curio +accost +usher +indirect +intellect +doorstep +nobleman +cooper +combat +letterhead +ice +rhino +newsprint +skylight +convict +christmas +splendor +cord +core +khaki +brawl +corn +brawn +cork +discount +shuck +plug +census +cowboy +plum +choke +surround +caulk +dinner +plus +alga +duke +abet +civic +civil +bath +engulf +cafeteria +art +scamper +transform +sunlight +forbad +virgin +gin +head +medium +amateur +heal +stereo +heat +hear +heap +raft +counsel +muster +bargain +bide +latch +adorn +trim +trio +forearm +cobbler +trip +no +tit +when +junta +tin +whet +tie +implant +depot +pseudonym +evergreen +cleric +toad +geneticist +bullet +navel +yacht +withhold +fasten +backward +coach +impression +rob +rod +focus +livelihood +snip +yokel +rot +discern +environment +aplomb +melon +prop +coop +impend +plantain +cook +cool +looney +level +brother +encroach +quick +lever +pork +drier +trend +bullfrog +pore +inland +voucher +takeoff +bake +port +colic +hymn +choral +postman +spire +theist +thresh +tormentor +humorist +water +fluke +entertain +witch +twentieth +tire +boast +catnap +blotch +cinnamon +prude +weird +tweak +brighten +touchdown +post +panacea +concoct +scan +handler +prey +today +chapter +conductor +altar +cashier +drown +dismal +inhabit +judo +conceal +flagship +hullabaloo +fauna +laughter +streak +overpass +sandbag +trump +stream +despot +stroke +cube +hydrogen +bigot +dress +vital +fourth +dope +ballroom +fascist +clone +scoff +fascism +birthday +apprenticeship +eighth +repeat +classroom +twitch +liquid +inform +reaper +lagoon +superscript +refund +rye +midnight +blare +worship +thermostat +apex +platform +farmer +meridian +cutter +underneath +conquer +fern +rescind +wagon +term +name +realist +opera +bunion +bullion +realism +ailment +torch +zebra +distributor +hysteria +hacker +concur +profit +middleman +gram +clover +hull +hulk +flyer +tuner +flare +highjack +motion +turn +place +swine +swing +turf +preach +childhood +origin +pelican +feign +suspend +insist +scollop +bobcat +array +peddler +given +afterthought +district +opus +trillion +plastic +assort +white +hue +hug +hub +cope +season +hut +enigma +naught +grunt +holder +wide +bewitch +spokeswoman +oath +powder +rend +froth +pro +ani +ant +rent +dragon +stolid +marathon +ideal +blunt +surf +sure +aspen +equestrian +tornado +freshman +librarian +bluebird +icon +latex +tendon +annul +seafood +later +koran +readjust +senior +slope +perch +convoy +cheat +cheap +trespass +hack +broach +hustler +trot +woolen +gulf +genius +gull +shimmer +crime +gulp +woof +wood +deign +wool +entrant +viewpoint +lighten +jazz +festoon +tailor +lighter +dye +homestead +reveal +aluminum +workman +joker +dumfound +bison +picnic +pane +vizor +prowl +optometrist +detect +crooked +review +spoons +hiss +smokestack +caucus +fearless +guitar +coma +comb +come +zodiac +isthmus +reaction +superstar +region +quiet +contract +railway +penal +adjoin +color +armchair +pot +period +pop +pole +colon +polo +pod +poll +runaway +turkey +hobo +schoolboy +tiger +padlock +hernia +careful +spirit +robber +pilot +case +shaft +amend +mount +cash +cask +cast +mound +ventriloquist +vest +exult +clutter +helmet +projector +author +alphabet +fender +bowl +check +macaroni +catfish +bellboy +hermit +week +sang +applaud +nest +driver +weed +director +petticoat +lute +puke +vowel +muffler +weep +cartoon +ranch +relief +model +reward +sinner +clod +clog +tip +kilt +ninth +clot +lavish +violent +kill +kiln +kilo +polish +satin +blow +blot +hint +rose +regent +except +blog +bloc +blob +hind +scrapbook +disrupt +impound +kingdom +blowout +sandman +mugger +towel +bracelet +snort +friar +tower +node +deduct +wombat +interlock +canteen +slice +mood +slick +legal +moon +moos +teardrop +moot +heir +porter +metropolis +quit +unmask +slaughter +quip +ok +oh +of +jeer +shrimp +pistachio +karat +stand +ox +doze +accredit +amber +tribe +vicar +polka +garb +spinal +forewarn +feudal +whaler +there +racism +strict +racist +valley +fish +gibe +relic +jug +regard +cabinet +castaway +strenuous +jut +terminus +feeler +grasp +grass +toilet +ruffian +cinema +frighten +lichen +encompass +bishop +incorrect +abyss +fiddler +heather +idiot +diarrhea +rubber +idiom +heathen +trash +stalwart +championship +symbol +cove +nucleus +serious +brass +wife +invest +derrick +treason +apparel +platter +all +lace +duplex +lack +spacecraft +disc +dish +follow +settlement +titter +wanton +thursday +program +neglig +woman +song +fat +roe +psychologist +retract +ultraviolet +awful +dapper +fad +induct +stimulus +list +trench +align +flick +ten +tea +tee +rate +design +chalk +what +sub +sun +sum +whimper +rascal +brief +overload +crush +version +pulpit +intersect +row +womb +lacquer +pumpernickel +backer +goodby +thrift +misinterpret +heifer +jogger +cataract +haemophilia +murmur +snug +snub +herring +proceed +tarpaulin +wield +hurray +rustic +quash +inlay +garnish +hurrah +minor +ladybug +wretched +flap +mire +protestor +stutter +flit +flag +stick +mellow +chaplain +berth +wrestler +plagiarist +searchlight +sunscreen +pond +court +goal +goad +goat +sandwich +okay +algebra +headrest +embalm +reflect +catalog +numb +short +ricochet +tsar +shade +waylay +mission +scientist +flaunt +reconnect +pretext +stride +islam +thirtieth +style +glide +pray +inward +wilder +abbey +mattress +resort +airstrip +bout +soccer +might +alter +return +hunter +underground +abacus +mathematician +liniment +policeman +refresh +tactless +friendship +weight +needless +duchess +falsetto +expect +inflict +wager +alcohol +disquiet +hilt +dugout +loll +health +hill +shipment +fiber +solvent +friday +differ +effect +disinfect +octagon +physic +teach +sidewalk +jew +blister +thread +threat +bushel +feed +dine +feel +sailor +revolution +least +blank +cigaret +idea +moan +script +gourmet +interact +grime +stork +swarm +storm +moat +syrup +store +mainstay +imperfect +option +hotel +fidget +king +kind +vial +kink +stall +cuff +foreleg +stale +restful +amass +cleaner +exert +strengthen +shrewd +bookworm +gale +gala +gall +remodel +smallpox +toughen +bacteria +chairman +donor +pianist +buff +gill +foreman +rapist +reckon +english +reach +react +nothing +quorum +hyena +amphibian +saloon +notch +scaffold +asphalt +memorandum +felon +font +anvil +firewood +betray +hip +shepherd +hit +deaden +reprint +him +adulthood +snowstorm +forego +stump +martyrdom +arc +bare +bard +bark +ark +arm +barn +blurt +parchment +various +plywood +nincompoop +solo +muslim +sole +outfit +succeed +inertia +orangutang +blazer +bandit +context +bond +cynic +sloth +flier +distress +chaperon +sweet +wastebasket +sweep +weasel +rave +shaykh +bolster +dub +overlook +dud +due +buttermilk +pa +watchword +brick +pi +flight +quintet +dropout +marihuana +cinch +temperament +instructor +heighten +toga +shove +batch +pitchfork +kick +behavior +incognito +lodger +bluegrass +sirup +rip +shamrock +rim +frantic +rig +rid +reprogram +chauffeur +shirt +kimono +viola +shirk +sliver +straightjacket +restraint +painless +throwback +cement +birch +robust +knack +lower +earmark +cheek +cheep +cheer +pollen +facial +vigilant +cabaret +continent +tablet +contractor +plateau +tuxedo +complaint +vendor +foreshadow +awaken +confront +uproar +distrust +breeder +hallmark +play +global +litter +wonderland +butterscotch +saucer +prow +seller +prom +prod +sag +perpendicular +tinker +raider +vivid +cautious +undertow +yawn +ordeal +militia +dialog +tomboy +conquest +momma +piteous +holster +vagabond +stench +canal +pundit +question +swill +parsnip +etch +filet +potassium +glamour +cloth +crank +usurp +delta +upright +crane +outpost +penguin +patriot +consist +apricot +caller +peppermint +husk +cartwheel +highlight +dill +freak +dismay +sublet +sagebrush +rainbow +lemon +riot +peach +grouper +saffron +nick +parlor +ferment +bandstand +mock +nice +mustard +chirp +meaning +vigil +vice +ocular +remit +pyre +buffalo +scroll +pervert +lean +alien +dispel +gang +theorist +gold +uphold +floss +breach +sniper +materialist +toenail +spool +spoon +spook +spoof +harlot +outdo +pleas +pleat +trawl +procession +fold +reunion +acid +folk +sandstorm +outsmart +acronym +relent +kangaroo +gloat +miser +cyclist +barb +survivor +guarantor +orangutan +armpit +shovel +duct +ensign +apt +volt +motor +duck +thick +redo +ape +use +fee +fen +frog +germ +modicum +fez +sort +parliament +porch +musician +impress +sore +rabbit +recount +penis +sculptor +annoy +topic +augment +critic +lumber +executor +proof +bittersweet +tap +tar +tax +villain +tag +condescend +tab +spa +silt +tan +rape +counterfeit +sir +sip +scuff +sit +tamper +six +outclass +occur +sic +carrier +goldsmith +toddler +panic +sin +defeat +tension +lesion +attend +tact +hazard +discomfort +tack +wrist +taco +footpath +aftereffect +light +arduous +schoolchild +sailboat +stamped +minnow +damsel +accompanist +hemp +tyrant +badger +glen +superior +inlet +sill +glee +nostalgia +flank +restrain +glisten +turban +redhead +bye +flex +crash +citrus +flour +flout +emerald +flea +republican +investor +successor +easel +footstool +profound +edit +feast +fuzz +trap +blacktop +cocoon +tray +lilac +mincemeat +interplay +our +proclaim +out +semen +tabloid +cocktail +sentiment +frontier +vehement +disarray +clatter +impart +plural +proviso +planner +utensil +tenement +pendant +gospel +tenant +greenhorn +tanker +zoologist +rivet +uproot +embryo +sew +bouquet +echo +bonnet +eleventh +synagog +salient +droop +unknown +galley +snore +anaemia +drool +boil +tidbit +shell +shelf +transistor +woo +diminish +persecutor +goblin +institution +kickback +frugal +brazen +yodel +laughingstock +clip +fowl +splatter +flunkey +blip +footwork +outstrip +disjoint +pallor +catholic +clove +rout +outward +bagel +lope +divert +trivia +pharmacist +divers +clash +petrol +siphon +filch +fortieth +class +clasp +fang +dens +dent +pipe +vernacular +gain +son +stove +sonnet +utter +chicken +feat +winch +dandruff +rioter +herald +piano +local +counsellor +vigor +sued +skimp +plaintiff +spud +watercolor +barter +bronco +spur +rite +ghetto +bisect +compliment +ascertain +sediment +view +unison +workbench +ebb +expel +hymnal +distract +violet +still +closet +superb +favor +viper +crude +torpedo +avow +jot +exam +amen +joy +foetus +job +spoil +jog +swift +memento +lifeboat +april +grain +commando +wall +hyphen +walk +respect +unclean +decent +trademark +tutor +reindeer +mike +nickel +cypress +penmanship +dearth +overturn +present +kerchief +corset +wilt +vanilla +priesthood +will +fingertip +wild +whirlpool +layer +mutant +motif +apprehend +rooster +lightweight +thug +thud +whore +headlight +cross +member +pediatrist +inch +grandeur +slave +diploma +outcast +beast +student +pedal +whale +collar +gutter +masochist +overwork +scissor +twirl +flint +outgrow +bandanna +rocker +cameo +rocket +camel +boot +wren +obtain +replenish +biologist +daub +distend +smite +now +panther +drunk +smith +hall +book +ski +enact +knob +sick +myth +know +knot +press +redesign +doughnut +loser +cutlet +vortex +clutch +exceed +setter +flagrant +birthmark +demeanor +growth +export +leaf +lead +leak +miner +leap +belt +leader +trout +obey +slur +mitt +slut +slum +pasta +mite +slug +throne +pike +throng +rare +linger +column +biscuit +fear +swear +sweat +udder +emperor +owl +outset +own +owe +weather +champ +brush +billfold +gape +rowboat +van +platoon +transfer +spiral +grotto +cliff +vat +nourish +catsup +unwrap +saunter +mutter +brassier +assail +tomcat +daffodil +nightgown +record +cake +faggot +maroon +boardwalk +abbot +counteract +limb +squirrel +mutual +glint +boor +percent +other +boom +branch +cutthroat +junk +mulch +june +squeak +squeal +extort +jewel +gynecologist +vane +sash diff --git a/libs/pyutil/fileutil.py~ b/libs/pyutil/fileutil.py~ deleted file mode 100644 index e37eb792..00000000 --- a/libs/pyutil/fileutil.py~ +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -""" -Futz with files like a pro. -""" - -import errno, exceptions, os, stat, tempfile - -try: - import bsddb -except ImportError: - DBNoSuchFileError = None -else: - DBNoSuchFileError = bsddb.db.DBNoSuchFileError - -# read_file() and write_file() copied from Mark Seaborn's blog post. Please -# read it for complete rationale: -# http://lackingrhoticity.blogspot.com/2009/12/readfile-and-writefile-in-python.html - -def read_file(filename, mode='rb'): - """ Read the contents of the file named filename and return it in - a string. This function closes the file handle before it returns - (even if the underlying Python implementation's garbage collector - doesn't). """ - fh = open(filename, mode) - try: - return fh.read() - finally: - fh.close() - -def write_file(filename, data, mode='wb'): - """ Write the string data into a file named filename. This - function closes the file handle (ensuring that the written data is - flushed from the perspective of the Python implementation) before - it returns (even if the underlying Python implementation's garbage - collector doesn't).""" - fh = open(filename, mode) - try: - fh.write(data) - finally: - fh.close() - -# For backwards-compatibility in case someone is using these names. We used to -# have a superkludge in fileutil.py under these names. -def rename(src, dst, tries=4, basedelay=0.1): - return os.rename(src, dst) - -def remove(f, tries=4, basedelay=0.1): - return os.remove(f) - -def rmdir(f, tries=4, basedelay=0.1): - return os.rmdir(f) - -class _Dir(object): - """ - Hold a set of files and subdirs and clean them all up when asked to. - """ - def __init__(self, name, cleanup=True): - self.name = name - self.cleanup = cleanup - self.files = [] - self.subdirs = set() - - def file(self, fname, mode=None): - """ - Create a file in the tempdir and remember it so as to close() it - before attempting to cleanup the temp dir. - - @rtype: file - """ - ffn = os.path.join(self.name, fname) - if mode is not None: - fo = open(ffn, mode) - else: - fo = open(ffn) - self.register_file(fo) - return fo - - def subdir(self, dirname): - """ - Create a subdirectory in the tempdir and remember it so as to call - shutdown() on it before attempting to clean up. - - @rtype: _Dir instance - """ - ffn = os.path.join(self.name, dirname) - sd = _Dir(ffn, self.cleanup) - self.register_subdir(sd) - make_dirs(sd.name) - return sd - - def register_file(self, fileobj): - """ - Remember the file object and call close() on it before attempting to - clean up. - """ - self.files.append(fileobj) - - def register_subdir(self, dirobj): - """ - Remember the _Dir object and call shutdown() on it before attempting - to clean up. - """ - self.subdirs.add(dirobj) - - def shutdown(self): - if self.cleanup: - for subdir in hasattr(self, 'subdirs') and self.subdirs or []: - subdir.shutdown() - for fileobj in hasattr(self, 'files') and self.files or []: - if DBNoSuchFileError is None: - fileobj.close() # "close()" is idempotent so we don't need to catch exceptions here - else: - try: - fileobj.close() - except DBNoSuchFileError: - # Ah, except that the bsddb module's file-like object (a DB object) has a non-idempotent close... - pass - - if hasattr(self, 'name'): - rm_dir(self.name) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - try: - self.shutdown() - except: - import traceback - traceback.print_exc() - -class NamedTemporaryDirectory(_Dir): - """ - Call tempfile.mkdtemp(), store the name of the dir in self.name, and - rm_dir() when it gets garbage collected or "shutdown()". - - Also keep track of file objects for files within the tempdir and call - close() on them before rm_dir(). This is a convenient way to open temp - files within the directory, and it is very helpful on Windows because you - can't delete a directory which contains a file which is currently open. - """ - - def __init__(self, cleanup=True, *args, **kwargs): - """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """ - name = tempfile.mkdtemp(*args, **kwargs) - _Dir.__init__(self, name, cleanup) - -class ReopenableNamedTemporaryFile: - """ - This uses tempfile.mkstemp() to generate a secure temp file. It then closes - the file, leaving a zero-length file as a placeholder. You can get the - filename with ReopenableNamedTemporaryFile.name. When the - ReopenableNamedTemporaryFile instance is garbage collected or its shutdown() - method is called, it deletes the file. - """ - def __init__(self, *args, **kwargs): - fd, self.name = tempfile.mkstemp(*args, **kwargs) - os.close(fd) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - self.shutdown() - - def shutdown(self): - remove(self.name) - -def make_dirs(dirname, mode=0777): - """ - An idempotent version of os.makedirs(). If the dir already exists, do - nothing and return without raising an exception. If this call creates the - dir, return without raising an exception. If there is an error that - prevents creation or if the directory gets deleted after make_dirs() creates - it and before make_dirs() checks that it exists, raise an exception. - """ - tx = None - try: - os.makedirs(dirname, mode) - except OSError, x: - tx = x - - if not os.path.isdir(dirname): - if tx: - raise tx - raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning... - -def rmtree(dirname): - """ - A threadsafe and idempotent version of shutil.rmtree(). If the dir is - already gone, do nothing and return without raising an exception. If this - call removes the dir, return without raising an exception. If there is an - error that prevents deletion or if the directory gets created again after - rm_dir() deletes it and before rm_dir() checks that it is gone, raise an - exception. - """ - excs = [] - try: - os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) - for f in os.listdir(dirname): - fullname = os.path.join(dirname, f) - if os.path.isdir(fullname): - rm_dir(fullname) - else: - remove(fullname) - os.rmdir(dirname) - except EnvironmentError, le: - # Ignore "No such file or directory", collect any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - excs.append(le) - except Exception, le: - excs.append(le) - - # Okay, now we've recursively removed everything, ignoring any "No - # such file or directory" errors, and collecting any other errors. - - if os.path.exists(dirname): - if len(excs) == 1: - raise excs[0] - if len(excs) == 0: - raise OSError, "Failed to remove dir for unknown reason." - raise OSError, excs - -def rm_dir(dirname): - # Renamed to be like shutil.rmtree and unlike rmdir. - return rmtree(dirname) - -def remove_if_possible(f): - try: - remove(f) - except EnvironmentError: - pass - -def remove_if_present(f): - try: - remove(f) - except EnvironmentError, le: - # Ignore "No such file or directory", re-raise any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - raise - -def rmdir_if_possible(f): - try: - rmdir(f) - except EnvironmentError: - pass - -def open_or_create(fname, binarymode=True): - try: - f = open(fname, binarymode and "r+b" or "r+") - except EnvironmentError: - f = open(fname, binarymode and "w+b" or "w+") - return f - -def du(basedir): - size = 0 - - for root, dirs, files in os.walk(basedir): - for f in files: - fn = os.path.join(root, f) - size += os.path.getsize(fn) - - return size diff --git a/libs/pyutil/iputil.py b/libs/pyutil/iputil.py index b8b96362..cb3e7c02 100644 --- a/libs/pyutil/iputil.py +++ b/libs/pyutil/iputil.py @@ -1,22 +1,12 @@ -# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) -# portions adapted from nattraverso.ipdiscover -# portions authored by Brian Warner, working for Allmydata -# most recent version authored by Zooko O'Whielacronx, working for Allmydata - # from the Python Standard Library -import os, re, socket, sys +import os, re, socket, sys, subprocess # from Twisted -from twisted.internet import defer, reactor -from twisted.python import failure +from twisted.internet import defer, threads, reactor from twisted.internet.protocol import DatagramProtocol -from twisted.internet.utils import getProcessOutput from twisted.python.procutils import which from twisted.python import log -# from pyutil -import observer - try: import resource def increase_rlimits(): @@ -77,6 +67,7 @@ except ImportError: # since one might be shadowing the other. This hack appeases pyflakes. increase_rlimits = _increase_rlimits + def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET """ Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad @@ -121,14 +112,16 @@ def get_local_ip_for(target): except socket.gaierror: # DNS isn't running, or somehow we encountered an error - # note: if an interface is configured and up, but nothing is connected to it, - # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror - # . This is synchronous and occurs for each node being started, so users of certain unit - # tests will see something like 120s of delay, which may be enough to hit the default - # trial timeouts. For that reason, get_local_addresses_async() was changed to default to - # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also - # makes node startup a tad faster. - + # note: if an interface is configured and up, but nothing is + # connected to it, gethostbyname("A.ROOT-SERVERS.NET") will take 20 + # seconds to raise socket.gaierror . This is synchronous and occurs + # for each node being started, so users of + # test.common.SystemTestMixin (like test_system) will see something + # like 120s of delay, which may be enough to hit the default trial + # timeouts. For that reason, get_local_addresses_async() was changed + # to default to the numerical ip address for A.ROOT-SERVERS.NET, to + # avoid this DNS lookup. This also makes node startup fractionally + # faster. return None udpprot = DatagramProtocol() port = reactor.listenUDP(0, udpprot) @@ -146,16 +139,29 @@ _platform_map = { "linux-i386": "linux", # redhat "linux-ppc": "linux", # redhat "linux2": "linux", # debian + "linux3": "linux", # debian "win32": "win32", "irix6-n32": "irix", "irix6-n64": "irix", "irix6": "irix", "openbsd2": "bsd", + "openbsd3": "bsd", + "openbsd4": "bsd", + "openbsd5": "bsd", "darwin": "bsd", # Mac OS X "freebsd4": "bsd", "freebsd5": "bsd", "freebsd6": "bsd", + "freebsd7": "bsd", + "freebsd8": "bsd", + "freebsd9": "bsd", "netbsd1": "bsd", + "netbsd2": "bsd", + "netbsd3": "bsd", + "netbsd4": "bsd", + "netbsd5": "bsd", + "netbsd6": "bsd", + "dragonfly2": "bsd", "sunos5": "sunos", "cygwin": "cygwin", } @@ -173,12 +179,12 @@ _win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\ # These work in Redhat 6.x and Debian 2.2 potato _linux_path = '/sbin/ifconfig' -_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +_linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) -# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD -_bsd_path = '/sbin/ifconfig' -_bsd_args = ('-a',) -_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +# NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X +_netbsd_path = '/sbin/ifconfig' +_netbsd_args = ('-a',) +_netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) # Irix 6.5 _irix_path = '/usr/etc/ifconfig' @@ -186,39 +192,6 @@ _irix_path = '/usr/etc/ifconfig' # Solaris 2.x _sunos_path = '/usr/sbin/ifconfig' -class SequentialTrier(object): - """ I hold a list of executables to try and try each one in turn - until one gives me a list of IP addresses.""" - - def __init__(self, exebasename, args, regex): - assert not os.path.isabs(exebasename) - self.exes_left_to_try = which(exebasename) - self.exes_left_to_try.reverse() - self.args = args - self.regex = regex - self.o = observer.OneShotObserverList() - self._try_next() - - def _try_next(self): - if not self.exes_left_to_try: - self.o.fire(None) - else: - exe = self.exes_left_to_try.pop() - d2 = _query(exe, self.args, self.regex) - - def cb(res): - if res: - self.o.fire(res) - else: - self._try_next() - - def eb(why): - self._try_next() - - d2.addCallbacks(cb, eb) - - def when_tried(self): - return self.o.when_fired() # k: platform string as provided in the value of _platform_map # v: tuple of (path_to_tool, args, regex,) @@ -226,19 +199,22 @@ _tool_map = { "linux": (_linux_path, (), _linux_re,), "win32": (_win32_path, _win32_args, _win32_re,), "cygwin": (_win32_path, _win32_args, _win32_re,), - "bsd": (_bsd_path, _bsd_args, _bsd_re,), - "irix": (_irix_path, _bsd_args, _bsd_re,), - "sunos": (_sunos_path, _bsd_args, _bsd_re,), + "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,), + "irix": (_irix_path, _netbsd_args, _netbsd_re,), + "sunos": (_sunos_path, _netbsd_args, _netbsd_re,), } + def _find_addresses_via_config(): - # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + return threads.deferToThread(_synchronously_find_addresses_via_config) + +def _synchronously_find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian's API platform = _platform_map.get(sys.platform) - (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + if not platform: + raise UnsupportedPlatformError(sys.platform) - # If the platform isn't known then we attempt BSD-style ifconfig. If it - # turns out that we don't get anything resembling a dotted quad IPv4 address - # out of it, then we'll raise UnsupportedPlatformError. + (pathtotool, args, regex,) = _tool_map[platform] # If pathtotool is a fully qualified path then we just try that. # If it is merely an executable name then we use Twisted's @@ -246,34 +222,33 @@ def _find_addresses_via_config(): # gives us something that resembles a dotted-quad IPv4 address. if os.path.isabs(pathtotool): - d = _query(pathtotool, args, regex) + return _query(pathtotool, args, regex) else: - d = SequentialTrier(pathtotool, args, regex).when_tried() - - d.addCallback(_check_result) - return d - -def _check_result(result): - if not result and not _platform_map.has_key(sys.platform): - return failure.Failure(UnsupportedPlatformError(sys.platform)) - else: - return result + exes_to_try = which(pathtotool) + for exe in exes_to_try: + try: + addresses = _query(exe, args, regex) + except Exception: + addresses = [] + if addresses: + return addresses + return [] def _query(path, args, regex): - d = getProcessOutput(path, args) - def _parse(output): - addresses = [] - outputsplit = output.split('\n') - for outline in outputsplit: - m = regex.match(outline) - if m: - addr = m.groupdict()['address'] - if addr not in addresses: - addresses.append(addr) + env = {'LANG': 'en_US.UTF-8'} + p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + (output, err) = p.communicate() - return addresses - d.addCallback(_parse) - return d + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses def _cygwin_hack_find_addresses(target): addresses = [] diff --git a/libs/pyutil/iputil.py~ b/libs/pyutil/iputil.py~ new file mode 100644 index 00000000..b8b96362 --- /dev/null +++ b/libs/pyutil/iputil.py~ @@ -0,0 +1,288 @@ +# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) +# portions adapted from nattraverso.ipdiscover +# portions authored by Brian Warner, working for Allmydata +# most recent version authored by Zooko O'Whielacronx, working for Allmydata + +# from the Python Standard Library +import os, re, socket, sys + +# from Twisted +from twisted.internet import defer, reactor +from twisted.python import failure +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.utils import getProcessOutput +from twisted.python.procutils import which +from twisted.python import log + +# from pyutil +import observer + +try: + import resource + def increase_rlimits(): + # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain + # systems (OS-X, probably solaris) start with a relatively low limit + # (256), and some unit tests want to open up more sockets than this. + # Most linux systems start with both hard and soft limits at 1024, + # which is plenty. + + # unfortunately the values to pass to setrlimit() vary widely from + # one system to another. OS-X reports (256, HUGE), but the real hard + # limit is 10240, and accepts (-1,-1) to mean raise it to the + # maximum. Cygwin reports (256, -1), then ignores a request of + # (-1,-1): instead you have to guess at the hard limit (it appears to + # be 3200), so using (3200,-1) seems to work. Linux reports a + # sensible (1024,1024), then rejects (-1,-1) as trying to raise the + # maximum limit, so you could set it to (1024,1024) but you might as + # well leave it alone. + + try: + current = resource.getrlimit(resource.RLIMIT_NOFILE) + except AttributeError: + # we're probably missing RLIMIT_NOFILE + return + + if current[0] >= 1024: + # good enough, leave it alone + return + + try: + if current[1] > 0 and current[1] < 1000000: + # solaris reports (256, 65536) + resource.setrlimit(resource.RLIMIT_NOFILE, + (current[1], current[1])) + else: + # this one works on OS-X (bsd), and gives us 10240, but + # it doesn't work on linux (on which both the hard and + # soft limits are set to 1024 by default). + resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1)) + new = resource.getrlimit(resource.RLIMIT_NOFILE) + if new[0] == current[0]: + # probably cygwin, which ignores -1. Use a real value. + resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1)) + + except ValueError: + log.msg("unable to set RLIMIT_NOFILE: current value %s" + % (resource.getrlimit(resource.RLIMIT_NOFILE),)) + except: + # who knows what. It isn't very important, so log it and continue + log.err() +except ImportError: + def _increase_rlimits(): + # TODO: implement this for Windows. Although I suspect the + # solution might be "be running under the iocp reactor and + # make this function be a no-op". + pass + # pyflakes complains about two 'def FOO' statements in the same time, + # since one might be shadowing the other. This hack appeases pyflakes. + increase_rlimits = _increase_rlimits + +def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET + """ + Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad + strings) that are currently configured on this host, sorted in descending + order of how likely we think they are to work. + + @param target: we want to learn an IP address they could try using to + connect to us; The default value is fine, but it might help if you + pass the address of a host that you are actually trying to be + reachable to. + """ + addresses = [] + local_ip = get_local_ip_for(target) + if local_ip: + addresses.append(local_ip) + + if sys.platform == "cygwin": + d = _cygwin_hack_find_addresses(target) + else: + d = _find_addresses_via_config() + + def _collect(res): + for addr in res: + if addr != "0.0.0.0" and not addr in addresses: + addresses.append(addr) + return addresses + d.addCallback(_collect) + + return d + +def get_local_ip_for(target): + """Find out what our IP address is for use by a given target. + + @return: the IP address as a dotted-quad string which could be used by + to connect to us. It might work for them, it might not. If + there is no suitable address (perhaps we don't currently have an + externally-visible interface), this will return None. + """ + + try: + target_ipaddr = socket.gethostbyname(target) + except socket.gaierror: + # DNS isn't running, or somehow we encountered an error + + # note: if an interface is configured and up, but nothing is connected to it, + # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror + # . This is synchronous and occurs for each node being started, so users of certain unit + # tests will see something like 120s of delay, which may be enough to hit the default + # trial timeouts. For that reason, get_local_addresses_async() was changed to default to + # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also + # makes node startup a tad faster. + + return None + udpprot = DatagramProtocol() + port = reactor.listenUDP(0, udpprot) + try: + udpprot.transport.connect(target_ipaddr, 7) + localip = udpprot.transport.getHost().host + except socket.error: + # no route to that host + localip = None + port.stopListening() # note, this returns a Deferred + return localip + +# k: result of sys.platform, v: which kind of IP configuration reader we use +_platform_map = { + "linux-i386": "linux", # redhat + "linux-ppc": "linux", # redhat + "linux2": "linux", # debian + "win32": "win32", + "irix6-n32": "irix", + "irix6-n64": "irix", + "irix6": "irix", + "openbsd2": "bsd", + "darwin": "bsd", # Mac OS X + "freebsd4": "bsd", + "freebsd5": "bsd", + "freebsd6": "bsd", + "netbsd1": "bsd", + "sunos5": "sunos", + "cygwin": "cygwin", + } + +class UnsupportedPlatformError(Exception): + pass + +# Wow, I'm really amazed at home much mileage we've gotten out of calling +# the external route.exe program on windows... It appears to work on all +# versions so far. Still, the real system calls would much be preferred... +# ... thus wrote Greg Smith in time immemorial... +_win32_path = 'route.exe' +_win32_args = ('print',) +_win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\d+)\s+(?P\d+)\s*$', flags=re.M|re.I|re.S) + +# These work in Redhat 6.x and Debian 2.2 potato +_linux_path = '/sbin/ifconfig' +_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD +_bsd_path = '/sbin/ifconfig' +_bsd_args = ('-a',) +_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# Irix 6.5 +_irix_path = '/usr/etc/ifconfig' + +# Solaris 2.x +_sunos_path = '/usr/sbin/ifconfig' + +class SequentialTrier(object): + """ I hold a list of executables to try and try each one in turn + until one gives me a list of IP addresses.""" + + def __init__(self, exebasename, args, regex): + assert not os.path.isabs(exebasename) + self.exes_left_to_try = which(exebasename) + self.exes_left_to_try.reverse() + self.args = args + self.regex = regex + self.o = observer.OneShotObserverList() + self._try_next() + + def _try_next(self): + if not self.exes_left_to_try: + self.o.fire(None) + else: + exe = self.exes_left_to_try.pop() + d2 = _query(exe, self.args, self.regex) + + def cb(res): + if res: + self.o.fire(res) + else: + self._try_next() + + def eb(why): + self._try_next() + + d2.addCallbacks(cb, eb) + + def when_tried(self): + return self.o.when_fired() + +# k: platform string as provided in the value of _platform_map +# v: tuple of (path_to_tool, args, regex,) +_tool_map = { + "linux": (_linux_path, (), _linux_re,), + "win32": (_win32_path, _win32_args, _win32_re,), + "cygwin": (_win32_path, _win32_args, _win32_re,), + "bsd": (_bsd_path, _bsd_args, _bsd_re,), + "irix": (_irix_path, _bsd_args, _bsd_re,), + "sunos": (_sunos_path, _bsd_args, _bsd_re,), + } +def _find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + + platform = _platform_map.get(sys.platform) + (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + + # If the platform isn't known then we attempt BSD-style ifconfig. If it + # turns out that we don't get anything resembling a dotted quad IPv4 address + # out of it, then we'll raise UnsupportedPlatformError. + + # If pathtotool is a fully qualified path then we just try that. + # If it is merely an executable name then we use Twisted's + # "which()" utility and try each executable in turn until one + # gives us something that resembles a dotted-quad IPv4 address. + + if os.path.isabs(pathtotool): + d = _query(pathtotool, args, regex) + else: + d = SequentialTrier(pathtotool, args, regex).when_tried() + + d.addCallback(_check_result) + return d + +def _check_result(result): + if not result and not _platform_map.has_key(sys.platform): + return failure.Failure(UnsupportedPlatformError(sys.platform)) + else: + return result + +def _query(path, args, regex): + d = getProcessOutput(path, args) + def _parse(output): + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses + d.addCallback(_parse) + return d + +def _cygwin_hack_find_addresses(target): + addresses = [] + for h in [target, "localhost", "127.0.0.1",]: + try: + addr = get_local_ip_for(h) + if addr not in addresses: + addresses.append(addr) + except socket.gaierror: + pass + + return defer.succeed(addresses) diff --git a/libs/pyutil/mathutil.py b/libs/pyutil/mathutil.py index 46781b0c..9c169801 100644 --- a/libs/pyutil/mathutil.py +++ b/libs/pyutil/mathutil.py @@ -11,7 +11,7 @@ def div_ceil(n, d): """ The smallest integer k such that k*d >= n. """ - return (n/d) + (n%d != 0) + return int((n//d) + (n%d != 0)) def next_multiple(n, k): """ diff --git a/libs/pyutil/mathutil.py~ b/libs/pyutil/mathutil.py~ new file mode 100644 index 00000000..46781b0c --- /dev/null +++ b/libs/pyutil/mathutil.py~ @@ -0,0 +1,106 @@ +# Copyright (c) 2005-2010 Zooko Wilcox-O'Hearn +# This file is part of pyutil; see README.rst for licensing terms. + +""" +A few commonly needed functions. +""" + +import math + +def div_ceil(n, d): + """ + The smallest integer k such that k*d >= n. + """ + return (n/d) + (n%d != 0) + +def next_multiple(n, k): + """ + The smallest multiple of k which is >= n. Note that if n is 0 then the + answer is 0. + """ + return div_ceil(n, k) * k + +def pad_size(n, k): + """ + The smallest number that has to be added to n to equal a multiple of k. + """ + if n%k: + return k - n%k + else: + return 0 + +def is_power_of_k(n, k): + return k**int(math.log(n, k) + 0.5) == n + +def next_power_of_k(n, k): + p = 1 + while p < n: + p *= k + return p + +def ave(l): + return sum(l) / len(l) + +def log_ceil(n, b): + """ + The smallest integer k such that b^k >= n. + + log_ceil(n, 2) is the number of bits needed to store any of n values, e.g. + the number of bits needed to store any of 128 possible values is 7. + """ + p = 1 + k = 0 + while p < n: + p *= b + k += 1 + return k + +def log_floor(n, b): + """ + The largest integer k such that b^k <= n. + """ + p = 1 + k = 0 + while p <= n: + p *= b + k += 1 + return k - 1 + +def linear_fit_slope(ps): + """ + Single-independent-variable linear regression -- least squares method. + + At least, I *think* this function computes that answer. I no longer + remember where I learned this trick and at the moment I can't prove to + myself that this is correct. + + @param ps a sequence of tuples of (x, y) + """ + avex = ave([x for (x, y) in ps]) + avey = ave([y for (x, y) in ps]) + sxy = sum([ (x - avex) * (y - avey) for (x, y) in ps ]) + sxx = sum([ (x - avex) ** 2 for (x, y) in ps ]) + if sxx == 0: + return None + return sxy / sxx + +def permute(l): + """ + Return all possible permutations of l. + + @type l: sequence + @rtype a set of sequences + """ + if len(l) == 1: + return [l,] + + res = [] + for i in range(len(l)): + l2 = list(l[:]) + x = l2.pop(i) + for l3 in permute(l2): + l3.append(x) + res.append(l3) + + return res + diff --git a/libs/pyutil/odict.py~ b/libs/pyutil/odict.py~ deleted file mode 100644 index 0ed5ce7b..00000000 --- a/libs/pyutil/odict.py~ +++ /dev/null @@ -1,552 +0,0 @@ -# Copyright (c) 2002-2009 Zooko "Zooko" Wilcox-O'Hearn - -""" -This module offers a Ordered Dict, which is a dict that preserves -insertion order. See PEP 372 for description of the problem. This -implementation uses a linked-list to get good O(1) asymptotic -performance. (Actually it is O(hashtable-update-cost), but whatever.) - -Warning: if -O optimizations are not turned on then OrderedDict performs -extensive self-analysis in every function call, which can take minutes -and minutes for a large cache. Turn on -O, or comment out assert -self._assert_invariants() -""" - -import operator - -from assertutil import _assert, precondition -from humanreadable import hr - -class OrderedDict: - """ - An efficient ordered dict. - - Adding an item that is already in the dict *does not* make it the - most- recently-added item although it may change the state of the - dict itself (if the value is different than the previous value). - - See also SmallOrderedDict (below), which is faster in some cases. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return (k, v,) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return k - - class ValIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - precondition(self.c.d.has_key(self.i), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - (v, p, n,) = self.c.d[self.i] - self.i = p - return v - - class Sentinel: - def __init__(self, msg): - self.msg = msg - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.msg,) - - def __init__(self, initialdata={}): - self.d = {} # k: k, v: [v, prev, next,] # the dict - self.hs = OrderedDict.Sentinel("hs") - self.ts = OrderedDict.Sentinel("ts") - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.update(initialdata) - - assert self._assert_invariants() - - def __repr_n__(self, n=None): - s = ["{",] - try: - iter = self.iteritems() - x = iter.next() - s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - i = 1 - while (n is None) or (i < n): - x = iter.next() - s.append(", "); s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - except StopIteration: - pass - s.append("}") - return ''.join(s) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(),) - - def __str__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(16),) - - def _assert_invariants(self): - _assert((len(self.d) > 2) == (self.d[self.hs][2] is not self.ts) == (self.d[self.ts][1] is not self.hs), "Head and tail point to something other than each other if and only if there is at least one element in the dictionary.", self.hs, self.ts, len(self.d)) - foundprevsentinel = 0 - foundnextsentinel = 0 - for (k, (v, p, n,)) in self.d.iteritems(): - _assert(v not in (self.hs, self.ts,)) - _assert(p is not self.ts, "A reference to the tail sentinel may not appear in prev.", k, v, p, n) - _assert(n is not self.hs, "A reference to the head sentinel may not appear in next.", k, v, p, n) - _assert(p in self.d, "Each prev is required to appear as a key in the dict.", k, v, p, n) - _assert(n in self.d, "Each next is required to appear as a key in the dict.", k, v, p, n) - if p is self.hs: - foundprevsentinel += 1 - _assert(foundprevsentinel <= 2, "No more than two references to the head sentinel may appear as a prev.", k, v, p, n) - if n is self.ts: - foundnextsentinel += 1 - _assert(foundnextsentinel <= 2, "No more than one reference to the tail sentinel may appear as a next.", k, v, p, n) - _assert(foundprevsentinel == 2, "A reference to the head sentinel is required appear as a prev (plus a self-referential reference).") - _assert(foundnextsentinel == 2, "A reference to the tail sentinel is required appear as a next (plus a self-referential reference).") - - count = 0 - for (k, v,) in self.iteritems(): - _assert(k not in (self.hs, self.ts,), k, self.hs, self.ts) - count += 1 - _assert(count == len(self.d)-2, count, len(self.d)) # -2 for the sentinels - - return True - - def move_to_most_recent(self, k, strictkey=False): - assert self._assert_invariants() - - if not self.d.has_key(k): - if strictkey: - raise KeyError, k - return - - node = self.d[k] - - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - - # move to front - hnode = self.d[self.hs] - - node[1] = self.hs - node[2] = hnode[2] - hnode[2] = k - self.d[node[2]][1] = k - - assert self._assert_invariants() - - def iteritems(self): - return OrderedDict.ItemIterator(self) - - def itervalues(self): - return OrderedDict.ValIterator(self) - - def iterkeys(self): - return self.__iter__() - - def __iter__(self): - return OrderedDict.KeyIterator(self) - - def __getitem__(self, key, default=None, strictkey=True): - node = self.d.get(key) - if not node: - if strictkey: - raise KeyError, key - return default - return node[0] - - def __setitem__(self, k, v=None): - assert self._assert_invariants() - - node = self.d.get(k) - if node: - node[0] = v - return - - hnode = self.d[self.hs] - n = hnode[2] - self.d[k] = [v, self.hs, n,] - hnode[2] = k - self.d[n][1] = k - - assert self._assert_invariants() - return v - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the value removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if self.d.has_key(key): - node = self.d[key] - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - del self.d[key] - assert self._assert_invariants() - return node[0] - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def has_key(self, key): - assert self._assert_invariants() - if self.d.has_key(key): - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def clear(self): - assert self._assert_invariants() - self.d.clear() - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - - for (k, v,) in otherdict.iteritems(): - assert self._assert_invariants() - self[k] = v - assert self._assert_invariants() - - def pop(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - self.remove(k) - assert self._assert_invariants() - return k - - def popitem(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - val = self.remove(k) - assert self._assert_invariants() - return (k, val,) - - def keys_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return t.keys() - - def keys(self): - res = [None] * len(self) - i = 0 - for k in self.iterkeys(): - res[i] = k - i += 1 - return res - - def values_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return map(operator.__getitem__, t.values(), [0]*len(t)) - - def values(self): - res = [None] * len(self) - i = 0 - for v in self.itervalues(): - res[i] = v - i += 1 - return res - - def items(self): - res = [None] * len(self) - i = 0 - for it in self.iteritems(): - res[i] = it - i += 1 - return res - - def __len__(self): - return len(self.d) - 2 - - def insert(self, key, val=None): - assert self._assert_invariants() - result = self.__setitem__(key, val) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def get(self, key, default=None): - return self.__getitem__(key, default, strictkey=False) - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - -class SmallOrderedDict(dict): - """ - SmallOrderedDict is faster than OrderedDict for small sets. How small? That - depends on your machine and which operations you use most often. Use - performance profiling to determine whether the cache class that you are - using makes any difference to the performance of your program, and if it - does, then run "quick_bench()" in test/test_cache.py to see which cache - implementation is faster for the size of your datasets. - - A simple least-recently-used cache. It keeps an LRU queue, and - when the number of items in the cache reaches maxsize, it removes - the least recently used item. - - "Looking" at an item or a key such as with "has_key()" makes that - item become the most recently used item. - - You can also use "refresh()" to explicitly make an item become the most - recently used item. - - Adding an item that is already in the dict *does* make it the - most- recently-used item although it does not change the state of - the dict itself. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return (k, dict.__getitem__(self.c, k),) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return k - - class ValueIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return dict.__getitem__(self.c, k) - - def __init__(self, initialdata={}, maxsize=128): - dict.__init__(self, initialdata) - self._lru = initialdata.keys() # contains keys - self._maxsize = maxsize - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - assert self._assert_invariants() - - def _assert_invariants(self): - _assert(len(self._lru) <= self._maxsize, "Size is required to be <= maxsize.") - _assert(len(filter(lambda x: dict.has_key(self, x), self._lru)) == len(self._lru), "Each key in self._lru is required to be in dict.", filter(lambda x: not dict.has_key(self, x), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(filter(lambda x: x in self._lru, self.keys())) == len(self), "Each key in dict is required to be in self._lru.", filter(lambda x: x not in self._lru, self.keys()), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) == len(self), "internal consistency", filter(lambda x: x not in self.keys(), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) <= self._maxsize, "internal consistency", len(self._lru), self._lru, self._maxsize) - return True - - def insert(self, key, item=None): - assert self._assert_invariants() - result = self.__setitem__(key, item) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def __setitem__(self, key, item=None): - assert self._assert_invariants() - if dict.has_key(self, key): - self._lru.remove(key) - else: - if len(self._lru) == self._maxsize: - # If this insert is going to increase the size of the cache to bigger than maxsize: - killkey = self._lru.pop(0) - dict.__delitem__(self, killkey) - dict.__setitem__(self, key, item) - self._lru.append(key) - assert self._assert_invariants() - return item - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the object removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if dict.has_key(self, key): - val = dict.__getitem__(self, key) - dict.__delitem__(self, key) - self._lru.remove(key) - assert self._assert_invariants() - return val - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def clear(self): - assert self._assert_invariants() - dict.clear(self) - self._lru = [] - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - if len(otherdict) > self._maxsize: - # Handling this special case here makes it possible to implement the - # other more common cases faster below. - dict.clear(self) - self._lru = [] - if self._maxsize > (len(otherdict) - self._maxsize): - dict.update(self, otherdict) - while len(self) > self._maxsize: - dict.popitem(self) - else: - for k, v, in otherdict.iteritems(): - if len(self) == self._maxsize: - break - dict.__setitem__(self, k, v) - self._lru = dict.keys(self) - assert self._assert_invariants() - return self - - for k in otherdict.iterkeys(): - if dict.has_key(self, k): - self._lru.remove(k) - self._lru.extend(otherdict.keys()) - dict.update(self, otherdict) - - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - - assert self._assert_invariants() - return self - - def has_key(self, key): - assert self._assert_invariants() - if dict.has_key(self, key): - assert key in self._lru, "key: %s, self._lru: %s" % tuple(map(hr, (key, self._lru,))) - self._lru.remove(key) - self._lru.append(key) - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def refresh(self, key, strictkey=True): - """ - @param strictkey: raise a KeyError exception if key isn't present - """ - assert self._assert_invariants() - if not dict.has_key(self, key): - if strictkey: - raise KeyError, key - return - self._lru.remove(key) - self._lru.append(key) - - def popitem(self): - if not self._lru: - raise KeyError, 'popitem(): dictionary is empty' - k = self._lru[-1] - obj = self.remove(k) - return (k, obj,) diff --git a/libs/pyutil/randutil.py b/libs/pyutil/randutil.py index a3efb74f..82eb3e18 100644 --- a/libs/pyutil/randutil.py +++ b/libs/pyutil/randutil.py @@ -80,6 +80,5 @@ seed = randobj.seed def randstr(n): return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) -import random as insecurerandom def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) + return os.urandom(n) diff --git a/libs/pyutil/randutil.py~ b/libs/pyutil/randutil.py~ deleted file mode 100644 index b0f1c4f9..00000000 --- a/libs/pyutil/randutil.py~ +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -import warnings -import os, random - -try: - import hashexpand - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=None, deterministic=True): - warnings.warn("deprecated", DeprecationWarning) - if not deterministic: - raise NotImplementedError, "SHA256Expander is always deterministic. For non-deterministic, try urandomRandom." - - hashexpand.SHA256Expander.__init__(self) - random.Random.__init__(self, seed) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - import increasing_timer - seed = repr(increasing_timer.time()) - hashexpand.SHA256Expander.seed(self, seed) - - - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=""): - warnings.warn("deprecated", DeprecationWarning) - hashexpand.SHA256Expander.__init__(self) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - seed = os.urandom(32) - hashexpand.SHA256Expander.seed(self, seed) -except ImportError, le: - class InsecureSHA256Random: - def __init__(self, seed=None): - raise ImportError, le - class SHA256Random: - def __init__(self, seed=""): - raise ImportError, le - -class devrandomRandom(random.Random): - """ The problem with using this one, of course, is that it blocks. This - is, of course, a security flaw. (On Linux and probably on other - systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def __init__(self): - warnings.warn("deprecated", DeprecationWarning) - self.dr = open("/dev/random", "r") - - def get(self, bytes): - return self.dr.read(bytes) - - -class devurandomRandom(random.Random): - """ The problem with using this one is that it gives answers even when it - has never been properly seeded, e.g. when you are booting from CD and have - just started up and haven't yet gathered enough entropy to actually be - unguessable. (On Linux and probably on other systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def get(self, bytes): - warnings.warn("deprecated", DeprecationWarning) - return os.urandom(bytes) - - -randobj = devurandomRandom() -get = randobj.get -random = randobj.random -randrange = randobj.randrange -shuffle = randobj.shuffle -choice = randobj.choice -seed = randobj.seed - -def randstr(n): - return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) - -import random as insecurerandom -def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) diff --git a/libs/pyutil/scripts/passphrase.py b/libs/pyutil/scripts/passphrase.py new file mode 100644 index 00000000..bed79c13 --- /dev/null +++ b/libs/pyutil/scripts/passphrase.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse, math, random + +from pyutil.mathutil import div_ceil + +from pkg_resources import resource_stream + +def recursive_subset_sum(entropy_needed, wordlists): + # Pick a minimalish set of numbers which sum to at least + # entropy_needed. + + # Okay now what's the smallest number of words which will give us + # at least this much entropy? + entropy_of_biggest_wordlist = wordlists[-1][0] + assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1] + needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist) + # How much entropy do we need from each word? + needed_entropy_per_word = entropy_needed / needed_words + # What's the smallest wordlist that offers at least this much + # entropy per word? + for (wlentropy, wl) in wordlists: + if wlentropy >= needed_entropy_per_word: + break + assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word) + + result = [(wlentropy, wl)] + # If we need more, recurse... + if wlentropy < entropy_needed: + rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists) + result.extend(rest) + return result + +def gen_passphrase(entropy, allwords): + maxlenwords = [] + i = 2 # The smallest set is words of length 1 or 2. + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + while len(maxlenwords[-1][1]) < len(allwords): + i += 1 + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + + sr = random.SystemRandom() + passphrase = [] + + wordlists_to_use = recursive_subset_sum(entropy, maxlenwords) + + passphraseentropy = 0.0 + for (wle, wl) in wordlists_to_use: + passphrase.append(sr.choice(wl)) + passphraseentropy += wle + + return (u".".join(passphrase), passphraseentropy) + +def main(): + parser = argparse.ArgumentParser(prog="chbs", description="Create a random passphrase by picking a few random words.") + + parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use chbs's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT") + parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS") + args = parser.parse_args() + + dicti = args.dictionary + if not dicti: + dicti = resource_stream('pyutil', 'data/wordlist.txt') + allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()]) + + passphrase, bits = gen_passphrase(args.bits, allwords) + + print u"Your new password is: '%s'. It is worth about %s bits." % (passphrase, bits) diff --git a/libs/pyutil/scripts/time_comparisons.py b/libs/pyutil/scripts/time_comparisons.py new file mode 100644 index 00000000..15a38852 --- /dev/null +++ b/libs/pyutil/scripts/time_comparisons.py @@ -0,0 +1,209 @@ +# If you run this file, it will make up a random secret and then crack it +# using timing information from a string comparison function. Maybe--if it +# gets lucky. It takes a long, long time to work. + +# So, the thing I need help with is statistics. The way this thing works is +# extremely stupid. Suppose you want to know which function invocation takes +# longer: comparison(secret, guess1) or comparison(secret, guess2)? + +# If you can correctly determine that one of them takes longer than the +# other, then (a) you can use that to crack the secret, and (b) this is a +# unit test demonstrating that comparison() is not timing-safe. + +# So how does this script do it? Extremely stupidly. First of all, you can't +# reliably measure tiny times, so to measure the time that a function takes, +# we run that function 10,000 times in a row, measure how long that took, and +# divide by 10,000 to estimate how long any one run would have taken. + +# Then, we do that 100 times in a row, and take the fastest of 100 runs. (I +# also experimented with taking the mean of 100 runs instead of the fastest.) + +# Then, we just say whichever comparison took longer (for its fastest run of +# 100 runs of 10,000 executions per run) is the one we think is a closer +# guess to the secret. + +# Now I would *like* to think that there is some kind of statistical analysis +# more sophisticated than "take the slowest of the fastest of 100 runs of +# 10,000 executions". Such improved statistical analysis would hopefully be +# able to answer these two questions: + +# 1. Are these two function calls -- comparison(secret, guess1) and +# comparison(secret, guess2) -- drawing from the same distribution or +# different? If you can answer that question, then you've answered the +# question of whether "comparison" is timing-safe or not. + +# And, this would also allow the cracker to recover from a false step. If it +# incorrectly decides the the prefix of the secret is ABCX, when the real +# secret is ABCD, then after that every next step it takes will be the +# "drawing from the same distribution" kind -- any difference between ABCXQ +# and ABCXR will be just due to noise, since both are equally far from the +# correct answer, which startsw with ABCD. If it could realize that there is +# no real difference between the distributions, then it could back-track and +# recover. + +# 2. Giving the ability to measure, noisily, the time taken by comparison(), +# how can you most efficiently figure out which guess takes the longest? If +# you can do that more efficiently, you can crack secrets more efficiently. + +# The script takes two arguments. The first is how many symbols in the +# secret, and the second is how big the alphabet from which the symbols are +# drawn. To prove that this script can *ever* work, try passing length 5 and +# alphabet size 2. Also try editing the code to let is use sillycomp. That'll +# definitely make it work. If you can improve this script (as per the thing +# above about "needing better statistics") to the degree that it can crack a +# secret with length 32 and alphabet size 256, then that would be awesome. + +# See the result of this commandline: + +# $ python -c 'import time_comparisons ; time_comparisons.print_measurements()' + + +from pyutil import benchutil + +import hashlib, random, os + +from decimal import Decimal +D=Decimal + +p1 = 'a'*32 +p1a = 'a'*32 +p2 = 'a'*31+'b' # close, but no cigar +p3 = 'b'*32 # different in the first byte + +def randstr(n, alphabetsize): + alphabet = [ chr(x) for x in range(alphabetsize) ] + return ''.join([random.choice(alphabet) for i in range(n)]) + +def compare(n, f, a, b): + for i in xrange(n): + f(a, b) + +def eqeqcomp(a, b): + return a == b + +def sillycomp(a, b): + # This exposes a lot of information in its timing about how many leading bytes match. + for i in range(len(a)): + if a[i] != b[i]: + return False + for i in xrange(2**9): + pass + if len(a) == len(b): + return True + else: + return False + +def hashcomp(a, b): + # Brian Warner invented this for Tahoe-LAFS. It seems like it should be very safe agaist timing leakage of any kind, because of the inclusion of a new random randkey every time. Note that exposing the value of the hash (i.e. the output of md5(randkey+secret)) is *not* a security problem. You can post that on your web site and let all attackers have it, no problem. (Provided that the value of "randkey" remains secret.) + + randkey = os.urandom(32) + return hashlib.md5(randkey+ a).digest() == hashlib.md5(randkey+b).digest() + +def xorcomp(a, b): + # This appears to be the most popular timing-insensitive string comparison function. I'm not completely sure it is fully timing-insensitive. (There are all sorts of funny things inside Python, such as caching of integer objects < 100...) + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + +def print_measurements(): + N=10**4 + REPS=10**2 + + print "all times are in nanoseconds per comparison (in scientific notation)" + print + + for comparator in [eqeqcomp, hashcomp, xorcomp, sillycomp]: + print "using comparator ", comparator + + # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: + trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] + random.shuffle(trials) + for (a, b, desc) in trials: + print "comparing two strings that are %s to each other" % (desc,) + + def f(n): + compare(n, comparator, a, b) + + benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) + + print + +def try_to_crack_secret(cracker, comparator, secretlen, alphabetsize): + secret = randstr(secretlen, alphabetsize) + + def test_guess(x): + return comparator(secret, x) + + print "Giving cracker %s a chance to figure out the secret. Don't tell him, but the secret is %s. Whenever he makes a guess, we'll use comparator %s to decide if his guess is right ..." % (cracker, secret.encode('hex'), comparator,) + + guess = cracker(test_guess, secretlen, alphabetsize) + + print "Cracker %s guessed %r" % (cracker, guess,) + if guess == secret: + print "HE FIGURED IT OUT!? HOW DID HE DO THAT." + else: + print "HAHA. Our secret is safe." + +def byte_at_a_time_cracker(test_guess, secretlen, alphabetsize): + # If we were cleverer, we'd add some backtracking behaviour where, if we can't find any x such that ABCx stands out from the crowd as taking longer than all the other ABCy's, then we start to think that we've taken a wrong step and we go back to trying ABy's. Make sense? But we're not that clever. Once we take a step, we don't backtrack. + + print + + guess=[] + + while len(guess) < secretlen: + best_next_byte = None + best_next_byte_time = None + + # For each possible byte... + for next_byte in range(alphabetsize): + c = chr(next_byte) + + # Construct a guess with our best candidate so far... + candidate_guess = guess[:] + + # Plus that byte... + candidate_guess.append(c) + s = ''.join(candidate_guess) + + # Plus random bytes... + s += os.urandom(32 - len(s)) + + # And see how long it takes the test_guess to consider it... + def f(n): + for i in xrange(n): + test_guess(s) + + times = benchutil.rep_bench(f, 10**7, MAXREPS=10**3, quiet=True) + + fastesttime = times['mean'] + + print "%s..."%(c.encode('hex'),), + if best_next_byte is None or fastesttime > best_next_byte_time: + print "new candidate for slowest next-char: %s, took: %s" % (c.encode('hex'), fastesttime,), + + best_next_byte_time = fastesttime + best_next_byte = c + + # Okay we've tried all possible next bytes. Our guess is this one (the one that took longest to be tested by test_guess): + guess.append(best_next_byte) + print "SLOWEST next-char %s! Current guess at secret: %s" % (best_next_byte.encode('hex'), ''.join(guess).encode('hex'),) + + guess = ''.join(guess) + print "Our guess for the secret: %r" % (guess,) + return guess + +if __name__ == '__main__': + import sys + secretlen = int(sys.argv[1]) + alphabetsize = int(sys.argv[2]) + if alphabetsize > 256: + raise Exception("We assume we can fit one element of the alphabet into a byte.") + + print "secretlen: %d, alphabetsize: %d" % (secretlen, alphabetsize,) + + # try_to_crack_secret(byte_at_a_time_cracker, sillycomp, secretlen, alphabetsize) + try_to_crack_secret(byte_at_a_time_cracker, eqeqcomp, secretlen, alphabetsize) diff --git a/libs/pyutil/test/current/test_mathutil.py b/libs/pyutil/test/current/test_mathutil.py index 7c189dfb..da788758 100644 --- a/libs/pyutil/test/current/test_mathutil.py +++ b/libs/pyutil/test/current/test_mathutil.py @@ -42,6 +42,13 @@ class MathUtilTestCase(unittest.TestCase): self.failUnlessEqual(f(5, 3), 2) self.failUnlessEqual(f(6, 3), 2) self.failUnlessEqual(f(7, 3), 3) + self.failUnless(isinstance(f(0.0, 1), int)) + self.failUnlessEqual(f(7.0, 3.0), 3) + self.failUnlessEqual(f(7, 3.0), 3) + self.failUnlessEqual(f(7.0, 3), 3) + self.failUnlessEqual(f(6.0, 3.0), 2) + self.failUnlessEqual(f(6.0, 3), 2) + self.failUnlessEqual(f(6, 3.0), 2) def test_next_multiple(self): f = mathutil.next_multiple diff --git a/libs/pyutil/test/current/test_mathutil.py~ b/libs/pyutil/test/current/test_mathutil.py~ new file mode 100644 index 00000000..7c189dfb --- /dev/null +++ b/libs/pyutil/test/current/test_mathutil.py~ @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import unittest + +from pyutil import mathutil +from pyutil.assertutil import _assert + +class MathUtilTestCase(unittest.TestCase): + def _help_test_is_power_of_k(self, k): + for i in range(2, 40): + _assert(mathutil.is_power_of_k(k**i, k), k, i) + + def test_is_power_of_k(self): + for i in range(2, 5): + self._help_test_is_power_of_k(i) + + def test_log_ceil(self): + f = mathutil.log_ceil + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 2), 2) + + def test_log_floor(self): + f = mathutil.log_floor + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 0) + self.failUnlessEqual(f(3, 2), 1) + + def test_div_ceil(self): + f = mathutil.div_ceil + self.failUnlessEqual(f(0, 1), 0) + self.failUnlessEqual(f(0, 2), 0) + self.failUnlessEqual(f(0, 3), 0) + self.failUnlessEqual(f(1, 3), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 3), 1) + self.failUnlessEqual(f(4, 3), 2) + self.failUnlessEqual(f(5, 3), 2) + self.failUnlessEqual(f(6, 3), 2) + self.failUnlessEqual(f(7, 3), 3) + + def test_next_multiple(self): + f = mathutil.next_multiple + self.failUnlessEqual(f(5, 1), 5) + self.failUnlessEqual(f(5, 2), 6) + self.failUnlessEqual(f(5, 3), 6) + self.failUnlessEqual(f(5, 4), 8) + self.failUnlessEqual(f(5, 5), 5) + self.failUnlessEqual(f(5, 6), 6) + self.failUnlessEqual(f(32, 1), 32) + self.failUnlessEqual(f(32, 2), 32) + self.failUnlessEqual(f(32, 3), 33) + self.failUnlessEqual(f(32, 4), 32) + self.failUnlessEqual(f(32, 5), 35) + self.failUnlessEqual(f(32, 6), 36) + self.failUnlessEqual(f(32, 7), 35) + self.failUnlessEqual(f(32, 8), 32) + self.failUnlessEqual(f(32, 9), 36) + self.failUnlessEqual(f(32, 10), 40) + self.failUnlessEqual(f(32, 11), 33) + self.failUnlessEqual(f(32, 12), 36) + self.failUnlessEqual(f(32, 13), 39) + self.failUnlessEqual(f(32, 14), 42) + self.failUnlessEqual(f(32, 15), 45) + self.failUnlessEqual(f(32, 16), 32) + self.failUnlessEqual(f(32, 17), 34) + self.failUnlessEqual(f(32, 18), 36) + self.failUnlessEqual(f(32, 589), 589) + + def test_pad_size(self): + f = mathutil.pad_size + self.failUnlessEqual(f(0, 4), 0) + self.failUnlessEqual(f(1, 4), 3) + self.failUnlessEqual(f(2, 4), 2) + self.failUnlessEqual(f(3, 4), 1) + self.failUnlessEqual(f(4, 4), 0) + self.failUnlessEqual(f(5, 4), 3) + + def test_is_power_of_k_part_2(self): + f = mathutil.is_power_of_k + for i in range(1, 100): + if i in (1, 2, 4, 8, 16, 32, 64): + self.failUnless(f(i, 2), "but %d *is* a power of 2" % i) + else: + self.failIf(f(i, 2), "but %d is *not* a power of 2" % i) + for i in range(1, 100): + if i in (1, 3, 9, 27, 81): + self.failUnless(f(i, 3), "but %d *is* a power of 3" % i) + else: + self.failIf(f(i, 3), "but %d is *not* a power of 3" % i) + + def test_next_power_of_k(self): + f = mathutil.next_power_of_k + self.failUnlessEqual(f(0,2), 1) + self.failUnlessEqual(f(1,2), 1) + self.failUnlessEqual(f(2,2), 2) + self.failUnlessEqual(f(3,2), 4) + self.failUnlessEqual(f(4,2), 4) + for i in range(5, 8): self.failUnlessEqual(f(i,2), 8, "%d" % i) + for i in range(9, 16): self.failUnlessEqual(f(i,2), 16, "%d" % i) + for i in range(17, 32): self.failUnlessEqual(f(i,2), 32, "%d" % i) + for i in range(33, 64): self.failUnlessEqual(f(i,2), 64, "%d" % i) + for i in range(65, 100): self.failUnlessEqual(f(i,2), 128, "%d" % i) + + self.failUnlessEqual(f(0,3), 1) + self.failUnlessEqual(f(1,3), 1) + self.failUnlessEqual(f(2,3), 3) + self.failUnlessEqual(f(3,3), 3) + for i in range(4, 9): self.failUnlessEqual(f(i,3), 9, "%d" % i) + for i in range(10, 27): self.failUnlessEqual(f(i,3), 27, "%d" % i) + for i in range(28, 81): self.failUnlessEqual(f(i,3), 81, "%d" % i) + for i in range(82, 200): self.failUnlessEqual(f(i,3), 243, "%d" % i) + + def test_ave(self): + f = mathutil.ave + self.failUnlessEqual(f([1,2,3]), 2) + self.failUnlessEqual(f([0,0,0,4]), 1) + self.failUnlessAlmostEqual(f([0.0, 1.0, 1.0]), .666666666666) + + def failUnlessEqualContents(self, a, b): + self.failUnlessEqual(sorted(a), sorted(b)) + + def test_permute(self): + f = mathutil.permute + self.failUnlessEqualContents(f([]), []) + self.failUnlessEqualContents(f([1]), [[1]]) + self.failUnlessEqualContents(f([1,2]), [[1,2], [2,1]]) + self.failUnlessEqualContents(f([1,2,3]), + [[1,2,3], [1,3,2], + [2,1,3], [2,3,1], + [3,1,2], [3,2,1]]) diff --git a/libs/pyutil/time_comparisons.py b/libs/pyutil/time_comparisons.py deleted file mode 100644 index ee1bcfa0..00000000 --- a/libs/pyutil/time_comparisons.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyutil import benchutil - -import hashlib, random, os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeqcomp(a, b): - return a == b - -def hashcomp(a, b): - salt = os.urandom(32) - return hashlib.md5(salt+ a).digest() == hashlib.md5(salt+b).digest() - -N=10**4 -REPS=10**2 - -print "all times are in nanoseconds per comparison (scientific notation)" -print - -for comparator in [eqeqcomp, hashcomp]: - print "using comparator ", comparator - - # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: - trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] - random.shuffle(trials) - for (a, b, desc) in trials: - print "comparing two strings that are %s to each other" % (desc,) - - def f(n): - compare(n, comparator, a, b) - - benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - - print diff --git a/libs/pyutil/time_comparisons.py~ b/libs/pyutil/time_comparisons.py~ deleted file mode 100644 index abf151ad..00000000 --- a/libs/pyutil/time_comparisons.py~ +++ /dev/null @@ -1,72 +0,0 @@ -from pyutil import benchutil - -import hashlib -import os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeq(a, b): - return a == b - -def equalsequals_s(n): - # return compare(n, eqeq, - for i in xrange(n): - p1 == p1a - -def equalsequals_c(n): - for i in xrange(n): - p1 == p2 - -def equalsequals_f(n): - for i in xrange(n): - p1 == p3 - -def hash_s(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p1a).digest() - -def hash_c(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p2).digest() - -def hash_f(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p3).digest() - -N=10**4 -REPS=10**2 - -print "using '=='" - -print "same" -benchutil.rep_bench(equalsequals_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(equalsequals_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(equalsequals_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "using hash" - -print "same" -benchutil.rep_bench(hash_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(hash_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(hash_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 0cbfa7c0..a2cef356 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -549,7 +549,7 @@ class OAuth2Mixin(object): @return_future def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None, - callback=None): + callback=None, scope=None, response_type="code"): """Redirects the user to obtain OAuth authorization for this service. Some providers require that you register a redirect URL with @@ -566,10 +566,13 @@ class OAuth2Mixin(object): """ args = { "redirect_uri": redirect_uri, - "client_id": client_id + "client_id": client_id, + "response_type": response_type } if extra_params: args.update(extra_params) + if scope: + args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) callback() @@ -945,6 +948,67 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): return OpenIdMixin.get_authenticated_user(self) +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2.""" + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = 'google_oauth' + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Google user, returning a user object. + + Example usage:: + + class GoogleOAuth2LoginHandler(LoginHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.asynchronous + @tornado.gen.coroutine + def get(self): + if self.get_argument("code", False): + user = yield self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + yield self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings["google_consumer_key"], + scope=['openid', 'email'], + response_type='code', + extra_params={"approval_prompt": "auto"}) + """ + http = self.get_auth_http_client() + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, + "code": code, + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], + "grant_type": "authorization_code", + }) + + http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + self.async_callback(self._on_access_token, callback), + method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) + + def _on_access_token(self, future, response): + """Callback function for the exchange to the access token.""" + if response.error: + future.set_exception(AuthError('Google auth error: %s' % str(response))) + return + + args = escape.json_decode(response.body) + future.set_result(args) + + def get_auth_http_client(self): + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + class FacebookMixin(object): """Facebook Connect authentication. diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 05754299..79cccb49 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -16,11 +16,15 @@ """xAutomatically restart the server when a source file is modified. -Most applications should not access this module directly. Instead, pass the -keyword argument ``debug=True`` to the `tornado.web.Application` constructor. -This will enable autoreload mode as well as checking for changes to templates -and static resources. Note that restarting is a destructive operation -and any requests in progress will be aborted when the process restarts. +Most applications should not access this module directly. Instead, +pass the keyword argument ``autoreload=True`` to the +`tornado.web.Application` constructor (or ``debug=True``, which +enables this setting and several others). This will enable autoreload +mode as well as checking for changes to templates and static +resources. Note that restarting is a destructive operation and any +requests in progress will be aborted when the process restarts. (If +you want to disable autoreload while using other debug-mode features, +pass both ``debug=True`` and ``autoreload=False``). This module can also be used as a command-line wrapper around scripts such as unit test runners. See the `main` method for details. @@ -38,6 +42,7 @@ Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) because it re-executes Python using ``sys.executable`` and ``sys.argv``. Additionally, modifying these variables will cause reloading to behave incorrectly. + """ from __future__ import absolute_import, division, print_function, with_statement diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index e0900569..cb97710a 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -360,6 +360,7 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.PROXYUSERPWD, credentials) else: curl.setopt(pycurl.PROXY, '') + curl.unsetopt(pycurl.PROXYUSERPWD) if request.validate_cert: curl.setopt(pycurl.SSL_VERIFYPEER, 1) curl.setopt(pycurl.SSL_VERIFYHOST, 2) @@ -382,6 +383,8 @@ def _curl_setup_request(curl, request, buffer, headers): # that we can't reach, so allow ipv6 unless the user asks to disable. # (but see version check in _process_queue above) curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + else: + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER) # Set the request method through curl's irritating interface which makes # up names for almost every single method @@ -404,6 +407,11 @@ def _curl_setup_request(curl, request, buffer, headers): # Handle curl's cryptic options for every individual HTTP method if request.method in ("POST", "PUT"): + if request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % request.method) + request_buffer = BytesIO(utf8(request.body)) curl.setopt(pycurl.READFUNCTION, request_buffer.read) if request.method == "POST": @@ -414,6 +422,9 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.POSTFIELDSIZE, len(request.body)) else: curl.setopt(pycurl.INFILESIZE, len(request.body)) + elif request.method == "GET": + if request.body is not None: + raise AssertionError('Body must be empty for GET request') if request.auth_username is not None: userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index 92b7458e..217ebdf5 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -38,8 +38,8 @@ since it is both shorter and provides better exception handling):: def get(self): yield gen.Task(AsyncHTTPClient().fetch, "http://example.com") -You can also yield a list of ``Futures`` and/or ``Tasks``, which will be -started at the same time and run in parallel; a list of results will +You can also yield a list or dict of ``Futures`` and/or ``Tasks``, which will be +started at the same time and run in parallel; a list or dict of results will be returned when they are all finished:: @gen.coroutine @@ -47,6 +47,13 @@ be returned when they are all finished:: http_client = AsyncHTTPClient() response1, response2 = yield [http_client.fetch(url1), http_client.fetch(url2)] + response_dict = yield dict(response3=http_client.fetch(url3), + response4=http_client.fetch(url4)) + response3 = response_dict['response3'] + response4 = response_dict['response4'] + +.. versionchanged:: 3.2 + Dict support added. For more complicated interfaces, `Task` can be split into two parts: `Callback` and `Wait`:: @@ -404,6 +411,10 @@ class Multi(YieldPoint): a list of ``YieldPoints``. """ def __init__(self, children): + self.keys = None + if isinstance(children, dict): + self.keys = list(children.keys()) + children = children.values() self.children = [] for i in children: if isinstance(i, Future): @@ -423,7 +434,11 @@ class Multi(YieldPoint): return not self.unfinished_children def get_result(self): - return [i.get_result() for i in self.children] + result = (i.get_result() for i in self.children) + if self.keys is not None: + return dict(zip(self.keys, result)) + else: + return list(result) class _NullYieldPoint(YieldPoint): @@ -523,7 +538,7 @@ class Runner(object): self.finished = True self.yield_point = _null_yield_point raise - if isinstance(yielded, list): + if isinstance(yielded, (list, dict)): yielded = Multi(yielded) elif isinstance(yielded, Future): yielded = YieldFuture(yielded) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 67675894..b58a8348 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -282,7 +282,8 @@ class HTTPRequest(object): :arg int max_redirects: Limit for ``follow_redirects`` :arg string user_agent: String to send as ``User-Agent`` header :arg bool use_gzip: Request gzip encoding from the server - :arg string network_interface: Network interface to use for request + :arg string network_interface: Network interface to use for request. + ``curl_httpclient`` only; see note below. :arg callable streaming_callback: If set, ``streaming_callback`` will be run with each chunk of data as it is received, and ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in @@ -310,14 +311,26 @@ class HTTPRequest(object): :arg bool validate_cert: For HTTPS requests, validate the server's certificate? :arg string ca_certs: filename of CA certificates in PEM format, - or None to use defaults. Note that in ``curl_httpclient``, if - any request uses a custom ``ca_certs`` file, they all must (they - don't have to all use the same ``ca_certs``, but it's not possible - to mix requests with ``ca_certs`` and requests that use the defaults. + or None to use defaults. See note below when used with + ``curl_httpclient``. :arg bool allow_ipv6: Use IPv6 when available? Default is false in ``simple_httpclient`` and true in ``curl_httpclient`` - :arg string client_key: Filename for client SSL key, if any - :arg string client_cert: Filename for client SSL certificate, if any + :arg string client_key: Filename for client SSL key, if any. See + note below when used with ``curl_httpclient``. + :arg string client_cert: Filename for client SSL certificate, if any. + See note below when used with ``curl_httpclient``. + + .. note:: + + When using ``curl_httpclient`` certain options may be + inherited by subsequent fetches because ``pycurl`` does + not allow them to be cleanly reset. This applies to the + ``ca_certs``, ``client_key``, ``client_cert``, and + ``network_interface`` arguments. If you use these + options, you should pass them on every request (you don't + have to always use the same values, but it's not possible + to mix requests that specify these options with ones that + use the defaults). .. versionadded:: 3.1 The ``auth_mode`` argument. @@ -372,6 +385,9 @@ class HTTPResponse(object): * headers: `tornado.httputil.HTTPHeaders` object + * effective_url: final location of the resource after following any + redirects + * buffer: ``cStringIO`` object for response body * body: response body as string (created on demand from ``self.buffer``) diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index d005545e..34e7b768 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -29,6 +29,7 @@ from __future__ import absolute_import, division, print_function, with_statement import socket import ssl import time +import copy from tornado.escape import native_str, parse_qs_bytes from tornado import httputil @@ -326,8 +327,8 @@ class HTTPConnection(object): self.request_callback(self._request) except _BadRequestException as e: - gen_log.info("Malformed HTTP request from %s: %s", - self.address[0], e) + gen_log.info("Malformed HTTP request from %r: %s", + self.address, e) self.close() return @@ -336,7 +337,10 @@ class HTTPConnection(object): if self._request.method in ("POST", "PATCH", "PUT"): httputil.parse_body_arguments( self._request.headers.get("Content-Type", ""), data, - self._request.arguments, self._request.files) + self._request.body_arguments, self._request.files) + + for k, v in self._request.body_arguments.items(): + self._request.arguments.setdefault(k, []).extend(v) self.request_callback(self._request) @@ -403,6 +407,20 @@ class HTTPRequest(object): `.RequestHandler.get_argument`, which returns argument values as unicode strings. + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + .. attribute:: files File uploads are available in the files property, which maps file @@ -457,6 +475,8 @@ class HTTPRequest(object): self.path, sep, self.query = uri.partition('?') self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 3e7337d9..2575bc56 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -320,7 +320,11 @@ def parse_body_arguments(content_type, body, arguments, files): with the parsed contents. """ if content_type.startswith("application/x-www-form-urlencoded"): - uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + try: + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + except Exception as e: + gen_log.warning('Invalid x-www-form-urlencoded body: %s', e) + uri_arguments = {} for name, values in uri_arguments.items(): if values: arguments.setdefault(name, []).extend(values) diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 91ee2c5b..a36ab7a5 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -676,8 +676,7 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - if self._handlers.has_key(fd): - self._handlers[fd](fd, events) + self._handlers[fd](fd, events) except (OSError, IOError) as e: if e.args[0] == errno.EPIPE: # Happens when the client closes the connection diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 6bdc6397..08430cea 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -774,7 +774,7 @@ class IOStream(BaseIOStream): # Sometimes setsockopt will fail if the socket is closed # at the wrong time. This can happen with HTTPServer # resetting the value to false between requests. - if e.errno != errno.EINVAL: + if e.errno not in (errno.EINVAL, errno.ECONNRESET): raise diff --git a/libs/tornado/log.py b/libs/tornado/log.py index fa11f379..648db5c6 100755 --- a/libs/tornado/log.py +++ b/libs/tornado/log.py @@ -51,7 +51,7 @@ gen_log = logging.getLogger("tornado.general") def _stderr_supports_color(): color = False - if curses and sys.stderr.isatty(): + if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 9dc8506e..21db4755 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -20,7 +20,6 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os -import re import socket import ssl import stat @@ -30,6 +29,13 @@ from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec from tornado.util import Configurable +if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ + ssl_match_hostname = ssl.match_hostname + SSLCertificateError = ssl.CertificateError +else: + import backports.ssl_match_hostname + ssl_match_hostname = backports.ssl_match_hostname.match_hostname + SSLCertificateError = backports.ssl_match_hostname.CertificateError def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. @@ -391,73 +397,3 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): return context.wrap_socket(socket, **kwargs) else: return ssl.wrap_socket(socket, **dict(context, **kwargs)) - -if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ - ssl_match_hostname = ssl.match_hostname - SSLCertificateError = ssl.CertificateError -else: - # match_hostname was added to the standard library ssl module in python 3.2. - # The following code was backported for older releases and copied from - # https://bitbucket.org/brandon/backports.ssl_match_hostname - class SSLCertificateError(ValueError): - pass - - def _dnsname_to_pat(dn, max_wildcards=1): - pats = [] - for frag in dn.split(r'.'): - if frag.count('*') > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survery of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise SSLCertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - if frag == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - else: - # Otherwise, '*' matches any dotless fragment. - frag = re.escape(frag) - pats.append(frag.replace(r'\*', '[^.]*')) - return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - - def ssl_match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules - are mostly followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate") - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise SSLCertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise SSLCertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise SSLCertificateError("no appropriate commonName or " - "subjectAltName fields were found") diff --git a/libs/tornado/platform/asyncio.py b/libs/tornado/platform/asyncio.py new file mode 100644 index 00000000..a8f5bad4 --- /dev/null +++ b/libs/tornado/platform/asyncio.py @@ -0,0 +1,134 @@ +"""Bridges between the `asyncio` module and Tornado IOLoop. + +This is a work in progress and interfaces are subject to change. + +To test: +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOLoop +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOMainLoop +(the tests log a few warnings with AsyncIOMainLoop because they leave some +unfinished callbacks on the event loop that fail when it resumes) +""" +import asyncio +import datetime +import functools +import os + +from tornado.ioloop import IOLoop +from tornado import stack_context + +class BaseAsyncIOLoop(IOLoop): + def initialize(self, asyncio_loop, close_loop=False): + self.asyncio_loop = asyncio_loop + self.close_loop = close_loop + self.asyncio_loop.call_soon(self.make_current) + # Maps fd to handler function (as in IOLoop.add_handler) + self.handlers = {} + # Set of fds listening for reads/writes + self.readers = set() + self.writers = set() + self.closing = False + + def close(self, all_fds=False): + self.closing = True + for fd in list(self.handlers): + self.remove_handler(fd) + if all_fds: + os.close(fd) + if self.close_loop: + self.asyncio_loop.close() + + def add_handler(self, fd, handler, events): + if fd in self.handlers: + raise ValueError("fd %d added twice" % fd) + self.handlers[fd] = stack_context.wrap(handler) + if events & IOLoop.READ: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + if events & IOLoop.WRITE: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + + def update_handler(self, fd, events): + if events & IOLoop.READ: + if fd not in self.readers: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + else: + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if events & IOLoop.WRITE: + if fd not in self.writers: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + else: + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + + def remove_handler(self, fd): + if fd not in self.handlers: + return + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + del self.handlers[fd] + + def _handle_events(self, fd, events): + self.handlers[fd](fd, events) + + def start(self): + self.asyncio_loop.run_forever() + + def stop(self): + self.asyncio_loop.stop() + + def _run_callback(self, callback, *args, **kwargs): + try: + callback(*args, **kwargs) + except Exception: + self.handle_callback_exception(callback) + + def add_timeout(self, deadline, callback): + if isinstance(deadline, (int, float)): + delay = max(deadline - self.time(), 0) + elif isinstance(deadline, datetime.timedelta): + delay = deadline.total_seconds() + else: + raise TypeError("Unsupported deadline %r", deadline) + return self.asyncio_loop.call_later(delay, self._run_callback, + stack_context.wrap(callback)) + + def remove_timeout(self, timeout): + timeout.cancel() + + def add_callback(self, callback, *args, **kwargs): + if self.closing: + raise RuntimeError("IOLoop is closing") + if kwargs: + self.asyncio_loop.call_soon_threadsafe(functools.partial( + self._run_callback, stack_context.wrap(callback), + *args, **kwargs)) + else: + self.asyncio_loop.call_soon_threadsafe( + self._run_callback, stack_context.wrap(callback), *args) + + add_callback_from_signal = add_callback + + +class AsyncIOMainLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(), + close_loop=False) + +class AsyncIOLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOLoop, self).initialize(asyncio.new_event_loop(), + close_loop=True) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index ffd2d29d..942c5c3f 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -92,7 +92,8 @@ def fork_processes(num_processes, max_restarts=100): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the debug=True option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``fork_processes``. diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index d8dbb271..2558ada8 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -72,6 +72,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.max_clients = max_clients self.queue = collections.deque() self.active = {} + self.waiting = {} self.max_buffer_size = max_buffer_size if resolver: self.resolver = resolver @@ -89,7 +90,16 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.resolver.close() def fetch_impl(self, request, callback): - self.queue.append((request, callback)) + key = object() + self.queue.append((key, request, callback)) + if not len(self.active) < self.max_clients: + timeout_handle = self.io_loop.add_timeout( + self.io_loop.time() + min(request.connect_timeout, + request.request_timeout), + functools.partial(self._on_timeout, key)) + else: + timeout_handle = None + self.waiting[key] = (request, callback, timeout_handle) self._process_queue() if self.queue: gen_log.debug("max_clients limit reached, request queued. " @@ -99,8 +109,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _process_queue(self): with stack_context.NullContext(): while self.queue and len(self.active) < self.max_clients: - request, callback = self.queue.popleft() - key = object() + key, request, callback = self.queue.popleft() + if key not in self.waiting: + continue + self._remove_timeout(key) self.active[key] = (request, callback) release_callback = functools.partial(self._release_fetch, key) self._handle_request(request, release_callback, callback) @@ -113,6 +125,22 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): del self.active[key] self._process_queue() + def _remove_timeout(self, key): + if key in self.waiting: + request, callback, timeout_handle = self.waiting[key] + if timeout_handle is not None: + self.io_loop.remove_timeout(timeout_handle) + del self.waiting[key] + + def _on_timeout(self, key): + request, callback, timeout_handle = self.waiting[key] + self.queue.remove((key, request, callback)) + timeout_response = HTTPResponse( + request, 599, error=HTTPError(599, "Timeout"), + request_time=self.io_loop.time() - request.start_time) + self.io_loop.add_callback(callback, timeout_response) + del self.waiting[key] + class _HTTPConnection(object): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) @@ -162,15 +190,18 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) self.resolver.resolve(host, port, af, callback=self._on_resolve) def _on_resolve(self, addrinfo): + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout + return self.stream = self._create_stream(addrinfo) - timeout = min(self.request.connect_timeout, self.request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) self.stream.set_close_callback(self._on_close) # ipv6 addresses are broken (in self.parsed.hostname) until # 2.7, here is correctly parsed value calculated in __init__ @@ -199,10 +230,10 @@ class _HTTPConnection(object): # the SSL_OP_NO_SSLv2, but that wasn't exposed to python # until 3.2. Python 2.7 adds the ciphers argument, which # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to SSLv3. This is + # on python 2.6, we set ssl_version to TLSv1. This is # more narrow than we'd like since it also breaks - # compatibility with servers configured for TLSv1 only, - # but nearly all servers support SSLv3: + # compatibility with servers configured for SSLv3 only, + # but nearly all servers support both SSLv3 and TLSv1: # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html if sys.version_info >= (2, 7): ssl_options["ciphers"] = "DEFAULT:!SSLv2" @@ -210,7 +241,7 @@ class _HTTPConnection(object): # This is really only necessary for pre-1.0 versions # of openssl, but python 2.6 doesn't expose version # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 return SSLIOStream(socket.socket(af), io_loop=self.io_loop, @@ -233,6 +264,8 @@ class _HTTPConnection(object): def _on_connect(self): self._remove_timeout() + if self.final_callback is None: + return if self.request.request_timeout: self._timeout = self.io_loop.add_timeout( self.start_time + self.request.request_timeout, @@ -269,9 +302,15 @@ class _HTTPConnection(object): self.request.headers["User-Agent"] = self.request.user_agent if not self.request.allow_nonstandard_methods: if self.request.method in ("POST", "PATCH", "PUT"): - assert self.request.body is not None + if self.request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % self.request.method) else: - assert self.request.body is None + if self.request.body is not None: + raise AssertionError( + 'Body must be empty for "%s" request' + % self.request.method) if self.request.body is not None: self.request.headers["Content-Length"] = str(len( self.request.body)) diff --git a/libs/tornado/speedups.c b/libs/tornado/speedups.c new file mode 100644 index 00000000..8a316c58 --- /dev/null +++ b/libs/tornado/speedups.c @@ -0,0 +1,49 @@ +#include + +static PyObject* websocket_mask(PyObject* self, PyObject* args) { + const char* mask; + int mask_len; + const char* data; + int data_len; + int i; + + if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { + return NULL; + } + + PyObject* result = PyBytes_FromStringAndSize(NULL, data_len); + if (!result) { + return NULL; + } + char* buf = PyBytes_AsString(result); + for (i = 0; i < data_len; i++) { + buf[i] = data[i] ^ mask[i % 4]; + } + + return result; +} + +static PyMethodDef methods[] = { + {"websocket_mask", websocket_mask, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef speedupsmodule = { + PyModuleDef_HEAD_INIT, + "speedups", + NULL, + -1, + methods +}; + +PyMODINIT_FUNC +PyInit_speedups() { + return PyModule_Create(&speedupsmodule); +} +#else // Python 2.x +PyMODINIT_FUNC +initspeedups() { + Py_InitModule("tornado.speedups", methods); +} +#endif diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index 8473a21a..c0773732 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -180,7 +180,8 @@ class TCPServer(object): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the ``debug=True`` option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``TCPServer.start(n)``. """ diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 5f8d6091..b6d7e97e 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -250,7 +250,7 @@ class RequestHandler(object): not self.request.connection.no_keep_alive): conn_header = self.request.headers.get("Connection") if conn_header and (conn_header.lower() == "keep-alive"): - self.set_header("Connection", "Keep-Alive") + self._headers["Connection"] = "Keep-Alive" self._write_buffer = [] self._status_code = 200 self._reason = httputil.responses[200] @@ -348,12 +348,7 @@ class RequestHandler(object): The returned value is always unicode. """ - args = self.get_arguments(name, strip=strip) - if not args: - if default is self._ARG_DEFAULT: - raise MissingArgumentError(name) - return default - return args[-1] + return self._get_argument(name, default, self.request.arguments, strip) def get_arguments(self, name, strip=True): """Returns a list of the arguments with the given name. @@ -362,9 +357,73 @@ class RequestHandler(object): The returned values are always unicode. """ + return self._get_arguments(name, self.request.arguments, strip) + def get_body_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request body. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.body_arguments, strip) + + def get_body_arguments(self, name, strip=True): + """Returns a list of the body arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.body_arguments, strip) + + def get_query_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request query string. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.query_arguments, strip) + + def get_query_arguments(self, name, strip=True): + """Returns a list of the query arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.query_arguments, strip) + + def _get_argument(self, name, default, source, strip=True): + args = self._get_arguments(name, source, strip=strip) + if not args: + if default is self._ARG_DEFAULT: + raise MissingArgumentError(name) + return default + return args[-1] + + def _get_arguments(self, name, source, strip=True): values = [] - for v in self.request.arguments.get(name, []): + for v in source.get(name, []): v = self.decode_argument(v, name=name) if isinstance(v, unicode_type): # Get rid of any weird control chars (unless decoding gave @@ -838,7 +897,7 @@ class RequestHandler(object): else: self.finish(self.get_error_html(status_code, **kwargs)) return - if self.settings.get("debug") and "exc_info" in kwargs: + if self.settings.get("serve_traceback") and "exc_info" in kwargs: # in debug mode, try to send a traceback self.set_header('Content-Type', 'text/plain') for line in traceback.format_exception(*kwargs["exc_info"]): @@ -1318,6 +1377,12 @@ def asynchronous(method): if not self._finished: self.finish() IOLoop.current().add_future(result, future_complete) + # Once we have done this, hide the Future from our + # caller (i.e. RequestHandler._when_complete), which + # would otherwise set up its own callback and + # exception handler (resulting in exceptions being + # logged twice). + return None return result return wrapper @@ -1383,10 +1448,16 @@ class Application(object): or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. + The request class can be specified as either a class object or a + (fully-qualified) name. - Each tuple can contain an optional third element, which should be - a dictionary if it is present. That dictionary is passed as - keyword arguments to the contructor of the handler. This pattern + Each tuple can contain additional elements, which correspond to the + arguments to the `URLSpec` constructor. (Prior to Tornado 3.2, this + only tuples of two or three elements were allowed). + + A dictionary may be passed as the third element of the tuple, + which will be used as keyword arguments to the handler's + constructor and `~RequestHandler.initialize` method. This pattern is used for the `StaticFileHandler` in this example (note that a `StaticFileHandler` can be installed automatically with the static_path setting described below):: @@ -1409,6 +1480,7 @@ class Application(object): and ``/robots.txt`` from the same directory. A custom subclass of `StaticFileHandler` can be specified with the ``static_handler_class`` setting. + """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1447,8 +1519,14 @@ class Application(object): if handlers: self.add_handlers(".*$", handlers) + if self.settings.get('debug'): + self.settings.setdefault('autoreload', True) + self.settings.setdefault('compiled_template_cache', False) + self.settings.setdefault('static_hash_cache', False) + self.settings.setdefault('serve_traceback', True) + # Automatically reload modified modules - if self.settings.get("debug") and not wsgi: + if self.settings.get('autoreload') and not wsgi: from tornado import autoreload autoreload.start() @@ -1493,20 +1571,8 @@ class Application(object): for spec in host_handlers: if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3) - pattern = spec[0] - handler = spec[1] - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - if len(spec) == 3: - kwargs = spec[2] - else: - kwargs = {} - spec = URLSpec(pattern, handler, kwargs) + assert len(spec) in (2, 3, 4) + spec = URLSpec(*spec) handlers.append(spec) if spec.name: if spec.name in self.named_handlers: @@ -1597,14 +1663,23 @@ class Application(object): args = [unquote(s) for s in match.groups()] break if not handler: - handler = ErrorHandler(self, request, status_code=404) + if self.settings.get('default_handler_class'): + handler_class = self.settings['default_handler_class'] + handler_args = self.settings.get( + 'default_handler_args', {}) + else: + handler_class = ErrorHandler + handler_args = dict(status_code=404) + handler = handler_class(self, request, **handler_args) - # In debug mode, re-compile templates and reload static files on every + # If template cache is disabled (usually in the debug mode), + # re-compile templates and reload static files on every # request so you don't need to restart to see changes - if self.settings.get("debug"): + if not self.settings.get("compiled_template_cache", True): with RequestHandler._template_loader_lock: for loader in RequestHandler._template_loaders.values(): loader.reset() + if not self.settings.get('static_hash_cache', True): StaticFileHandler.reset() handler._execute(transforms, *args, **kwargs) @@ -2454,7 +2529,7 @@ class _UIModuleNamespace(object): class URLSpec(object): """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler_class, kwargs=None, name=None): + def __init__(self, pattern, handler, kwargs=None, name=None): """Parameters: * ``pattern``: Regular expression to be matched. Any groups @@ -2475,7 +2550,13 @@ class URLSpec(object): assert len(self.regex.groupindex) in (0, self.regex.groups), \ ("groups in url regexes must either be all named or all " "positional: %r" % self.regex.pattern) - self.handler_class = handler_class + + if isinstance(handler, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + handler = import_object(handler) + + self.handler_class = handler self.kwargs = kwargs or {} self.name = name self._path, self._group_count = self._find_groups() diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 676d21bf..8c2f5a64 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -33,7 +33,7 @@ import tornado.web from tornado.concurrent import TracebackFuture from tornado.escape import utf8, native_str -from tornado import httpclient +from tornado import httpclient, httputil from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log @@ -52,6 +52,10 @@ class WebSocketError(Exception): class WebSocketClosedError(WebSocketError): + """Raised by operations on a closed connection. + + .. versionadded:: 3.2 + """ pass @@ -163,6 +167,12 @@ class WebSocketHandler(tornado.web.RequestHandler): encoded as json). If the ``binary`` argument is false, the message will be sent as utf8; in binary mode any byte string is allowed. + + If the connection is already closed, raises `WebSocketClosedError`. + + .. versionchanged:: 3.2 + `WebSocketClosedError` was added (previously a closed connection + would raise an `AttributeError`) """ if self.ws_connection is None: raise WebSocketClosedError() @@ -586,7 +596,7 @@ class WebSocketProtocol13(WebSocketProtocol): frame += struct.pack("!BQ", 127 | mask_bit, l) if self.mask_outgoing: mask = os.urandom(4) - data = mask + self._apply_mask(mask, data) + data = mask + _websocket_mask(mask, data) frame += data self.stream.write(frame) @@ -671,21 +681,8 @@ class WebSocketProtocol13(WebSocketProtocol): except StreamClosedError: self._abort() - def _apply_mask(self, mask, data): - mask = array.array("B", mask) - unmasked = array.array("B", data) - for i in xrange(len(data)): - unmasked[i] = unmasked[i] ^ mask[i % 4] - if hasattr(unmasked, 'tobytes'): - # tostring was deprecated in py32. It hasn't been removed, - # but since we turn on deprecation warnings in our tests - # we need to use the right one. - return unmasked.tobytes() - else: - return unmasked.tostring() - def _on_masked_frame_data(self, data): - self._on_frame_data(self._apply_mask(self._frame_mask, data)) + self._on_frame_data(_websocket_mask(self._frame_mask, data)) def _on_frame_data(self, data): if self._frame_opcode_is_control: @@ -771,7 +768,11 @@ class WebSocketProtocol13(WebSocketProtocol): class WebSocketClientConnection(simple_httpclient._HTTPConnection): - """WebSocket client connection.""" + """WebSocket client connection. + + This class should not be instantiated directly; use the + `websocket_connect` function instead. + """ def __init__(self, io_loop, request): self.connect_future = TracebackFuture() self.read_future = None @@ -793,9 +794,19 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): io_loop, None, request, lambda: None, self._on_http_response, 104857600, self.resolver) + def close(self): + """Closes the websocket connection. + + .. versionadded:: 3.2 + """ + if self.protocol is not None: + self.protocol.close() + self.protocol = None + def _on_close(self): self.on_message(None) self.resolver.close() + super(WebSocketClientConnection, self)._on_close() def _on_http_response(self, response): if not self.connect_future.done(): @@ -859,13 +870,54 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): Takes a url and returns a Future whose result is a `WebSocketClientConnection`. + + .. versionchanged:: 3.2 + Also accepts ``HTTPRequest`` objects in place of urls. """ if io_loop is None: io_loop = IOLoop.current() - request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) + if isinstance(url, httpclient.HTTPRequest): + assert connect_timeout is None + request = url + # Copy and convert the headers dict/object (see comments in + # AsyncHTTPClient.fetch) + request.headers = httputil.HTTPHeaders(request.headers) + else: + request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) request = httpclient._RequestProxy( request, httpclient.HTTPRequest._DEFAULTS) conn = WebSocketClientConnection(io_loop, request) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future + +def _websocket_mask_python(mask, data): + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. + Returns a `bytes` object of the same length as `data` with the mask applied + as specified in section 5.3 of RFC 6455. + + This pure-python implementation may be replaced by an optimized version when available. + """ + mask = array.array("B", mask) + unmasked = array.array("B", data) + for i in xrange(len(data)): + unmasked[i] = unmasked[i] ^ mask[i % 4] + if hasattr(unmasked, 'tobytes'): + # tostring was deprecated in py32. It hasn't been removed, + # but since we turn on deprecation warnings in our tests + # we need to use the right one. + return unmasked.tobytes() + else: + return unmasked.tostring() + +if os.environ.get('TORNADO_NO_EXTENSION'): + # This environment variable exists to make it easier to do performance comparisons; + # it's not guaranteed to remain supported in the future. + _websocket_mask = _websocket_mask_python +else: + try: + from tornado.speedups import websocket_mask as _websocket_mask + except ImportError: + _websocket_mask = _websocket_mask_python diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 5e25a564..8e5ddedb 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -33,6 +33,7 @@ from __future__ import absolute_import, division, print_function, with_statement import sys import time +import copy import tornado from tornado import escape @@ -142,11 +143,14 @@ class HTTPRequest(object): self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) self.uri = self.path self.arguments = {} + self.query_arguments = {} + self.body_arguments = {} self.query = environ.get("QUERY_STRING", "") if self.query: self.uri += "?" + self.query self.arguments = parse_qs_bytes(native_str(self.query), keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) self.version = "HTTP/1.1" self.headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): @@ -171,7 +175,10 @@ class HTTPRequest(object): # Parse request body self.files = {} httputil.parse_body_arguments(self.headers.get("Content-Type", ""), - self.body, self.arguments, self.files) + self.body, self.body_arguments, self.files) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) self._start_time = time.time() self._finish_time = None
self.processSpaceCharactersNonPre = self.processSpaceCharacters self.startTagHandler = utils.MethodDispatcher([ ("html", self.startTagHtml), - (("base", "basefont", "bgsound", "command", "link", "meta", - "noframes", "script", "style", "title"), + (("base", "basefont", "bgsound", "command", "link", "meta", + "noframes", "script", "style", "title"), self.startTagProcessInHead), ("body", self.startTagBody), ("frameset", self.startTagFrameset), (("address", "article", "aside", "blockquote", "center", "details", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "menu", "nav", "ol", "p", + "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", "section", "summary", "ul"), - self.startTagCloseP), + self.startTagCloseP), (headingElements, self.startTagHeading), (("pre", "listing"), self.startTagPreListing), ("form", self.startTagForm), (("li", "dd", "dt"), self.startTagListItem), - ("plaintext",self.startTagPlaintext), + ("plaintext", self.startTagPlaintext), ("a", self.startTagA), - (("b", "big", "code", "em", "font", "i", "s", "small", "strike", - "strong", "tt", "u"),self.startTagFormatting), + (("b", "big", "code", "em", "font", "i", "s", "small", "strike", + "strong", "tt", "u"), self.startTagFormatting), ("nobr", self.startTagNobr), ("button", self.startTagButton), (("applet", "marquee", "object"), self.startTagAppletMarqueeObject), @@ -961,21 +912,21 @@ def getPhases(debug): self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("body",self.endTagBody), - ("html",self.endTagHtml), - (("address", "article", "aside", "blockquote", "center", - "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", + ("body", self.endTagBody), + ("html", self.endTagHtml), + (("address", "article", "aside", "blockquote", "button", "center", + "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "section", "summary", "ul"), self.endTagBlock), ("form", self.endTagForm), - ("p",self.endTagP), + ("p", self.endTagP), (("dd", "dt", "li"), self.endTagListItem), (headingElements, self.endTagHeading), (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"), self.endTagFormatting), - (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), + (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), ("br", self.endTagBr), - ]) + ]) self.endTagHandler.default = self.endTagOther def isMatchingFormattingElement(self, node1, node2): @@ -995,14 +946,14 @@ def getPhases(debug): def addFormattingElement(self, token): self.tree.insertElement(token) element = self.tree.openElements[-1] - + matchingElements = [] for node in self.tree.activeFormattingElements[::-1]: if node is Marker: break elif self.isMatchingFormattingElement(node, element): matchingElements.append(node) - + assert len(matchingElements) <= 3 if len(matchingElements) == 3: self.tree.activeFormattingElements.remove(matchingElements[-1]) @@ -1017,7 +968,7 @@ def getPhases(debug): if node.name not in allowed_elements: self.parser.parseError("expected-closing-tag-but-got-eof") break - #Stop parsing + # Stop parsing def processSpaceCharactersDropNewline(self, token): # Sometimes (start of , , and blocks) we @@ -1026,19 +977,19 @@ def getPhases(debug): self.processSpaceCharacters = self.processSpaceCharactersNonPre if (data.startswith("\n") and self.tree.openElements[-1].name in ("pre", "listing", "textarea") - and not self.tree.openElements[-1].hasContent()): + and not self.tree.openElements[-1].hasContent()): data = data[1:] if data: self.tree.reconstructActiveFormattingElements() self.tree.insertText(data) def processCharacters(self, token): - if token["data"] == u"\u0000": - #The tokenizer should always emit null on its own + if token["data"] == "\u0000": + # The tokenizer should always emit null on its own return self.tree.reconstructActiveFormattingElements() self.tree.insertText(token["data"]) - #This must be bad for performance + # This must be bad for performance if (self.parser.framesetOK and any([char not in spaceCharacters for char in token["data"]])): @@ -1054,11 +1005,11 @@ def getPhases(debug): def startTagBody(self, token): self.parser.parseError("unexpected-start-tag", {"name": "body"}) if (len(self.tree.openElements) == 1 - or self.tree.openElements[1].name != "body"): + or self.tree.openElements[1].name != "body"): assert self.parser.innerHTML else: self.parser.framesetOK = False - for attr, value in token["data"].iteritems(): + for attr, value in token["data"].items(): if attr not in self.tree.openElements[1].attributes: self.tree.openElements[1].attributes[attr] = value @@ -1090,7 +1041,7 @@ def getPhases(debug): def startTagForm(self, token): if self.tree.formPointer: - self.parser.parseError(u"unexpected-start-tag", {"name": "form"}) + self.parser.parseError("unexpected-start-tag", {"name": "form"}) else: if self.tree.elementInScope("p", variant="button"): self.endTagP(impliedTagToken("p")) @@ -1100,9 +1051,9 @@ def getPhases(debug): def startTagListItem(self, token): self.parser.framesetOK = False - stopNamesMap = {"li":["li"], - "dt":["dt", "dd"], - "dd":["dt", "dd"]} + stopNamesMap = {"li": ["li"], + "dt": ["dt", "dd"], + "dd": ["dt", "dd"]} stopNames = stopNamesMap[token["name"]] for node in reversed(self.tree.openElements): if node.name in stopNames: @@ -1110,7 +1061,7 @@ def getPhases(debug): impliedTagToken(node.name, "EndTag")) break if (node.nameTuple in specialElements and - node.name not in ("address", "div", "p")): + node.name not in ("address", "div", "p")): break if self.tree.elementInScope("p", variant="button"): @@ -1137,7 +1088,7 @@ def getPhases(debug): afeAElement = self.tree.elementInActiveFormattingElements("a") if afeAElement: self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "a", "endName": "a"}) + {"startName": "a", "endName": "a"}) self.endTagFormatting(impliedTagToken("a")) if afeAElement in self.tree.openElements: self.tree.openElements.remove(afeAElement) @@ -1154,7 +1105,7 @@ def getPhases(debug): self.tree.reconstructActiveFormattingElements() if self.tree.elementInScope("nobr"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "nobr", "endName": "nobr"}) + {"startName": "nobr", "endName": "nobr"}) self.processEndTag(impliedTagToken("nobr")) # XXX Need tests that trigger the following self.tree.reconstructActiveFormattingElements() @@ -1163,7 +1114,7 @@ def getPhases(debug): def startTagButton(self, token): if self.tree.elementInScope("button"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "button", "endName": "button"}) + {"startName": "button", "endName": "button"}) self.processEndTag(impliedTagToken("button")) return token else: @@ -1203,8 +1154,8 @@ def getPhases(debug): framesetOK = self.parser.framesetOK self.startTagVoidFormatting(token) if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): - #input type=hidden doesn't change framesetOK + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + # input type=hidden doesn't change framesetOK self.parser.framesetOK = framesetOK def startTagParamSource(self, token): @@ -1223,7 +1174,7 @@ def getPhases(debug): def startTagImage(self, token): # No really... self.parser.parseError("unexpected-start-tag-treated-as", - {"originalName": "image", "newName": "img"}) + {"originalName": "image", "newName": "img"}) self.processStartTag(impliedTagToken("img", "StartTag", attributes=token["data"], selfClosing=token["selfClosing"])) @@ -1243,18 +1194,18 @@ def getPhases(debug): if "prompt" in token["data"]: prompt = token["data"]["prompt"] else: - prompt = u"This is a searchable index. Enter search keywords: " + prompt = "This is a searchable index. Enter search keywords: " self.processCharacters( - {"type":tokenTypes["Characters"], "data":prompt}) + {"type": tokenTypes["Characters"], "data": prompt}) attributes = token["data"].copy() if "action" in attributes: del attributes["action"] if "prompt" in attributes: del attributes["prompt"] attributes["name"] = "isindex" - self.processStartTag(impliedTagToken("input", "StartTag", - attributes = attributes, - selfClosing = + self.processStartTag(impliedTagToken("input", "StartTag", + attributes=attributes, + selfClosing= token["selfClosing"])) self.processEndTag(impliedTagToken("label")) self.processStartTag(impliedTagToken("hr", "StartTag")) @@ -1287,7 +1238,7 @@ def getPhases(debug): if self.parser.phase in (self.parser.phases["inTable"], self.parser.phases["inCaption"], self.parser.phases["inColumnGroup"], - self.parser.phases["inTableBody"], + self.parser.phases["inTableBody"], self.parser.phases["inRow"], self.parser.phases["inCell"]): self.parser.phase = self.parser.phases["inSelectInTable"] @@ -1307,8 +1258,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["mathml"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1319,8 +1270,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["svg"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1362,7 +1313,7 @@ def getPhases(debug): "tbody", "td", "tfoot", "th", "thead", "tr", "body", "html")): - #Not sure this is the correct name for the parse error + # Not sure this is the correct name for the parse error self.parser.parseError( "expected-one-end-tag-but-got-another", {"expectedName": "body", "gotName": node.name}) @@ -1370,20 +1321,20 @@ def getPhases(debug): self.parser.phase = self.parser.phases["afterBody"] def endTagHtml(self, token): - #We repeat the test for the body end tag token being ignored here + # We repeat the test for the body end tag token being ignored here if self.tree.elementInScope("body"): self.endTagBody(impliedTagToken("body")) return token def endTagBlock(self, token): - #Put us back in the right whitespace handling mode + # Put us back in the right whitespace handling mode if token["name"] == "pre": self.processSpaceCharacters = self.processSpaceCharactersNonPre inScope = self.tree.elementInScope(token["name"]) if inScope: self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != token["name"]: - self.parser.parseError("end-tag-too-early", {"name": token["name"]}) + self.parser.parseError("end-tag-too-early", {"name": token["name"]}) if inScope: node = self.tree.openElements.pop() while node.name != token["name"]: @@ -1394,7 +1345,7 @@ def getPhases(debug): self.tree.formPointer = None if node is None or not self.tree.elementInScope(node): self.parser.parseError("unexpected-end-tag", - {"name":"form"}) + {"name": "form"}) else: self.tree.generateImpliedEndTags() if self.tree.openElements[-1] != node: @@ -1410,7 +1361,7 @@ def getPhases(debug): if not self.tree.elementInScope(token["name"], variant=variant): self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) else: - self.tree.generateImpliedEndTags(exclude = token["name"]) + self.tree.generateImpliedEndTags(exclude=token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError( "end-tag-too-early", @@ -1436,65 +1387,105 @@ def getPhases(debug): def endTagFormatting(self, token): """The much-feared adoption agency algorithm""" - # http://www.whatwg.org/specs/web-apps/current-work/#adoptionAgency + # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867 # XXX Better parseError messages appreciated. - name = token["name"] + # Step 1 outerLoopCounter = 0 + + # Step 2 while outerLoopCounter < 8: + + # Step 3 outerLoopCounter += 1 - # Step 1 paragraph 1 + # Step 4: + + # Let the formatting element be the last element in + # the list of active formatting elements that: + # - is between the end of the list and the last scope + # marker in the list, if any, or the start of the list + # otherwise, and + # - has the same tag name as the token. formattingElement = self.tree.elementInActiveFormattingElements( token["name"]) - if (not formattingElement or + if (not formattingElement or (formattingElement in self.tree.openElements and not self.tree.elementInScope(formattingElement.name))): - self.parser.parseError("adoption-agency-1.1", {"name": token["name"]}) + # If there is no such node, then abort these steps + # and instead act as described in the "any other + # end tag" entry below. + self.endTagOther(token) return - # Step 1 paragraph 2 + # Otherwise, if there is such a node, but that node is + # not in the stack of open elements, then this is a + # parse error; remove the element from the list, and + # abort these steps. elif formattingElement not in self.tree.openElements: self.parser.parseError("adoption-agency-1.2", {"name": token["name"]}) self.tree.activeFormattingElements.remove(formattingElement) return - # Step 1 paragraph 3 - if formattingElement != self.tree.openElements[-1]: - self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + # Otherwise, if there is such a node, and that node is + # also in the stack of open elements, but the element + # is not in scope, then this is a parse error; ignore + # the token, and abort these steps. + elif not self.tree.elementInScope(formattingElement.name): + self.parser.parseError("adoption-agency-4.4", {"name": token["name"]}) + return - # Step 2 - # Start of the adoption agency algorithm proper + # Otherwise, there is a formatting element and that + # element is in the stack and is in scope. If the + # element is not the current node, this is a parse + # error. In any case, proceed with the algorithm as + # written in the following steps. + else: + if formattingElement != self.tree.openElements[-1]: + self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + + # Step 5: + + # Let the furthest block be the topmost node in the + # stack of open elements that is lower in the stack + # than the formatting element, and is an element in + # the special category. There might not be one. afeIndex = self.tree.openElements.index(formattingElement) furthestBlock = None for element in self.tree.openElements[afeIndex:]: if element.nameTuple in specialElements: furthestBlock = element break - # Step 3 + + # Step 6: + + # If there is no furthest block, then the UA must + # first pop all the nodes from the bottom of the stack + # of open elements, from the current node up to and + # including the formatting element, then remove the + # formatting element from the list of active + # formatting elements, and finally abort these steps. if furthestBlock is None: element = self.tree.openElements.pop() while element != formattingElement: element = self.tree.openElements.pop() self.tree.activeFormattingElements.remove(element) return - commonAncestor = self.tree.openElements[afeIndex-1] - # Step 5 - #if furthestBlock.parent: - # furthestBlock.parent.removeChild(furthestBlock) + # Step 7 + commonAncestor = self.tree.openElements[afeIndex - 1] - # Step 5 + # Step 8: # The bookmark is supposed to help us identify where to reinsert - # nodes in step 12. We have to ensure that we reinsert nodes after + # nodes in step 15. We have to ensure that we reinsert nodes after # the node before the active formatting element. Note the bookmark - # can move in step 7.4 + # can move in step 9.7 bookmark = self.tree.activeFormattingElements.index(formattingElement) - # Step 6 + # Step 9 lastNode = node = furthestBlock innerLoopCounter = 0 - + index = self.tree.openElements.index(node) while innerLoopCounter < 3: innerLoopCounter += 1 @@ -1504,15 +1495,13 @@ def getPhases(debug): if node not in self.tree.activeFormattingElements: self.tree.openElements.remove(node) continue - # Step 6.3 + # Step 9.6 if node == formattingElement: break - # Step 6.4 + # Step 9.7 if lastNode == furthestBlock: - bookmark = (self.tree.activeFormattingElements.index(node) - + 1) - # Step 6.5 - #cite = node.parent + bookmark = self.tree.activeFormattingElements.index(node) + 1 + # Step 9.8 clone = node.cloneNode() # Replace node with clone self.tree.activeFormattingElements[ @@ -1520,20 +1509,18 @@ def getPhases(debug): self.tree.openElements[ self.tree.openElements.index(node)] = clone node = clone - - # Step 6.6 + # Step 9.9 # Remove lastNode from its parents, if any if lastNode.parent: lastNode.parent.removeChild(lastNode) node.appendChild(lastNode) - # Step 7.7 + # Step 9.10 lastNode = node - # End of inner loop - # Step 7 + # Step 10 # Foster parent lastNode if commonAncestor is a - # table, tbody, tfoot, thead, or tr we need to foster parent the - # lastNode + # table, tbody, tfoot, thead, or tr we need to foster + # parent the lastNode if lastNode.parent: lastNode.parent.removeChild(lastNode) @@ -1543,23 +1530,23 @@ def getPhases(debug): else: commonAncestor.appendChild(lastNode) - # Step 8 + # Step 11 clone = formattingElement.cloneNode() - # Step 9 + # Step 12 furthestBlock.reparentChildren(clone) - # Step 10 + # Step 13 furthestBlock.appendChild(clone) - # Step 11 + # Step 14 self.tree.activeFormattingElements.remove(formattingElement) self.tree.activeFormattingElements.insert(bookmark, clone) - # Step 12 + # Step 15 self.tree.openElements.remove(formattingElement) self.tree.openElements.insert( - self.tree.openElements.index(furthestBlock) + 1, clone) + self.tree.openElements.index(furthestBlock) + 1, clone) def endTagAppletMarqueeObject(self, token): if self.tree.elementInScope(token["name"]): @@ -1575,7 +1562,7 @@ def getPhases(debug): def endTagBr(self, token): self.parser.parseError("unexpected-end-tag-treated-as", - {"originalName": "br", "newName": "br element"}) + {"originalName": "br", "newName": "br element"}) self.tree.reconstructActiveFormattingElements() self.tree.insertElement(impliedTagToken("br", "StartTag")) self.tree.openElements.pop() @@ -1600,31 +1587,31 @@ def getPhases(debug): self.startTagHandler = utils.MethodDispatcher([]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("script", self.endTagScript)]) + ("script", self.endTagScript)]) self.endTagHandler.default = self.endTagOther def processCharacters(self, token): self.tree.insertText(token["data"]) def processEOF(self): - self.parser.parseError("expected-named-closing-tag-but-got-eof", - self.tree.openElements[-1].name) + self.parser.parseError("expected-named-closing-tag-but-got-eof", + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase return True def startTagOther(self, token): - assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode"%token['name'] + assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name'] def endTagScript(self, token): node = self.tree.openElements.pop() assert node.name == "script" self.parser.phase = self.parser.originalPhase - #The rest of this method is all stuff that only happens if - #document.write works + # The rest of this method is all stuff that only happens if + # document.write works def endTagOther(self, token): - node = self.tree.openElements.pop() + self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase class InTablePhase(Phase): @@ -1656,7 +1643,7 @@ def getPhases(debug): def clearStackToTableContext(self): # "clear the stack back to a table context" while self.tree.openElements[-1].name not in ("table", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() # When the current node is it's an innerHTML case @@ -1667,7 +1654,7 @@ def getPhases(debug): self.parser.parseError("eof-in-table") else: assert self.parser.innerHTML - #Stop parsing + # Stop parsing def processSpaceCharacters(self, token): originalPhase = self.parser.phase @@ -1682,7 +1669,7 @@ def getPhases(debug): self.parser.phase.processCharacters(token) def insertText(self, token): - #If we get here there must be at least one non-whitespace character + # If we get here there must be at least one non-whitespace character # Do the table magic! self.tree.insertFromTable = True self.parser.phases["inBody"].processCharacters(token) @@ -1714,7 +1701,7 @@ def getPhases(debug): def startTagTable(self, token): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "table", "endName": "table"}) + {"startName": "table", "endName": "table"}) self.parser.phase.processEndTag(impliedTagToken("table")) if not self.parser.innerHTML: return token @@ -1723,8 +1710,8 @@ def getPhases(debug): return self.parser.phases["inHead"].processStartTag(token) def startTagInput(self, token): - if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + if ("type" in token["data"] and + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): self.parser.parseError("unexpected-hidden-input-in-table") self.tree.insertElement(token) # XXX associate with form @@ -1751,8 +1738,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "table": self.parser.parseError("end-tag-too-early-named", - {"gotName": "table", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "table", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "table": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1781,7 +1768,7 @@ def getPhases(debug): def flushCharacters(self): data = "".join([item["data"] for item in self.characterTokens]) if any([item not in spaceCharacters for item in data]): - token = {"type":tokenTypes["Characters"], "data":data} + token = {"type": tokenTypes["Characters"], "data": data} self.parser.phases["inTable"].insertText(token) elif data: self.tree.insertText(data) @@ -1798,12 +1785,12 @@ def getPhases(debug): return True def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.characterTokens.append(token) def processSpaceCharacters(self, token): - #pretty sure we should never reach here + # pretty sure we should never reach here self.characterTokens.append(token) # assert False @@ -1817,7 +1804,6 @@ def getPhases(debug): self.parser.phase = self.originalPhase return token - class InCaptionPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-caption def __init__(self, parser, tree): @@ -1849,7 +1835,7 @@ def getPhases(debug): def startTagTableElement(self, token): self.parser.parseError() - #XXX Have to duplicate logic here to find out if the tag is ignored + # XXX Have to duplicate logic here to find out if the tag is ignored ignoreEndTag = self.ignoreEndTagCaption() self.parser.phase.processEndTag(impliedTagToken("caption")) if not ignoreEndTag: @@ -1864,8 +1850,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "caption": self.parser.parseError("expected-one-end-tag-but-got-another", - {"gotName": "caption", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "caption", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "caption": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1889,7 +1875,6 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) - class InColumnGroupPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-column @@ -1955,7 +1940,6 @@ def getPhases(debug): if not ignoreEndTag: return token - class InTableBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table0 def __init__(self, parser, tree): @@ -1980,8 +1964,8 @@ def getPhases(debug): # helper methods def clearStackToTableBodyContext(self): while self.tree.openElements[-1].name not in ("tbody", "tfoot", - "thead", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + "thead", "html"): + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() if self.tree.openElements[-1].name == "html": @@ -2003,7 +1987,7 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inRow"] def startTagTableCell(self, token): - self.parser.parseError("unexpected-cell-in-table-body", + self.parser.parseError("unexpected-cell-in-table-body", {"name": token["name"]}) self.startTagTr(impliedTagToken("tr", "StartTag")) return token @@ -2012,7 +1996,7 @@ def getPhases(debug): # XXX AT Any ideas on how to share this with endTagTable? if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2032,12 +2016,12 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inTable"] else: self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagTable(self, token): if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2049,12 +2033,11 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) - class InRowPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-row def __init__(self, parser, tree): @@ -2072,7 +2055,7 @@ def getPhases(debug): ("table", self.endTagTable), (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), (("body", "caption", "col", "colgroup", "html", "td", "th"), - self.endTagIgnore) + self.endTagIgnore) ]) self.endTagHandler.default = self.endTagOther @@ -2080,7 +2063,7 @@ def getPhases(debug): def clearStackToTableRowContext(self): while self.tree.openElements[-1].name not in ("tr", "html"): self.parser.parseError("unexpected-implied-end-tag-in-table-row", - {"name": self.tree.openElements[-1].name}) + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() def ignoreEndTagTr(self): @@ -2091,7 +2074,7 @@ def getPhases(debug): self.parser.phases["inTable"].processEOF() def processSpaceCharacters(self, token): - return self.parser.phases["inTable"].processSpaceCharacters(token) + return self.parser.phases["inTable"].processSpaceCharacters(token) def processCharacters(self, token): return self.parser.phases["inTable"].processCharacters(token) @@ -2139,7 +2122,7 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-row", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) @@ -2178,7 +2161,7 @@ def getPhases(debug): def startTagTableOther(self, token): if (self.tree.elementInScope("td", variant="table") or - self.tree.elementInScope("th", variant="table")): + self.tree.elementInScope("th", variant="table")): self.closeCell() return token else: @@ -2194,7 +2177,7 @@ def getPhases(debug): self.tree.generateImpliedEndTags(token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError("unexpected-cell-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) while True: node = self.tree.openElements.pop() if node.name == token["name"]: @@ -2249,7 +2232,7 @@ def getPhases(debug): assert self.parser.innerHTML def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.tree.insertText(token["data"]) @@ -2283,19 +2266,19 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-select", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOption(self, token): if self.tree.openElements[-1].name == "option": self.tree.openElements.pop() else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "option"}) + {"name": "option"}) def endTagOptgroup(self, token): # implicitly closes if (self.tree.openElements[-1].name == "option" and - self.tree.openElements[-2].name == "optgroup"): + self.tree.openElements[-2].name == "optgroup"): self.tree.openElements.pop() # It also closes if self.tree.openElements[-1].name == "optgroup": @@ -2303,7 +2286,7 @@ def getPhases(debug): # But nothing else else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "optgroup"}) + {"name": "optgroup"}) def endTagSelect(self, token): if self.tree.elementInScope("select", variant="select"): @@ -2318,8 +2301,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-select", - {"name": token["name"]}) - + {"name": token["name"]}) class InSelectInTablePhase(Phase): def __init__(self, parser, tree): @@ -2360,64 +2342,64 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inSelect"].processEndTag(token) - class InForeignContentPhase(Phase): - breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", + breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", - "em", "embed", "h1", "h2", "h3", + "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", - "li", "listing", "menu", "meta", "nobr", - "ol", "p", "pre", "ruby", "s", "small", - "span", "strong", "strike", "sub", "sup", + "li", "listing", "menu", "meta", "nobr", + "ol", "p", "pre", "ruby", "s", "small", + "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var"]) + def __init__(self, parser, tree): Phase.__init__(self, parser, tree) def adjustSVGTagNames(self, token): - replacements = {u"altglyph":u"altGlyph", - u"altglyphdef":u"altGlyphDef", - u"altglyphitem":u"altGlyphItem", - u"animatecolor":u"animateColor", - u"animatemotion":u"animateMotion", - u"animatetransform":u"animateTransform", - u"clippath":u"clipPath", - u"feblend":u"feBlend", - u"fecolormatrix":u"feColorMatrix", - u"fecomponenttransfer":u"feComponentTransfer", - u"fecomposite":u"feComposite", - u"feconvolvematrix":u"feConvolveMatrix", - u"fediffuselighting":u"feDiffuseLighting", - u"fedisplacementmap":u"feDisplacementMap", - u"fedistantlight":u"feDistantLight", - u"feflood":u"feFlood", - u"fefunca":u"feFuncA", - u"fefuncb":u"feFuncB", - u"fefuncg":u"feFuncG", - u"fefuncr":u"feFuncR", - u"fegaussianblur":u"feGaussianBlur", - u"feimage":u"feImage", - u"femerge":u"feMerge", - u"femergenode":u"feMergeNode", - u"femorphology":u"feMorphology", - u"feoffset":u"feOffset", - u"fepointlight":u"fePointLight", - u"fespecularlighting":u"feSpecularLighting", - u"fespotlight":u"feSpotLight", - u"fetile":u"feTile", - u"feturbulence":u"feTurbulence", - u"foreignobject":u"foreignObject", - u"glyphref":u"glyphRef", - u"lineargradient":u"linearGradient", - u"radialgradient":u"radialGradient", - u"textpath":u"textPath"} + replacements = {"altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath"} if token["name"] in replacements: token["name"] = replacements[token["name"]] def processCharacters(self, token): - if token["data"] == u"\u0000": - token["data"] = u"\uFFFD" - elif (self.parser.framesetOK and + if token["data"] == "\u0000": + token["data"] = "\uFFFD" + elif (self.parser.framesetOK and any(char not in spaceCharacters for char in token["data"])): self.parser.framesetOK = False Phase.processCharacters(self, token) @@ -2428,9 +2410,9 @@ def getPhases(debug): (token["name"] == "font" and set(token["data"].keys()) & set(["color", "face", "size"]))): self.parser.parseError("unexpected-html-element-in-foreign-content", - token["name"]) + {"name": token["name"]}) while (self.tree.openElements[-1].namespace != - self.tree.defaultNamespace and + self.tree.defaultNamespace and not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])): self.tree.openElements.pop() @@ -2453,11 +2435,11 @@ def getPhases(debug): nodeIndex = len(self.tree.openElements) - 1 node = self.tree.openElements[-1] if node.name != token["name"]: - self.parser.parseError("unexpected-end-tag", token["name"]) + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) while True: if node.name.translate(asciiUpper2Lower) == token["name"]: - #XXX this isn't in the spec but it seems necessary + # XXX this isn't in the spec but it seems necessary if self.parser.phase == self.parser.phases["inTableText"]: self.parser.phase.flushCharacters() self.parser.phase = self.parser.phase.originalPhase @@ -2475,21 +2457,20 @@ def getPhases(debug): break return new_token - class AfterBodyPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) self.startTagHandler = utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) + ("html", self.startTagHtml) + ]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([("html", self.endTagHtml)]) self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processComment(self, token): @@ -2507,11 +2488,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token - def endTagHtml(self,name): + def endTagHtml(self, name): if self.parser.innerHTML: self.parser.parseError("unexpected-end-tag-after-body-innerhtml") else: @@ -2519,7 +2500,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2562,7 +2543,7 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagFrameset(self, token): if self.tree.openElements[-1].name == "html": @@ -2571,15 +2552,14 @@ def getPhases(debug): else: self.tree.openElements.pop() if (not self.parser.innerHTML and - self.tree.openElements[-1].name != "frameset"): + self.tree.openElements[-1].name != "frameset"): # If we're not in innerHTML mode and the the current node is not a # "frameset" element (anymore) then switch. self.parser.phase = self.parser.phases["afterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterFramesetPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#after3 @@ -2598,7 +2578,7 @@ def getPhases(debug): self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processCharacters(self, token): @@ -2609,15 +2589,14 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagHtml(self, token): self.parser.phase = self.parser.phases["afterAfterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterAfterBodyPhase(Phase): def __init__(self, parser, tree): @@ -2647,13 +2626,13 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2687,12 +2666,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) - + {"name": token["name"]}) return { "initial": InitialPhase, @@ -2719,14 +2697,16 @@ def getPhases(debug): "afterAfterBody": AfterAfterBodyPhase, "afterAfterFrameset": AfterAfterFramesetPhase, # XXX after after frameset - } + } -def impliedTagToken(name, type="EndTag", attributes = None, - selfClosing = False): + +def impliedTagToken(name, type="EndTag", attributes=None, + selfClosing=False): if attributes is None: attributes = {} - return {"type":tokenTypes[type], "name":unicode(name), "data":attributes, - "selfClosing":selfClosing} + return {"type": tokenTypes[type], "name": name, "data": attributes, + "selfClosing": selfClosing} + class ParseError(Exception): """Error in parsed document""" diff --git a/libs/html5lib/ihatexml.py b/libs/html5lib/ihatexml.py index dd785639..0fc79308 100644 --- a/libs/html5lib/ihatexml.py +++ b/libs/html5lib/ihatexml.py @@ -1,25 +1,105 @@ -import re +from __future__ import absolute_import, division, unicode_literals -baseChar = """[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | [#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | [#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | [#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | [#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | [#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | [#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | [#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | [#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | [#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | [#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | [#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | [#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | [#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | [#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | [#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | [#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | [#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | [#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | [#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | [#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | [#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | [#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | [#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | [#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | [#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | [#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | [#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | [#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | #x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | #x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | #x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | [#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | [#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | #x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | [#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | [#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | [#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | [#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | [#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | #x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | [#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | [#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | [#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | [#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" +import re +import warnings + +from .constants import DataLossWarning + +baseChar = """ +[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | +[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | +[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | +[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | +[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | +[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | +[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | +[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | +[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | +[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | +[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | +[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | +[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | +[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | +[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | +[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | +[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | +[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | +[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | +[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | +[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | +[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | +[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | +[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | +[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | +[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | +[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | +[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | +[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | +[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | +#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | +#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | +#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | +[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | +[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | +#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | +[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | +[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | +[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | +[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | +[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | +#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | +[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | +[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | +[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | +[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]""" -combiningCharacter = """[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | [#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | [#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | [#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | #x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | [#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | [#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | #x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | [#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | [#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | #x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | [#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | [#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | [#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | [#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | [#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | #x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | [#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | #x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | [#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | [#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | #x3099 | #x309A""" +combiningCharacter = """ +[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | +[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | +[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | +[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | +#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | +[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | +[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | +#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | +[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | +[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | +#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | +[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | +[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | +[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | +[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | +[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | +#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | +[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | +#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | +[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | +[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | +#x3099 | #x309A""" -digit = """[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | [#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | [#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | [#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" +digit = """ +[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | +[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | +[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | +[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" -extender = """#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | [#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" +extender = """ +#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | +#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" letter = " | ".join([baseChar, ideographic]) -#Without the -name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, - extender]) +# Without the +name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, + extender]) nameFirst = " | ".join([letter, "_"]) reChar = re.compile(r"#x([\d|A-F]{4,4})") reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]") + def charStringToList(chars): charRanges = [item.strip() for item in chars.split(" | ")] rv = [] @@ -30,16 +110,17 @@ def charStringToList(chars): if match is not None: rv.append([hexToInt(item) for item in match.groups()]) if len(rv[-1]) == 1: - rv[-1] = rv[-1]*2 + rv[-1] = rv[-1] * 2 foundMatch = True break if not foundMatch: assert len(item) == 1 - + rv.append([ord(item)] * 2) rv = normaliseCharList(rv) return rv + def normaliseCharList(charList): charList = sorted(charList) for item in charList: @@ -49,61 +130,69 @@ def normaliseCharList(charList): while i < len(charList): j = 1 rv.append(charList[i]) - while i + j < len(charList) and charList[i+j][0] <= rv[-1][1] + 1: - rv[-1][1] = charList[i+j][1] + while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1: + rv[-1][1] = charList[i + j][1] j += 1 i += j return rv -#We don't really support characters above the BMP :( +# We don't really support characters above the BMP :( max_unicode = int("FFFF", 16) - + + def missingRanges(charList): rv = [] if charList[0] != 0: rv.append([0, charList[0][0] - 1]) for i, item in enumerate(charList[:-1]): - rv.append([item[1]+1, charList[i+1][0] - 1]) + rv.append([item[1] + 1, charList[i + 1][0] - 1]) if charList[-1][1] != max_unicode: rv.append([charList[-1][1] + 1, max_unicode]) return rv + def listToRegexpStr(charList): rv = [] for item in charList: if item[0] == item[1]: - rv.append(escapeRegexp(unichr(item[0]))) + rv.append(escapeRegexp(chr(item[0]))) else: - rv.append(escapeRegexp(unichr(item[0])) + "-" + - escapeRegexp(unichr(item[1]))) - return "[%s]"%"".join(rv) + rv.append(escapeRegexp(chr(item[0])) + "-" + + escapeRegexp(chr(item[1]))) + return "[%s]" % "".join(rv) + def hexToInt(hex_str): return int(hex_str, 16) + def escapeRegexp(string): specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}", - "[", "]", "|", "(", ")", "-") + "[", "]", "|", "(", ")", "-") for char in specialCharacters: string = string.replace(char, "\\" + char) - if char in string: - print string return string -#output from the above -nonXmlNameBMPRegexp = re.compile(u'[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') +# output from the above +nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +# Simpler things +nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\-\'()+,./:=?;!*#@$_%]") -nonXmlNameFirstBMPRegexp = re.compile(u'[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') class InfosetFilter(object): replacementRegexp = re.compile(r"U[\dA-F]{5,5}") - def __init__(self, replaceChars = None, - dropXmlnsLocalName = False, - dropXmlnsAttrNs = False, - preventDoubleDashComments = False, - preventDashAtCommentEnd = False, - replaceFormFeedCharacters = True): + + def __init__(self, replaceChars=None, + dropXmlnsLocalName=False, + dropXmlnsAttrNs=False, + preventDoubleDashComments=False, + preventDashAtCommentEnd=False, + replaceFormFeedCharacters=True, + preventSingleQuotePubid=False): self.dropXmlnsLocalName = dropXmlnsLocalName self.dropXmlnsAttrNs = dropXmlnsAttrNs @@ -113,14 +202,17 @@ class InfosetFilter(object): self.replaceFormFeedCharacters = replaceFormFeedCharacters + self.preventSingleQuotePubid = preventSingleQuotePubid + self.replaceCache = {} def coerceAttribute(self, name, namespace=None): if self.dropXmlnsLocalName and name.startswith("xmlns:"): - #Need a datalosswarning here + warnings.warn("Attributes cannot begin with xmlns", DataLossWarning) return None - elif (self.dropXmlnsAttrNs and + elif (self.dropXmlnsAttrNs and namespace == "http://www.w3.org/2000/xmlns/"): + warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning) return None else: return self.toXmlName(name) @@ -131,20 +223,35 @@ class InfosetFilter(object): def coerceComment(self, data): if self.preventDoubleDashComments: while "--" in data: + warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning) data = data.replace("--", "- -") return data - + def coerceCharacters(self, data): if self.replaceFormFeedCharacters: + for i in range(data.count("\x0C")): + warnings.warn("Text cannot contain U+000C", DataLossWarning) data = data.replace("\x0C", " ") - #Other non-xml characters + # Other non-xml characters return data + def coercePubid(self, data): + dataOutput = data + for char in nonPubidCharRegexp.findall(data): + warnings.warn("Coercing non-XML pubid", DataLossWarning) + replacement = self.getReplacementCharacter(char) + dataOutput = dataOutput.replace(char, replacement) + if self.preventSingleQuotePubid and dataOutput.find("'") >= 0: + warnings.warn("Pubid cannot contain single quote", DataLossWarning) + dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'")) + return dataOutput + def toXmlName(self, name): nameFirst = name[0] nameRest = name[1:] m = nonXmlNameFirstBMPRegexp.match(nameFirst) if m: + warnings.warn("Coercing non-XML name", DataLossWarning) nameFirstOutput = self.getReplacementCharacter(nameFirst) else: nameFirstOutput = nameFirst @@ -152,10 +259,11 @@ class InfosetFilter(object): nameRestOutput = nameRest replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest)) for char in replaceChars: + warnings.warn("Coercing non-XML name", DataLossWarning) replacement = self.getReplacementCharacter(char) nameRestOutput = nameRestOutput.replace(char, replacement) return nameFirstOutput + nameRestOutput - + def getReplacementCharacter(self, char): if char in self.replaceCache: replacement = self.replaceCache[char] @@ -169,9 +277,9 @@ class InfosetFilter(object): return name def escapeChar(self, char): - replacement = "U" + hex(ord(char))[2:].upper().rjust(5, "0") + replacement = "U%05X" % ord(char) self.replaceCache[char] = replacement return replacement def unescapeChar(self, charcode): - return unichr(int(charcode[1:], 16)) + return chr(int(charcode[1:], 16)) diff --git a/libs/html5lib/inputstream.py b/libs/html5lib/inputstream.py index edec1329..004bdd4a 100644 --- a/libs/html5lib/inputstream.py +++ b/libs/html5lib/inputstream.py @@ -1,19 +1,33 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + import codecs import re -import types -import sys -from constants import EOF, spaceCharacters, asciiLetters, asciiUppercase -from constants import encodings, ReparseException -import utils +from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase +from .constants import encodings, ReparseException +from . import utils -#Non-unicode versions of constants for use in the pre-parser -spaceCharactersBytes = frozenset([str(item) for item in spaceCharacters]) -asciiLettersBytes = frozenset([str(item) for item in asciiLetters]) -asciiUppercaseBytes = frozenset([str(item) for item in asciiUppercase]) -spacesAngleBrackets = spaceCharactersBytes | frozenset([">", "<"]) +from io import StringIO -invalid_unicode_re = re.compile(u"[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") +try: + from io import BytesIO +except ImportError: + BytesIO = StringIO + +try: + from io import BufferedIOBase +except ImportError: + class BufferedIOBase(object): + pass + +# Non-unicode versions of constants for use in the pre-parser +spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters]) +asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters]) +asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase]) +spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"]) + +invalid_unicode_re = re.compile("[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, @@ -23,22 +37,23 @@ non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF]) -ascii_punctuation_re = re.compile(ur"[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") +ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") # Cache for charsUntil() charsUntilRegEx = {} - -class BufferedStream: + + +class BufferedStream(object): """Buffering for streams that do not have buffering of their own - The buffer is implemented as a list of chunks on the assumption that + The buffer is implemented as a list of chunks on the assumption that joining many strings will be slow since it is O(n**2) """ - + def __init__(self, stream): self.stream = stream self.buffer = [] - self.position = [-1,0] #chunk number, offset + self.position = [-1, 0] # chunk number, offset def tell(self): pos = 0 @@ -48,11 +63,11 @@ class BufferedStream: return pos def seek(self, pos): - assert pos < self._bufferedBytes() + assert pos <= self._bufferedBytes() offset = pos i = 0 while len(self.buffer[i]) < offset: - offset -= pos + offset -= len(self.buffer[i]) i += 1 self.position = [i, offset] @@ -64,7 +79,7 @@ class BufferedStream: return self._readStream(bytes) else: return self._readFromBuffer(bytes) - + def _bufferedBytes(self): return sum([len(item) for item in self.buffer]) @@ -83,7 +98,7 @@ class BufferedStream: while bufferIndex < len(self.buffer) and remainingBytes != 0: assert remainingBytes > 0 bufferedData = self.buffer[bufferIndex] - + if remainingBytes <= len(bufferedData) - bufferOffset: bytesToRead = remainingBytes self.position = [bufferIndex, bufferOffset + bytesToRead] @@ -91,20 +106,33 @@ class BufferedStream: bytesToRead = len(bufferedData) - bufferOffset self.position = [bufferIndex, len(bufferedData)] bufferIndex += 1 - data = rv.append(bufferedData[bufferOffset: - bufferOffset + bytesToRead]) + rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead]) remainingBytes -= bytesToRead bufferOffset = 0 if remainingBytes: rv.append(self._readStream(remainingBytes)) - - return "".join(rv) - + + return b"".join(rv) -class HTMLInputStream: +def HTMLInputStream(source, encoding=None, parseMeta=True, chardet=True): + if hasattr(source, "read"): + isUnicode = isinstance(source.read(0), text_type) + else: + isUnicode = isinstance(source, text_type) + + if isUnicode: + if encoding is not None: + raise TypeError("Cannot explicitly set an encoding with a unicode string") + + return HTMLUnicodeInputStream(source) + else: + return HTMLBinaryInputStream(source, encoding, parseMeta, chardet) + + +class HTMLUnicodeInputStream(object): """Provides a unicode stream of characters to the HTMLTokenizer. This class takes care of character encoding and removing or replacing @@ -114,7 +142,7 @@ class HTMLInputStream: _defaultChunkSize = 10240 - def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + def __init__(self, source): """Initialises the HTMLInputStream. HTMLInputStream(source, [encoding]) -> Normalized stream from source @@ -126,49 +154,29 @@ class HTMLInputStream: the encoding. If specified, that encoding will be used, regardless of any BOM or later declaration (such as in a meta element) - + parseMeta - Look for a element containing encoding information """ - #Craziness - if len(u"\U0010FFFF") == 1: + # Craziness + if len("\U0010FFFF") == 1: self.reportCharacterErrors = self.characterErrorsUCS4 - self.replaceCharactersRegexp = re.compile(u"[\uD800-\uDFFF]") + self.replaceCharactersRegexp = re.compile("[\uD800-\uDFFF]") else: self.reportCharacterErrors = self.characterErrorsUCS2 - self.replaceCharactersRegexp = re.compile(u"([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 1: lastv = ord(data[-1]) if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF: self._bufferedCharacter = data[-1] data = data[:-1] - + self.reportCharacterErrors(data) - + # Replace invalid characters # Note U+0000 is dealt with in the tokenizer - data = self.replaceCharactersRegexp.sub(u"\ufffd", data) - - data = data.replace(u"\r\n", u"\n") - data = data.replace(u"\r", u"\n") + data = self.replaceCharactersRegexp.sub("\ufffd", data) + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") self.chunk = data self.chunkSize = len(data) @@ -378,23 +275,22 @@ class HTMLInputStream: return True def characterErrorsUCS4(self, data): - for i in xrange(len(invalid_unicode_re.findall(data))): + for i in range(len(invalid_unicode_re.findall(data))): self.errors.append("invalid-codepoint") def characterErrorsUCS2(self, data): - #Someone picked the wrong compile option - #You lose + # Someone picked the wrong compile option + # You lose skip = False - import sys for match in invalid_unicode_re.finditer(data): if skip: continue codepoint = ord(match.group()) pos = match.start() - #Pretty sure there should be endianness issues here - if utils.isSurrogatePair(data[pos:pos+2]): - #We have a surrogate pair! - char_val = utils.surrogatePairToCodepoint(data[pos:pos+2]) + # Pretty sure there should be endianness issues here + if utils.isSurrogatePair(data[pos:pos + 2]): + # We have a surrogate pair! + char_val = utils.surrogatePairToCodepoint(data[pos:pos + 2]) if char_val in non_bmp_invalid_codepoints: self.errors.append("invalid-codepoint") skip = True @@ -405,7 +301,7 @@ class HTMLInputStream: skip = False self.errors.append("invalid-codepoint") - def charsUntil(self, characters, opposite = False): + def charsUntil(self, characters, opposite=False): """ Returns a string of characters from the stream up to but not including any character in 'characters' or EOF. 'characters' must be a container that supports the 'in' method and iteration over its @@ -417,12 +313,12 @@ class HTMLInputStream: chars = charsUntilRegEx[(characters, opposite)] except KeyError: if __debug__: - for c in characters: + for c in characters: assert(ord(c) < 128) - regex = u"".join([u"\\x%02x" % ord(c) for c in characters]) + regex = "".join(["\\x%02x" % ord(c) for c in characters]) if not opposite: - regex = u"^%s" % regex - chars = charsUntilRegEx[(characters, opposite)] = re.compile(u"[%s]+" % regex) + regex = "^%s" % regex + chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex) rv = [] @@ -449,7 +345,7 @@ class HTMLInputStream: # Reached EOF break - r = u"".join(rv) + r = "".join(rv) return r def unget(self, char): @@ -468,26 +364,210 @@ class HTMLInputStream: self.chunkOffset -= 1 assert self.chunk[self.chunkOffset] == char -class EncodingBytes(str): + +class HTMLBinaryInputStream(HTMLUnicodeInputStream): + """Provides a unicode stream of characters to the HTMLTokenizer. + + This class takes care of character encoding and removing or replacing + incorrect byte-sequences and also provides column and line tracking. + + """ + + def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + """Initialises the HTMLInputStream. + + HTMLInputStream(source, [encoding]) -> Normalized stream from source + for use by html5lib. + + source can be either a file-object, local filename or a string. + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + + parseMeta - Look for a element containing encoding information + + """ + # Raw Stream - for unicode objects this will encode to utf-8 and set + # self.charEncoding as appropriate + self.rawStream = self.openStream(source) + + HTMLUnicodeInputStream.__init__(self, self.rawStream) + + self.charEncoding = (codecName(encoding), "certain") + + # Encoding Information + # Number of bytes to use when looking for a meta element with + # encoding information + self.numBytesMeta = 512 + # Number of bytes to use when using detecting encoding using chardet + self.numBytesChardet = 100 + # Encoding to use if no other information can be found + self.defaultEncoding = "windows-1252" + + # Detect encoding iff no explicit "transport level" encoding is supplied + if (self.charEncoding[0] is None): + self.charEncoding = self.detectEncoding(parseMeta, chardet) + + # Call superclass + self.reset() + + def reset(self): + self.dataStream = codecs.getreader(self.charEncoding[0])(self.rawStream, + 'replace') + HTMLUnicodeInputStream.reset(self) + + def openStream(self, source): + """Produces a file object from source. + + source can be either a file object, local filename or a string. + + """ + # Already a file object + if hasattr(source, 'read'): + stream = source + else: + stream = BytesIO(source) + + try: + stream.seek(stream.tell()) + except: + stream = BufferedStream(stream) + + return stream + + def detectEncoding(self, parseMeta=True, chardet=True): + # First look for a BOM + # This will also read past the BOM if present + encoding = self.detectBOM() + confidence = "certain" + # If there is no BOM need to look for meta elements with encoding + # information + if encoding is None and parseMeta: + encoding = self.detectEncodingMeta() + confidence = "tentative" + # Guess with chardet, if avaliable + if encoding is None and chardet: + confidence = "tentative" + try: + try: + from charade.universaldetector import UniversalDetector + except ImportError: + from chardet.universaldetector import UniversalDetector + buffers = [] + detector = UniversalDetector() + while not detector.done: + buffer = self.rawStream.read(self.numBytesChardet) + assert isinstance(buffer, bytes) + if not buffer: + break + buffers.append(buffer) + detector.feed(buffer) + detector.close() + encoding = detector.result['encoding'] + self.rawStream.seek(0) + except ImportError: + pass + # If all else fails use the default encoding + if encoding is None: + confidence = "tentative" + encoding = self.defaultEncoding + + # Substitute for equivalent encodings: + encodingSub = {"iso-8859-1": "windows-1252"} + + if encoding.lower() in encodingSub: + encoding = encodingSub[encoding.lower()] + + return encoding, confidence + + def changeEncoding(self, newEncoding): + assert self.charEncoding[1] != "certain" + newEncoding = codecName(newEncoding) + if newEncoding in ("utf-16", "utf-16-be", "utf-16-le"): + newEncoding = "utf-8" + if newEncoding is None: + return + elif newEncoding == self.charEncoding[0]: + self.charEncoding = (self.charEncoding[0], "certain") + else: + self.rawStream.seek(0) + self.reset() + self.charEncoding = (newEncoding, "certain") + raise ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding)) + + def detectBOM(self): + """Attempts to detect at BOM at the start of the stream. If + an encoding can be determined from the BOM return the name of the + encoding otherwise return None""" + bomDict = { + codecs.BOM_UTF8: 'utf-8', + codecs.BOM_UTF16_LE: 'utf-16-le', codecs.BOM_UTF16_BE: 'utf-16-be', + codecs.BOM_UTF32_LE: 'utf-32-le', codecs.BOM_UTF32_BE: 'utf-32-be' + } + + # Go to beginning of file and read in 4 bytes + string = self.rawStream.read(4) + assert isinstance(string, bytes) + + # Try detecting the BOM using bytes from the string + encoding = bomDict.get(string[:3]) # UTF-8 + seek = 3 + if not encoding: + # Need to detect UTF-32 before UTF-16 + encoding = bomDict.get(string) # UTF-32 + seek = 4 + if not encoding: + encoding = bomDict.get(string[:2]) # UTF-16 + seek = 2 + + # Set the read position past the BOM if one was found, otherwise + # set it to the start of the stream + self.rawStream.seek(encoding and seek or 0) + + return encoding + + def detectEncodingMeta(self): + """Report the encoding declared by the meta element + """ + buffer = self.rawStream.read(self.numBytesMeta) + assert isinstance(buffer, bytes) + parser = EncodingParser(buffer) + self.rawStream.seek(0) + encoding = parser.getEncoding() + + if encoding in ("utf-16", "utf-16-be", "utf-16-le"): + encoding = "utf-8" + + return encoding + + +class EncodingBytes(bytes): """String-like object with an associated position and various extra methods If the position is ever greater than the string length then an exception is raised""" def __new__(self, value): - return str.__new__(self, value.lower()) + assert isinstance(value, bytes) + return bytes.__new__(self, value.lower()) def __init__(self, value): - self._position=-1 - + self._position = -1 + def __iter__(self): return self - - def next(self): + + def __next__(self): p = self._position = self._position + 1 if p >= len(self): raise StopIteration elif p < 0: raise TypeError - return self[p] + return self[p:p + 1] + + def next(self): + # Py2 compat + return self.__next__() def previous(self): p = self._position @@ -496,13 +576,13 @@ class EncodingBytes(str): elif p < 0: raise TypeError self._position = p = p - 1 - return self[p] - + return self[p:p + 1] + def setPosition(self, position): if self._position >= len(self): raise StopIteration self._position = position - + def getPosition(self): if self._position >= len(self): raise StopIteration @@ -510,19 +590,19 @@ class EncodingBytes(str): return self._position else: return None - + position = property(getPosition, setPosition) def getCurrentByte(self): - return self[self.position] - + return self[self.position:self.position + 1] + currentByte = property(getCurrentByte) def skip(self, chars=spaceCharactersBytes): """Skip past a list of characters""" p = self.position # use property for the error-checking while p < len(self): - c = self[p] + c = self[p:p + 1] if c not in chars: self._position = p return c @@ -533,7 +613,7 @@ class EncodingBytes(str): def skipUntil(self, chars): p = self.position while p < len(self): - c = self[p] + c = self[p:p + 1] if c in chars: self._position = p return c @@ -542,16 +622,16 @@ class EncodingBytes(str): return None def matchBytes(self, bytes): - """Look for a sequence of bytes at the start of a string. If the bytes - are found return True and advance the position to the byte after the + """Look for a sequence of bytes at the start of a string. If the bytes + are found return True and advance the position to the byte after the match. Otherwise return False and leave the position alone""" p = self.position - data = self[p:p+len(bytes)] + data = self[p:p + len(bytes)] rv = data.startswith(bytes) if rv: self.position += len(bytes) return rv - + def jumpTo(self, bytes): """Look for the next sequence of bytes matching a given sequence. If a match is found advance the position to the last byte of the match""" @@ -560,11 +640,12 @@ class EncodingBytes(str): # XXX: This is ugly, but I can't see a nicer way to fix this. if self._position == -1: self._position = 0 - self._position += (newPosition + len(bytes)-1) + self._position += (newPosition + len(bytes) - 1) return True else: raise StopIteration + class EncodingParser(object): """Mini parser for detecting character encoding from meta elements""" @@ -575,147 +656,158 @@ class EncodingParser(object): def getEncoding(self): methodDispatch = ( - ("") + return self.data.jumpTo(b"-->") def handleMeta(self): if self.data.currentByte not in spaceCharactersBytes: - #if we have ") + return self.data.jumpTo(b">") def getAttribute(self): - """Return a name,value pair for the next attribute in the stream, + """Return a name,value pair for the next attribute in the stream, if one is found, or None""" data = self.data # Step 1 (skip chars) - c = data.skip(spaceCharactersBytes | frozenset("/")) + c = data.skip(spaceCharactersBytes | frozenset([b"/"])) + assert c is None or len(c) == 1 # Step 2 - if c in (">", None): + if c in (b">", None): return None # Step 3 attrName = [] attrValue = [] - #Step 4 attribute name + # Step 4 attribute name while True: - if c == "=" and attrName: + if c == b"=" and attrName: break elif c in spaceCharactersBytes: - #Step 6! + # Step 6! c = data.skip() - c = data.next() break - elif c in ("/", ">"): - return "".join(attrName), "" + elif c in (b"/", b">"): + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrName.append(c.lower()) - elif c == None: + elif c is None: return None else: attrName.append(c) - #Step 5 - c = data.next() - #Step 7 - if c != "=": + # Step 5 + c = next(data) + # Step 7 + if c != b"=": data.previous() - return "".join(attrName), "" - #Step 8 - data.next() - #Step 9 + return b"".join(attrName), b"" + # Step 8 + next(data) + # Step 9 c = data.skip() - #Step 10 - if c in ("'", '"'): - #10.1 + # Step 10 + if c in (b"'", b'"'): + # 10.1 quoteChar = c while True: - #10.2 - c = data.next() - #10.3 + # 10.2 + c = next(data) + # 10.3 if c == quoteChar: - data.next() - return "".join(attrName), "".join(attrValue) - #10.4 + next(data) + return b"".join(attrName), b"".join(attrValue) + # 10.4 elif c in asciiUppercaseBytes: attrValue.append(c.lower()) - #10.5 + # 10.5 else: attrValue.append(c) - elif c == ">": - return "".join(attrName), "" + elif c == b">": + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -724,9 +816,9 @@ class EncodingParser(object): attrValue.append(c) # Step 11 while True: - c = data.next() + c = next(data) if c in spacesAngleBrackets: - return "".join(attrName), "".join(attrValue) + return b"".join(attrName), b"".join(attrValue) elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -737,21 +829,23 @@ class EncodingParser(object): class ContentAttrParser(object): def __init__(self, data): + assert isinstance(data, bytes) self.data = data + def parse(self): try: - #Check if the attr name is charset - #otherwise return - self.data.jumpTo("charset") + # Check if the attr name is charset + # otherwise return + self.data.jumpTo(b"charset") self.data.position += 1 self.data.skip() - if not self.data.currentByte == "=": - #If there is no = sign keep looking for attrs + if not self.data.currentByte == b"=": + # If there is no = sign keep looking for attrs return None self.data.position += 1 self.data.skip() - #Look for an encoding between matching quote marks - if self.data.currentByte in ('"', "'"): + # Look for an encoding between matching quote marks + if self.data.currentByte in (b'"', b"'"): quoteMark = self.data.currentByte self.data.position += 1 oldPosition = self.data.position @@ -760,13 +854,13 @@ class ContentAttrParser(object): else: return None else: - #Unquoted value + # Unquoted value oldPosition = self.data.position try: self.data.skipUntil(spaceCharactersBytes) return self.data[oldPosition:self.data.position] except StopIteration: - #Return the whole remaining value + # Return the whole remaining value return self.data[oldPosition:] except StopIteration: return None @@ -775,7 +869,12 @@ class ContentAttrParser(object): def codecName(encoding): """Return the python codec name corresponding to an encoding or None if the string doesn't correspond to a valid encoding.""" - if (encoding is not None and type(encoding) in types.StringTypes): + if isinstance(encoding, bytes): + try: + encoding = encoding.decode("ascii") + except UnicodeDecodeError: + return None + if encoding: canonicalName = ascii_punctuation_re.sub("", encoding).lower() return encodings.get(canonicalName, None) else: diff --git a/libs/html5lib/sanitizer.py b/libs/html5lib/sanitizer.py index ae4c7d83..71dc5212 100644 --- a/libs/html5lib/sanitizer.py +++ b/libs/html5lib/sanitizer.py @@ -1,142 +1,145 @@ +from __future__ import absolute_import, division, unicode_literals + import re from xml.sax.saxutils import escape, unescape -from tokenizer import HTMLTokenizer -from constants import tokenTypes +from .tokenizer import HTMLTokenizer +from .constants import tokenTypes + class HTMLSanitizerMixin(object): """ sanitization of XHTML+MathML+SVG and of inline style attributes.""" acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] - + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi', - 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', - 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', - 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', - 'munderover', 'none'] - + 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', + 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', + 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none'] + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', - 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', - 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', - 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', - 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', - 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] - + 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', + 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', + 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', - 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', - 'background', 'balance', 'bgcolor', 'bgproperties', 'border', - 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', - 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', - 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', - 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', - 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', - 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', - 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', - 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', - 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', - 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', - 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', - 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', - 'optimum', 'pattern', 'ping', 'point-size', 'prompt', 'pqg', - 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', - 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', - 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', - 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', - 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', - 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', - 'width', 'wrap', 'xml:lang'] + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', + 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', + 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', + 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', + 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', + 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', + 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', + 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', + 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', + 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', + 'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload', + 'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', + 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', + 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', + 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', + 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', + 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', + 'width', 'wrap', 'xml:lang'] mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', - 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', - 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', - 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', - 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', - 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', - 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', - 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', - 'xlink:type', 'xmlns', 'xmlns:xlink'] - - svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', - 'arabic-form', 'ascent', 'attributeName', 'attributeType', - 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', - 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', - 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', - 'fill-opacity', 'fill-rule', 'font-family', 'font-size', - 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', - 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', - 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', - 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', - 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', - 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', - 'opacity', 'orient', 'origin', 'overline-position', - 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', - 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', - 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', - 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', - 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', - 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', - 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', - 'transform', 'type', 'u1', 'u2', 'underline-position', - 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', - 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', - 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', - 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', - 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', - 'y1', 'y2', 'zoomAndPan'] + 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', + 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', + 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', + 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', + 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', + 'xlink:type', 'xmlns', 'xmlns:xlink'] - attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', - 'xlink:href', 'xml:base'] + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', + 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', + 'fill-opacity', 'fill-rule', 'font-family', 'font-size', + 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', + 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', + 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', + 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', + 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', + 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', + 'opacity', 'orient', 'origin', 'overline-position', + 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', + 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', + 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', + 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', + 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', + 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', + 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', + 'transform', 'type', 'u1', 'u2', 'underline-position', + 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', + 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', + 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', + 'y1', 'y2', 'zoomAndPan'] + + attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster', + 'xlink:href', 'xml:base'] svg_attr_val_allows_ref = ['clip-path', 'color-profile', 'cursor', 'fill', - 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', - 'mask', 'stroke'] + 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', + 'mask', 'stroke'] svg_allow_local_href = ['altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', - 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', - 'set', 'use'] - + 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', + 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', + 'set', 'use'] + acceptable_css_properties = ['azimuth', 'background-color', - 'border-bottom-color', 'border-collapse', 'border-color', - 'border-left-color', 'border-right-color', 'border-top-color', 'clear', - 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', - 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', - 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', - 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', - 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', - 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', - 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', - 'white-space', 'width'] - + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', - 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', - 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', - 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', - 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', - 'transparent', 'underline', 'white', 'yellow'] - - acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', - 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', - 'stroke-opacity'] - - acceptable_protocols = [ 'ed2k', 'ftp', 'http', 'https', 'irc', - 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', - 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', - 'ssh', 'sftp', 'rtsp', 'afs' ] - + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + acceptable_svg_properties = ['fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] + + acceptable_protocols = ['ed2k', 'ftp', 'http', 'https', 'irc', + 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', + 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', + 'ssh', 'sftp', 'rtsp', 'afs'] + # subclasses may define their own versions of these constants allowed_elements = acceptable_elements + mathml_elements + svg_elements allowed_attributes = acceptable_attributes + mathml_attributes + svg_attributes @@ -160,94 +163,104 @@ class HTMLSanitizerMixin(object): # accommodate filters which use token_type differently token_type = token["type"] - if token_type in tokenTypes.keys(): - token_type = tokenTypes[token_type] + if token_type in list(tokenTypes.keys()): + token_type = tokenTypes[token_type] - if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], - tokenTypes["EmptyTag"]): + if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"]): if token["name"] in self.allowed_elements: - if token.has_key("data"): - attrs = dict([(name,val) for name,val in - token["data"][::-1] - if name in self.allowed_attributes]) - for attr in self.attr_val_is_uri: - if not attrs.has_key(attr): - continue - val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', - unescape(attrs[attr])).lower() - #remove replacement characters from unescaped characters - val_unescaped = val_unescaped.replace(u"\ufffd", "") - if (re.match("^[a-z0-9][-+.a-z0-9]*:",val_unescaped) and - (val_unescaped.split(':')[0] not in - self.allowed_protocols)): - del attrs[attr] - for attr in self.svg_attr_val_allows_ref: - if attr in attrs: - attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', - ' ', - unescape(attrs[attr])) - if (token["name"] in self.svg_allow_local_href and - 'xlink:href' in attrs and re.search('^\s*[^#\s].*', - attrs['xlink:href'])): - del attrs['xlink:href'] - if attrs.has_key('style'): - attrs['style'] = self.sanitize_css(attrs['style']) - token["data"] = [[name,val] for name,val in attrs.items()] - return token + return self.allowed_token(token, token_type) else: - if token_type == tokenTypes["EndTag"]: - token["data"] = "%s>" % token["name"] - elif token["data"]: - attrs = ''.join([' %s="%s"' % (k,escape(v)) for k,v in token["data"]]) - token["data"] = "<%s%s>" % (token["name"],attrs) - else: - token["data"] = "<%s>" % token["name"] - if token.get("selfClosing"): - token["data"]=token["data"][:-1] + "/>" - - if token["type"] in tokenTypes.keys(): - token["type"] = "Characters" - else: - token["type"] = tokenTypes["Characters"] - - del token["name"] - return token + return self.disallowed_token(token, token_type) elif token_type == tokenTypes["Comment"]: pass else: return token + def allowed_token(self, token, token_type): + if "data" in token: + attrs = dict([(name, val) for name, val in + token["data"][::-1] + if name in self.allowed_attributes]) + for attr in self.attr_val_is_uri: + if attr not in attrs: + continue + val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', + unescape(attrs[attr])).lower() + # remove replacement characters from unescaped characters + val_unescaped = val_unescaped.replace("\ufffd", "") + if (re.match("^[a-z0-9][-+.a-z0-9]*:", val_unescaped) and + (val_unescaped.split(':')[0] not in + self.allowed_protocols)): + del attrs[attr] + for attr in self.svg_attr_val_allows_ref: + if attr in attrs: + attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', + ' ', + unescape(attrs[attr])) + if (token["name"] in self.svg_allow_local_href and + 'xlink:href' in attrs and re.search('^\s*[^#\s].*', + attrs['xlink:href'])): + del attrs['xlink:href'] + if 'style' in attrs: + attrs['style'] = self.sanitize_css(attrs['style']) + token["data"] = [[name, val] for name, val in list(attrs.items())] + return token + + def disallowed_token(self, token, token_type): + if token_type == tokenTypes["EndTag"]: + token["data"] = "%s>" % token["name"] + elif token["data"]: + attrs = ''.join([' %s="%s"' % (k, escape(v)) for k, v in token["data"]]) + token["data"] = "<%s%s>" % (token["name"], attrs) + else: + token["data"] = "<%s>" % token["name"] + if token.get("selfClosing"): + token["data"] = token["data"][:-1] + "/>" + + if token["type"] in list(tokenTypes.keys()): + token["type"] = "Characters" + else: + token["type"] = tokenTypes["Characters"] + + del token["name"] + return token + def sanitize_css(self, style): # disallow urls - style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) + style = re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) # gauntlet - if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' - if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): return '' + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): + return '' clean = [] - for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): - if not value: continue - if prop.lower() in self.allowed_css_properties: - clean.append(prop + ': ' + value + ';') - elif prop.split('-')[0].lower() in ['background','border','margin', - 'padding']: - for keyword in value.split(): - if not keyword in self.acceptable_css_keywords and \ - not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$",keyword): - break - else: - clean.append(prop + ': ' + value + ';') - elif prop.lower() in self.allowed_svg_properties: - clean.append(prop + ': ' + value + ';') + for prop, value in re.findall("([-\w]+)\s*:\s*([^:;]*)", style): + if not value: + continue + if prop.lower() in self.allowed_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background', 'border', 'margin', + 'padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif prop.lower() in self.allowed_svg_properties: + clean.append(prop + ': ' + value + ';') return ' '.join(clean) + class HTMLSanitizer(HTMLTokenizer, HTMLSanitizerMixin): def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True, lowercaseElementName=False, lowercaseAttrName=False, parser=None): - #Change case matching defaults as we only output lowercase html anyway - #This solution doesn't seem ideal... + # Change case matching defaults as we only output lowercase html anyway + # This solution doesn't seem ideal... HTMLTokenizer.__init__(self, stream, encoding, parseMeta, useChardet, lowercaseElementName, lowercaseAttrName, parser=parser) diff --git a/libs/html5lib/serializer/__init__.py b/libs/html5lib/serializer/__init__.py index 1b746655..8380839a 100644 --- a/libs/html5lib/serializer/__init__.py +++ b/libs/html5lib/serializer/__init__.py @@ -1,17 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals -from html5lib import treewalkers +from .. import treewalkers -from htmlserializer import HTMLSerializer -from xhtmlserializer import XHTMLSerializer +from .htmlserializer import HTMLSerializer -def serialize(input, tree="simpletree", format="html", encoding=None, + +def serialize(input, tree="etree", format="html", encoding=None, **serializer_opts): # XXX: Should we cache this? - walker = treewalkers.getTreeWalker(tree) + walker = treewalkers.getTreeWalker(tree) if format == "html": s = HTMLSerializer(**serializer_opts) - elif format == "xhtml": - s = XHTMLSerializer(**serializer_opts) else: - raise ValueError, "type must be either html or xhtml" + raise ValueError("type must be html") return s.render(walker(input), encoding) diff --git a/libs/html5lib/serializer/htmlserializer.py b/libs/html5lib/serializer/htmlserializer.py index 8dd0a815..412a5a22 100644 --- a/libs/html5lib/serializer/htmlserializer.py +++ b/libs/html5lib/serializer/htmlserializer.py @@ -1,18 +1,20 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type import gettext _ = gettext.gettext -from html5lib.constants import voidElements, booleanAttributes, spaceCharacters -from html5lib.constants import rcdataElements, entities, xmlEntities -from html5lib import utils +try: + from functools import reduce +except ImportError: + pass + +from ..constants import voidElements, booleanAttributes, spaceCharacters +from ..constants import rcdataElements, entities, xmlEntities +from .. import utils from xml.sax.saxutils import escape -spaceCharacters = u"".join(spaceCharacters) +spaceCharacters = "".join(spaceCharacters) try: from codecs import register_error, xmlcharrefreplace_errors @@ -21,24 +23,18 @@ except ImportError: else: unicode_encode_errors = "htmlentityreplace" - from html5lib.constants import entities - encode_entity_map = {} - is_ucs4 = len(u"\U0010FFFF") == 1 - for k, v in entities.items(): - #skip multi-character entities + is_ucs4 = len("\U0010FFFF") == 1 + for k, v in list(entities.items()): + # skip multi-character entities if ((is_ucs4 and len(v) > 1) or - (not is_ucs4 and len(v) > 2)): + (not is_ucs4 and len(v) > 2)): continue if v != "&": if len(v) == 2: v = utils.surrogatePairToCodepoint(v) else: - try: - v = ord(v) - except: - print v - raise + v = ord(v) if not v in encode_entity_map or k.islower(): # prefer < over < and similarly for &, >, etc. encode_entity_map[v] = k @@ -53,8 +49,8 @@ else: skip = False continue index = i + exc.start - if utils.isSurrogatePair(exc.object[index:min([exc.end, index+2])]): - codepoint = utils.surrogatePairToCodepoint(exc.object[index:index+2]) + if utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]): + codepoint = utils.surrogatePairToCodepoint(exc.object[index:index + 2]) skip = True else: codepoint = ord(c) @@ -67,8 +63,8 @@ else: if not e.endswith(";"): res.append(";") else: - res.append("%s;"%(hex(cp)[2:])) - return (u"".join(res), exc.end) + res.append("%s;" % (hex(cp)[2:])) + return ("".join(res), exc.end) else: return xmlcharrefreplace_errors(exc) @@ -81,7 +77,7 @@ class HTMLSerializer(object): # attribute quoting options quote_attr_values = False - quote_char = u'"' + quote_char = '"' use_best_quote_char = True # tag syntax options @@ -96,15 +92,17 @@ class HTMLSerializer(object): resolve_entities = True # miscellaneous options + alphabetical_attributes = False inject_meta_charset = True strip_whitespace = False sanitize = False options = ("quote_attr_values", "quote_char", "use_best_quote_char", - "minimize_boolean_attributes", "use_trailing_solidus", - "space_before_trailing_solidus", "omit_optional_tags", - "strip_whitespace", "inject_meta_charset", "escape_lt_in_attrs", - "escape_rcdata", "resolve_entities", "sanitize") + "omit_optional_tags", "minimize_boolean_attributes", + "use_trailing_solidus", "space_before_trailing_solidus", + "escape_lt_in_attrs", "escape_rcdata", "resolve_entities", + "alphabetical_attributes", "inject_meta_charset", + "strip_whitespace", "sanitize") def __init__(self, **kwargs): """Initialize HTMLSerializer. @@ -147,10 +145,12 @@ class HTMLSerializer(object): See `html5lib user documentation`_ omit_optional_tags=True|False Omit start/end tags that are optional. + alphabetical_attributes=False|True + Reorder attributes to be in alphabetical order. .. _html5lib user documentation: http://code.google.com/p/html5lib/wiki/UserDocumentation """ - if kwargs.has_key('quote_char'): + if 'quote_char' in kwargs: self.use_best_quote_char = False for attr in self.options: setattr(self, attr, kwargs.get(attr, getattr(self, attr))) @@ -158,14 +158,14 @@ class HTMLSerializer(object): self.strict = False def encode(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, unicode_encode_errors) else: return string def encodeStrict(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, "strict") else: @@ -175,39 +175,46 @@ class HTMLSerializer(object): self.encoding = encoding in_cdata = False self.errors = [] + if encoding and self.inject_meta_charset: - from html5lib.filters.inject_meta_charset import Filter + from ..filters.inject_meta_charset import Filter treewalker = Filter(treewalker, encoding) - # XXX: WhitespaceFilter should be used before OptionalTagFilter + # WhitespaceFilter should be used before OptionalTagFilter # for maximum efficiently of this latter filter if self.strip_whitespace: - from html5lib.filters.whitespace import Filter + from ..filters.whitespace import Filter treewalker = Filter(treewalker) if self.sanitize: - from html5lib.filters.sanitizer import Filter + from ..filters.sanitizer import Filter treewalker = Filter(treewalker) if self.omit_optional_tags: - from html5lib.filters.optionaltags import Filter + from ..filters.optionaltags import Filter treewalker = Filter(treewalker) + # Alphabetical attributes must be last, as other filters + # could add attributes and alter the order + if self.alphabetical_attributes: + from ..filters.alphabeticalattributes import Filter + treewalker = Filter(treewalker) + for token in treewalker: type = token["type"] if type == "Doctype": - doctype = u"= 0: - if token["systemId"].find(u"'") >= 0: + doctype += " SYSTEM" + if token["systemId"]: + if token["systemId"].find('"') >= 0: + if token["systemId"].find("'") >= 0: self.serializeError(_("System identifer contains both single and double quote characters")) - quote_char = u"'" + quote_char = "'" else: - quote_char = u'"' - doctype += u" %s%s%s" % (quote_char, token["systemId"], quote_char) - - doctype += u">" + quote_char = '"' + doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char) + + doctype += ">" yield self.encodeStrict(doctype) elif type in ("Characters", "SpaceCharacters"): @@ -220,41 +227,41 @@ class HTMLSerializer(object): elif type in ("StartTag", "EmptyTag"): name = token["name"] - yield self.encodeStrict(u"<%s" % name) + yield self.encodeStrict("<%s" % name) if name in rcdataElements and not self.escape_rcdata: in_cdata = True elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - attributes = [] - for (attr_namespace,attr_name),attr_value in sorted(token["data"].items()): - #TODO: Add namespace support here + for (attr_namespace, attr_name), attr_value in token["data"].items(): + # TODO: Add namespace support here k = attr_name v = attr_value - yield self.encodeStrict(u' ') + yield self.encodeStrict(' ') yield self.encodeStrict(k) if not self.minimize_boolean_attributes or \ - (k not in booleanAttributes.get(name, tuple()) \ - and k not in booleanAttributes.get("", tuple())): - yield self.encodeStrict(u"=") + (k not in booleanAttributes.get(name, tuple()) + and k not in booleanAttributes.get("", tuple())): + yield self.encodeStrict("=") if self.quote_attr_values or not v: quote_attr = True else: - quote_attr = reduce(lambda x,y: x or (y in v), - spaceCharacters + u">\"'=", False) - v = v.replace(u"&", u"&") - if self.escape_lt_in_attrs: v = v.replace(u"<", u"<") + quote_attr = reduce(lambda x, y: x or (y in v), + spaceCharacters + ">\"'=", False) + v = v.replace("&", "&") + if self.escape_lt_in_attrs: + v = v.replace("<", "<") if quote_attr: quote_char = self.quote_char if self.use_best_quote_char: - if u"'" in v and u'"' not in v: - quote_char = u'"' - elif u'"' in v and u"'" not in v: - quote_char = u"'" - if quote_char == u"'": - v = v.replace(u"'", u"'") + if "'" in v and '"' not in v: + quote_char = '"' + elif '"' in v and "'" not in v: + quote_char = "'" + if quote_char == "'": + v = v.replace("'", "'") else: - v = v.replace(u'"', u""") + v = v.replace('"', """) yield self.encodeStrict(quote_char) yield self.encode(v) yield self.encodeStrict(quote_char) @@ -262,10 +269,10 @@ class HTMLSerializer(object): yield self.encode(v) if name in voidElements and self.use_trailing_solidus: if self.space_before_trailing_solidus: - yield self.encodeStrict(u" /") + yield self.encodeStrict(" /") else: - yield self.encodeStrict(u"/") - yield self.encode(u">") + yield self.encodeStrict("/") + yield self.encode(">") elif type == "EndTag": name = token["name"] @@ -273,13 +280,13 @@ class HTMLSerializer(object): in_cdata = False elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - yield self.encodeStrict(u"%s>" % name) + yield self.encodeStrict("%s>" % name) elif type == "Comment": data = token["data"] if data.find("--") >= 0: self.serializeError(_("Comment contains --")) - yield self.encodeStrict(u"" % token["data"]) + yield self.encodeStrict("" % token["data"]) elif type == "Entity": name = token["name"] @@ -289,7 +296,7 @@ class HTMLSerializer(object): if self.resolve_entities and key not in xmlEntities: data = entities[key] else: - data = u"&%s;" % name + data = "&%s;" % name yield self.encodeStrict(data) else: @@ -297,9 +304,9 @@ class HTMLSerializer(object): def render(self, treewalker, encoding=None): if encoding: - return "".join(list(self.serialize(treewalker, encoding))) + return b"".join(list(self.serialize(treewalker, encoding))) else: - return u"".join(list(self.serialize(treewalker))) + return "".join(list(self.serialize(treewalker))) def serializeError(self, data="XXX ERROR MESSAGE NEEDED"): # XXX The idea is to make data mandatory. @@ -307,6 +314,7 @@ class HTMLSerializer(object): if self.strict: raise SerializeError + def SerializeError(Exception): """Error in serialized tree""" pass diff --git a/libs/html5lib/serializer/xhtmlserializer.py b/libs/html5lib/serializer/xhtmlserializer.py deleted file mode 100644 index 7fdce47b..00000000 --- a/libs/html5lib/serializer/xhtmlserializer.py +++ /dev/null @@ -1,9 +0,0 @@ -from htmlserializer import HTMLSerializer - -class XHTMLSerializer(HTMLSerializer): - quote_attr_values = True - minimize_boolean_attributes = False - use_trailing_solidus = True - escape_lt_in_attrs = True - omit_optional_tags = False - escape_rcdata = True diff --git a/libs/html5lib/tokenizer.py b/libs/html5lib/tokenizer.py index 7e9eca88..79774578 100644 --- a/libs/html5lib/tokenizer.py +++ b/libs/html5lib/tokenizer.py @@ -1,27 +1,25 @@ +from __future__ import absolute_import, division, unicode_literals + try: - frozenset + chr = unichr # flake8: noqa except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset -try: - from collections import deque -except ImportError: - from utils import deque - -from constants import spaceCharacters -from constants import entitiesWindows1252, entities -from constants import asciiLowercase, asciiLetters, asciiUpper2Lower -from constants import digits, hexDigits, EOF -from constants import tokenTypes, tagTokenTypes -from constants import replacementCharacters + pass -from inputstream import HTMLInputStream +from collections import deque + +from .constants import spaceCharacters +from .constants import entities +from .constants import asciiLetters, asciiUpper2Lower +from .constants import digits, hexDigits, EOF +from .constants import tokenTypes, tagTokenTypes +from .constants import replacementCharacters + +from .inputstream import HTMLInputStream + +from .trie import Trie + +entitiesTrie = Trie(entities) -# Group entities by their first character, for faster lookups -entitiesByFirstChar = {} -for e in entities: - entitiesByFirstChar.setdefault(e[0], []).append(e) class HTMLTokenizer(object): """ This class takes care of tokenizing HTML. @@ -42,10 +40,10 @@ class HTMLTokenizer(object): self.stream = HTMLInputStream(stream, encoding, parseMeta, useChardet) self.parser = parser - #Perform case conversions? + # Perform case conversions? self.lowercaseElementName = lowercaseElementName self.lowercaseAttrName = lowercaseAttrName - + # Setup the initial tokenizer state self.escapeFlag = False self.lastFourChars = [] @@ -100,78 +98,79 @@ class HTMLTokenizer(object): if charAsInt in replacementCharacters: char = replacementCharacters[charAsInt] self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) - elif ((0xD800 <= charAsInt <= 0xDFFF) or + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) + elif ((0xD800 <= charAsInt <= 0xDFFF) or (charAsInt > 0x10FFFF)): - char = u"\uFFFD" + char = "\uFFFD" self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) else: - #Should speed up this check somehow (e.g. move the set to a constant) - if ((0x0001 <= charAsInt <= 0x0008) or - (0x000E <= charAsInt <= 0x001F) or - (0x007F <= charAsInt <= 0x009F) or - (0xFDD0 <= charAsInt <= 0xFDEF) or - charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, + # Should speed up this check somehow (e.g. move the set to a constant) + if ((0x0001 <= charAsInt <= 0x0008) or + (0x000E <= charAsInt <= 0x001F) or + (0x007F <= charAsInt <= 0x009F) or + (0xFDD0 <= charAsInt <= 0xFDEF) or + charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, - 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, + 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, - 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, - 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, - 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, + 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, + 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, + 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF])): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", + "illegal-codepoint-for-numeric-entity", "datavars": {"charAsInt": charAsInt}}) try: # Try/except needed as UCS-2 Python builds' unichar only works # within the BMP. - char = unichr(charAsInt) + char = chr(charAsInt) except ValueError: - char = eval("u'\\U%08x'" % charAsInt) + v = charAsInt - 0x10000 + char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF)) # Discard the ; if present. Otherwise, put it back on the queue and # invoke parseError on parser. - if c != u";": + if c != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "numeric-entity-without-semicolon"}) + "numeric-entity-without-semicolon"}) self.stream.unget(c) return char def consumeEntity(self, allowedChar=None, fromAttribute=False): # Initialise to the default output for when no entity is matched - output = u"&" + output = "&" charStack = [self.stream.char()] - if (charStack[0] in spaceCharacters or charStack[0] in (EOF, u"<", u"&") - or (allowedChar is not None and allowedChar == charStack[0])): + if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") + or (allowedChar is not None and allowedChar == charStack[0])): self.stream.unget(charStack[0]) - elif charStack[0] == u"#": + elif charStack[0] == "#": # Read the next character to see if it's hex or decimal hex = False charStack.append(self.stream.char()) - if charStack[-1] in (u"x", u"X"): + if charStack[-1] in ("x", "X"): hex = True charStack.append(self.stream.char()) # charStack[-1] should be the first digit if (hex and charStack[-1] in hexDigits) \ - or (not hex and charStack[-1] in digits): + or (not hex and charStack[-1] in digits): # At least one digit found, so consume the whole number self.stream.unget(charStack[-1]) output = self.consumeNumberEntity(hex) else: # No digits found self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data": "expected-numeric-entity"}) + "data": "expected-numeric-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: # At this point in the process might have named entity. Entities @@ -179,46 +178,40 @@ class HTMLTokenizer(object): # # Consume characters and compare to these to a substring of the # entity names in the list until the substring no longer matches. - filteredEntityList = entitiesByFirstChar.get(charStack[0], []) - - def entitiesStartingWith(name): - return [e for e in filteredEntityList if e.startswith(name)] - - while (charStack[-1] is not EOF and - entitiesStartingWith("".join(charStack))): + while (charStack[-1] is not EOF): + if not entitiesTrie.has_keys_with_prefix("".join(charStack)): + break charStack.append(self.stream.char()) # At this point we have a string that starts with some characters # that may match an entity - entityName = None - # Try to find the longest entity the string will match to take care # of ¬i for instance. - for entityLength in xrange(len(charStack)-1, 1, -1): - possibleEntityName = "".join(charStack[:entityLength]) - if possibleEntityName in entities: - entityName = possibleEntityName - break + try: + entityName = entitiesTrie.longest_prefix("".join(charStack[:-1])) + entityLength = len(entityName) + except KeyError: + entityName = None if entityName is not None: if entityName[-1] != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "named-entity-without-semicolon"}) + "named-entity-without-semicolon"}) if (entityName[-1] != ";" and fromAttribute and (charStack[entityLength] in asciiLetters or charStack[entityLength] in digits or - charStack[entityLength] == "=")): + charStack[entityLength] == "=")): self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: output = entities[entityName] self.stream.unget(charStack.pop()) - output += u"".join(charStack[entityLength:]) + output += "".join(charStack[entityLength:]) else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-named-entity"}) + "expected-named-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) if fromAttribute: self.currentToken["data"][-1][1] += output @@ -246,28 +239,26 @@ class HTMLTokenizer(object): token["name"] = token["name"].translate(asciiUpper2Lower) if token["type"] == tokenTypes["EndTag"]: if token["data"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"attributes-in-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "attributes-in-end-tag"}) if token["selfClosing"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"self-closing-flag-on-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "self-closing-flag-on-end-tag"}) self.tokenQueue.append(token) self.state = self.dataState - # Below are the various tokenizer states worked out. - def dataState(self): data = self.stream.char() if data == "&": self.state = self.entityDataState elif data == "<": self.state = self.tagOpenState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data":"invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\u0000"}) + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "invalid-codepoint"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\u0000"}) elif data is EOF: # Tokenization ends. return False @@ -276,21 +267,21 @@ class HTMLTokenizer(object): # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def entityDataState(self): self.consumeEntity() self.state = self.dataState return True - + def rcdataState(self): data = self.stream.char() if data == "&": @@ -300,113 +291,113 @@ class HTMLTokenizer(object): elif data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data in spaceCharacters: # Directly after emitting a token you switch back to the "data # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def characterReferenceInRcdata(self): self.consumeEntity() self.state = self.rcdataState return True - + def rawtextState(self): data = self.stream.char() if data == "<": self.state = self.rawtextLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataState(self): data = self.stream.char() if data == "<": self.state = self.scriptDataLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def plaintextState(self): data = self.stream.char() if data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + self.stream.charsUntil(u"\u0000")}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + self.stream.charsUntil("\u0000")}) return True def tagOpenState(self): data = self.stream.char() - if data == u"!": + if data == "!": self.state = self.markupDeclarationOpenState - elif data == u"/": + elif data == "/": self.state = self.closeTagOpenState elif data in asciiLetters: - self.currentToken = {"type": tokenTypes["StartTag"], + self.currentToken = {"type": tokenTypes["StartTag"], "name": data, "data": [], "selfClosing": False, "selfClosingAcknowledged": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-right-bracket"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<>"}) + "expected-tag-name-but-got-right-bracket"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"}) self.state = self.dataState - elif data == u"?": + elif data == "?": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-question-mark"}) + "expected-tag-name-but-got-question-mark"}) self.stream.unget(data) self.state = self.bogusCommentState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + "expected-tag-name"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.dataState return True @@ -415,22 +406,22 @@ class HTMLTokenizer(object): data = self.stream.char() if data in asciiLetters: self.currentToken = {"type": tokenTypes["EndTag"], "name": data, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-right-bracket"}) + "expected-closing-tag-but-got-right-bracket"}) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-eof"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + "expected-closing-tag-but-got-eof"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.state = self.dataState else: # XXX data can be _'_... self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-char", - "datavars": {"data": data}}) + "expected-closing-tag-but-got-char", + "datavars": {"data": data}}) self.stream.unget(data) self.state = self.bogusCommentState return True @@ -439,229 +430,229 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-tag-name"}) + "eof-in-tag-name"}) self.state = self.dataState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" else: self.currentToken["name"] += data # (Don't use charsUntil here, because tag names are # very short and it's faster to not do anything fancy) return True - + def rcdataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rcdataEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rcdataEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rcdataState return True - + def rawtextLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rawtextEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rawtextEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rawtextState return True - + def scriptDataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEndTagOpenState elif data == "!": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapeStartDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.state = self.dataState else: - chars = self.stream.charsUntil((u"<", u"-", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "-", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -669,21 +660,21 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": self.state = self.scriptDataEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -691,61 +682,61 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEscapedEndTagOpenState elif data in asciiLetters: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<" + data}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data}) self.temporaryBuffer = data self.state = self.scriptDataDoubleEscapeStartState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer = data self.state = self.scriptDataEscapedEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapeStartState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -761,87 +752,87 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) return True - + def scriptDataDoubleEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - - def scriptDataDoubleEscapedDashState(self): + + def scriptDataDoubleEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapedLessThanSignState(self): data = self.stream.char() if data == "/": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"/"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"}) self.temporaryBuffer = "" self.state = self.scriptDataDoubleEscapeEndState else: self.stream.unget(data) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapeEndState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -865,23 +856,23 @@ class HTMLTokenizer(object): elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data in (u"'", u'"', u"=", u"<"): + elif data in ("'", '"', "=", "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-name-but-got-eof"}) + "expected-attribute-name-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -892,34 +883,34 @@ class HTMLTokenizer(object): data = self.stream.char() leavingThisState = True emitToken = False - if data == u"=": + if data == "=": self.state = self.beforeAttributeValueState elif data in asciiLetters: self.currentToken["data"][-1][0] += data +\ - self.stream.charsUntil(asciiLetters, True) + self.stream.charsUntil(asciiLetters, True) leavingThisState = False - elif data == u">": + elif data == ">": # XXX If we emit here the attributes are converted to a dict # without being checked and when the code below runs we error # because data is a dict not a list emitToken = True elif data in spaceCharacters: self.state = self.afterAttributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][0] += u"\uFFFD" + self.currentToken["data"][-1][0] += "\uFFFD" leavingThisState = False - elif data in (u"'", u'"', u"<"): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data in ("'", '"', "<"): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"][-1][0] += data leavingThisState = False elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-attribute-name"}) self.state = self.dataState else: @@ -936,7 +927,7 @@ class HTMLTokenizer(object): for name, value in self.currentToken["data"][:-1]: if self.currentToken["data"][-1][0] == name: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "duplicate-attribute"}) + "duplicate-attribute"}) break # XXX Fix for above XXX if emitToken: @@ -947,28 +938,28 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"=": + elif data == "=": self.state = self.beforeAttributeValueState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState - elif data in (u"'", u'"', u"<"): + elif data in ("'", '"', "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-after-attribute-name"}) + "invalid-character-after-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-end-of-tag-but-got-eof"}) + "expected-end-of-tag-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -979,30 +970,30 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"\"": + elif data == "\"": self.state = self.attributeValueDoubleQuotedState - elif data == u"&": + elif data == "&": self.state = self.attributeValueUnQuotedState - self.stream.unget(data); - elif data == u"'": + self.stream.unget(data) + elif data == "'": self.state = self.attributeValueSingleQuotedState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-right-bracket"}) + "expected-attribute-value-but-got-right-bracket"}) self.emitCurrentToken() - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" self.state = self.attributeValueUnQuotedState - elif data in (u"=", u"<", u"`"): + elif data in ("=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "equals-in-unquoted-attribute-value"}) + "equals-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data self.state = self.attributeValueUnQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-eof"}) + "expected-attribute-value-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data @@ -1013,81 +1004,81 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u'"') - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute('"') + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-double-quote"}) + "eof-in-attribute-value-double-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("\"", u"&")) + self.stream.charsUntil(("\"", "&", "\u0000")) return True def attributeValueSingleQuotedState(self): data = self.stream.char() if data == "'": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u"'") - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute("'") + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-single-quote"}) + "eof-in-attribute-value-single-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("'", u"&")) + self.stream.charsUntil(("'", "&", "\u0000")) return True def attributeValueUnQuotedState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u"&": + elif data == "&": self.processEntityInAttribute(">") - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data in (u'"', u"'", u"=", u"<", u"`"): + elif data in ('"', "'", "=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-in-unquoted-attribute-value"}) + "unexpected-character-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-no-quotes"}) + "eof-in-attribute-value-no-quotes"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data + self.stream.charsUntil( - frozenset((u"&", u">", u'"', u"'", u"=", u"<", u"`")) | spaceCharacters) + frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters) return True def afterAttributeValueState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-attribute-value"}) + "unexpected-EOF-after-attribute-value"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-attribute-value"}) + "unexpected-character-after-attribute-value"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1098,14 +1089,14 @@ class HTMLTokenizer(object): self.currentToken["selfClosing"] = True self.emitCurrentToken() elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-solidus-in-tag"}) + "unexpected-EOF-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-soldius-in-tag"}) + "unexpected-character-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1114,10 +1105,10 @@ class HTMLTokenizer(object): # Make a new comment token and give it as value all the characters # until the first > or EOF (charsUntil checks for EOF automatically) # and emit it. - data = self.stream.charsUntil(u">") - data = data.replace(u"\u0000", u"\uFFFD") + data = self.stream.charsUntil(">") + data = data.replace("\u0000", "\uFFFD") self.tokenQueue.append( - {"type": tokenTypes["Comment"], "data": data}) + {"type": tokenTypes["Comment"], "data": data}) # Eat the character directly after the bogus comment which is either a # ">" or an EOF. @@ -1127,28 +1118,28 @@ class HTMLTokenizer(object): def markupDeclarationOpenState(self): charStack = [self.stream.char()] - if charStack[-1] == u"-": + if charStack[-1] == "-": charStack.append(self.stream.char()) - if charStack[-1] == u"-": - self.currentToken = {"type": tokenTypes["Comment"], "data": u""} + if charStack[-1] == "-": + self.currentToken = {"type": tokenTypes["Comment"], "data": ""} self.state = self.commentStartState return True - elif charStack[-1] in (u'd', u'D'): + elif charStack[-1] in ('d', 'D'): matched = True - for expected in ((u'o', u'O'), (u'c', u'C'), (u't', u'T'), - (u'y', u'Y'), (u'p', u'P'), (u'e', u'E')): + for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'), + ('y', 'Y'), ('p', 'P'), ('e', 'E')): charStack.append(self.stream.char()) if charStack[-1] not in expected: matched = False break if matched: self.currentToken = {"type": tokenTypes["Doctype"], - "name": u"", - "publicId": None, "systemId": None, + "name": "", + "publicId": None, "systemId": None, "correct": True} self.state = self.doctypeState return True - elif (charStack[-1] == "[" and + elif (charStack[-1] == "[" and self.parser is not None and self.parser.tree.openElements and self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace): @@ -1163,7 +1154,7 @@ class HTMLTokenizer(object): return True self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-dashes-or-doctype"}) + "expected-dashes-or-doctype"}) while charStack: self.stream.unget(charStack.pop()) @@ -1174,41 +1165,41 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "-": self.state = self.commentStartDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data self.state = self.commentState return True - + def commentStartDashState(self): data = self.stream.char() if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: @@ -1216,95 +1207,94 @@ class HTMLTokenizer(object): self.state = self.commentState return True - def commentState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data + \ - self.stream.charsUntil((u"-", u"\u0000")) + self.stream.charsUntil(("-", "\u0000")) return True def commentEndDashState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-dash"}) + "eof-in-comment-end-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"-" + data + self.currentToken["data"] += "-" + data self.state = self.commentState return True def commentEndState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--\uFFFD" + self.currentToken["data"] += "--\uFFFD" self.state = self.commentState elif data == "!": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-bang-after-double-dash-in-comment"}) + "unexpected-bang-after-double-dash-in-comment"}) self.state = self.commentEndBangState - elif data == u"-": + elif data == "-": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-dash-after-double-dash-in-comment"}) + "unexpected-dash-after-double-dash-in-comment"}) self.currentToken["data"] += data elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-double-dash"}) + "eof-in-comment-double-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-comment"}) - self.currentToken["data"] += u"--" + data + "unexpected-char-in-comment"}) + self.currentToken["data"] += "--" + data self.state = self.commentState return True def commentEndBangState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"-": + elif data == "-": self.currentToken["data"] += "--!" self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--!\uFFFD" + self.currentToken["data"] += "--!\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-bang-state"}) + "eof-in-comment-end-bang-state"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"--!" + data + self.currentToken["data"] += "--!" + data self.state = self.commentState return True @@ -1314,13 +1304,13 @@ class HTMLTokenizer(object): self.state = self.beforeDoctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "need-space-after-doctype"}) + "need-space-after-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeNameState return True @@ -1329,20 +1319,20 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-right-bracket"}) + "expected-doctype-name-but-got-right-bracket"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] = u"\uFFFD" + self.currentToken["name"] = "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1356,18 +1346,18 @@ class HTMLTokenizer(object): if data in spaceCharacters: self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.state = self.afterDoctypeNameState - elif data == u">": + elif data == ">": self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype-name"}) + "eof-in-doctype-name"}) self.currentToken["correct"] = False self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) @@ -1380,21 +1370,21 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.currentToken["correct"] = False self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - if data in (u"p", u"P"): + if data in ("p", "P"): matched = True - for expected in ((u"u", u"U"), (u"b", u"B"), (u"l", u"L"), - (u"i", u"I"), (u"c", u"C")): + for expected in (("u", "U"), ("b", "B"), ("l", "L"), + ("i", "I"), ("c", "C")): data = self.stream.char() if data not in expected: matched = False @@ -1402,10 +1392,10 @@ class HTMLTokenizer(object): if matched: self.state = self.afterDoctypePublicKeywordState return True - elif data in (u"s", u"S"): + elif data in ("s", "S"): matched = True - for expected in ((u"y", u"Y"), (u"s", u"S"), (u"t", u"T"), - (u"e", u"E"), (u"m", u"M")): + for expected in (("y", "Y"), ("s", "S"), ("t", "T"), + ("e", "E"), ("m", "M")): data = self.stream.char() if data not in expected: matched = False @@ -1420,25 +1410,25 @@ class HTMLTokenizer(object): # and needs to be ungetted self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-space-or-right-bracket-in-doctype", "datavars": - {"data": data}}) + "expected-space-or-right-bracket-in-doctype", "datavars": + {"data": data}}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypePublicKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypePublicIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypePublicIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1452,26 +1442,26 @@ class HTMLTokenizer(object): if data in spaceCharacters: pass elif data == "\"": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierDoubleQuotedState elif data == "'": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1480,19 +1470,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1504,19 +1494,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1533,27 +1523,27 @@ class HTMLTokenizer(object): self.state = self.dataState elif data == '"': self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def betweenDoctypePublicAndSystemIdentifiersState(self): data = self.stream.char() if data in spaceCharacters: @@ -1562,36 +1552,36 @@ class HTMLTokenizer(object): self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data == '"': - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypeSystemKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypeSystemIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1599,32 +1589,32 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState return True - + def beforeDoctypeSystemIdentifierState(self): data = self.stream.char() if data in spaceCharacters: pass elif data == "\"": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1633,19 +1623,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1657,19 +1647,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1686,19 +1676,19 @@ class HTMLTokenizer(object): self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.state = self.bogusDoctypeState return True def bogusDoctypeState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: @@ -1713,32 +1703,29 @@ class HTMLTokenizer(object): def cdataSectionState(self): data = [] while True: - data.append(self.stream.charsUntil(u"]")) - charStack = [] - - for expected in ["]", "]", ">"]: - charStack.append(self.stream.char()) - matched = True - if charStack[-1] == EOF: - data.extend(charStack[:-1]) - break - elif charStack[-1] != expected: - matched = False - data.extend(charStack) - break - - if matched: + data.append(self.stream.charsUntil("]")) + data.append(self.stream.charsUntil(">")) + char = self.stream.char() + if char == EOF: break + else: + assert char == ">" + if data[-1][-2:] == "]]": + data[-1] = data[-1][:-2] + break + else: + data.append(char) + data = "".join(data) - #Deal with null here rather than in the parser - nullCount = data.count(u"\u0000") + # Deal with null here rather than in the parser + nullCount = data.count("\u0000") if nullCount > 0: - for i in xrange(nullCount): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + for i in range(nullCount): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - data = data.replace(u"\u0000", u"\uFFFD") + data = data.replace("\u0000", "\uFFFD") if data: - self.tokenQueue.append({"type": tokenTypes["Characters"], + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.dataState return True diff --git a/libs/html5lib/treeadapters/__init__.py b/libs/html5lib/treeadapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/html5lib/treeadapters/sax.py b/libs/html5lib/treeadapters/sax.py new file mode 100644 index 00000000..ad47df95 --- /dev/null +++ b/libs/html5lib/treeadapters/sax.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from xml.sax.xmlreader import AttributesNSImpl + +from ..constants import adjustForeignAttributes, unadjustForeignAttributes + +prefix_mapping = {} +for prefix, localName, namespace in adjustForeignAttributes.values(): + if prefix is not None: + prefix_mapping[prefix] = namespace + + +def to_sax(walker, handler): + """Call SAX-like content handler based on treewalker walker""" + handler.startDocument() + for prefix, namespace in prefix_mapping.items(): + handler.startPrefixMapping(prefix, namespace) + + for token in walker: + type = token["type"] + if type == "Doctype": + continue + elif type in ("StartTag", "EmptyTag"): + attrs = AttributesNSImpl(token["data"], + unadjustForeignAttributes) + handler.startElementNS((token["namespace"], token["name"]), + token["name"], + attrs) + if type == "EmptyTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type == "EndTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type in ("Characters", "SpaceCharacters"): + handler.characters(token["data"]) + elif type == "Comment": + pass + else: + assert False, "Unknown token type" + + for prefix, namespace in prefix_mapping.items(): + handler.endPrefixMapping(prefix) + handler.endDocument() diff --git a/libs/html5lib/treebuilders/__init__.py b/libs/html5lib/treebuilders/__init__.py index 14f66d40..6a6b2a4c 100755 --- a/libs/html5lib/treebuilders/__init__.py +++ b/libs/html5lib/treebuilders/__init__.py @@ -7,7 +7,7 @@ implement several things: 1) A set of classes for various types of elements: Document, Doctype, Comment, Element. These must implement the interface of _base.treebuilders.Node (although comment nodes have a different -signature for their constructor, see treebuilders.simpletree.Comment) +signature for their constructor, see treebuilders.etree.Comment) Textual content may also be implemented as another node type, or not, as your tree implementation requires. @@ -24,73 +24,53 @@ getDocument - Returns the root node of the complete document tree testSerializer method on your treebuilder which accepts a node and returns a string containing Node and its children serialized according to the format used in the unittests - -The supplied simpletree module provides a python-only implementation -of a full treebuilder and is a useful reference for the semantics of -the various methods. """ +from __future__ import absolute_import, division, unicode_literals + +from ..utils import default_etree + treeBuilderCache = {} -import sys def getTreeBuilder(treeType, implementation=None, **kwargs): """Get a TreeBuilder class for various types of tree with built-in support - + treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" - - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. - "dom" - A generic builder for DOM implementations, defaulting to - a xml.dom.minidom based implementation for the sake of - backwards compatibility (as releases up until 0.10 had a - builder called "dom" that was a minidom implemenation). - "etree" - A generic builder for tree implementations exposing an - elementtree-like interface (known to work with - ElementTree, cElementTree and lxml.etree). - "beautifulsoup" - Beautiful soup (if installed) - + values are: + + "dom" - A generic builder for DOM implementations, defaulting to + a xml.dom.minidom based implementation. + "etree" - A generic builder for tree implementations exposing an + ElementTree-like interface, defaulting to + xml.etree.cElementTree if available and + xml.etree.ElementTree if not. + "lxml" - A etree-based builder for lxml.etree, handling + limitations of lxml's implementation. + implementation - (Currently applies to the "etree" and "dom" tree types). A module implementing the tree type e.g. - xml.etree.ElementTree or lxml.etree.""" - + xml.etree.ElementTree or xml.etree.cElementTree.""" + treeType = treeType.lower() if treeType not in treeBuilderCache: if treeType == "dom": - import dom - # XXX: Keep backwards compatibility by using minidom if no implementation is given - if implementation == None: + from . import dom + # Come up with a sane default (pref. from the stdlib) + if implementation is None: from xml.dom import minidom implementation = minidom - # XXX: NEVER cache here, caching is done in the dom submodule + # NEVER cache here, caching is done in the dom submodule return dom.getDomModule(implementation, **kwargs).TreeBuilder - elif treeType == "simpletree": - import simpletree - treeBuilderCache[treeType] = simpletree.TreeBuilder - elif treeType == "beautifulsoup": - import soup - treeBuilderCache[treeType] = soup.TreeBuilder elif treeType == "lxml": - import etree_lxml + from . import etree_lxml treeBuilderCache[treeType] = etree_lxml.TreeBuilder elif treeType == "etree": - # Come up with a sane default - if implementation == None: - try: - import xml.etree.cElementTree as ET - except ImportError: - try: - import xml.etree.ElementTree as ET - except ImportError: - try: - import cElementTree as ET - except ImportError: - import elementtree.ElementTree as ET - implementation = ET - import etree + from . import etree + if implementation is None: + implementation = default_etree # NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeBuilder else: - raise ValueError("""Unrecognised treebuilder "%s" """%treeType) + raise ValueError("""Unrecognised treebuilder "%s" """ % treeType) return treeBuilderCache.get(treeType) diff --git a/libs/html5lib/treebuilders/_base.py b/libs/html5lib/treebuilders/_base.py index f3782d28..8b97cc11 100755 --- a/libs/html5lib/treebuilders/_base.py +++ b/libs/html5lib/treebuilders/_base.py @@ -1,25 +1,34 @@ -from html5lib.constants import scopingElements, tableInsertModeElements, namespaces -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from ..constants import scopingElements, tableInsertModeElements, namespaces # The scope markers are inserted when entering object elements, # marquees, table cells, and table captions, and are used to prevent formatting # from "leaking" into tables, object elements, and marquees. Marker = None +listElementsMap = { + None: (frozenset(scopingElements), False), + "button": (frozenset(scopingElements | set([(namespaces["html"], "button")])), False), + "list": (frozenset(scopingElements | set([(namespaces["html"], "ol"), + (namespaces["html"], "ul")])), False), + "table": (frozenset([(namespaces["html"], "html"), + (namespaces["html"], "table")]), False), + "select": (frozenset([(namespaces["html"], "optgroup"), + (namespaces["html"], "option")]), True) +} + + class Node(object): def __init__(self, name): """Node representing an item in the tree. name - The tag name associated with the node parent - The parent of the current node (or None for the document node) - value - The value of the current node (applies to text nodes and + value - The value of the current node (applies to text nodes and comments attributes - a dict holding name, value pairs for attributes of the node - childNodes - a list of child nodes of the current node. This must + childNodes - a list of child nodes of the current node. This must include all elements but not necessarily other node types _flags - A list of miscellaneous flags that can be set on the node """ @@ -30,14 +39,14 @@ class Node(object): self.childNodes = [] self._flags = [] - def __unicode__(self): - attributesStr = " ".join(["%s=\"%s\""%(name, value) - for name, value in - self.attributes.iteritems()]) + def __str__(self): + attributesStr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in + self.attributes.items()]) if attributesStr: - return "<%s %s>"%(self.name,attributesStr) + return "<%s %s>" % (self.name, attributesStr) else: - return "<%s>"%(self.name) + return "<%s>" % (self.name) def __repr__(self): return "<%s>" % (self.name) @@ -48,14 +57,14 @@ class Node(object): raise NotImplementedError def insertText(self, data, insertBefore=None): - """Insert data as text in the current node, positioned before the + """Insert data as text in the current node, positioned before the start of node insertBefore or to the end of the node's text. """ raise NotImplementedError def insertBefore(self, node, refNode): - """Insert node as a child of the current node, before refNode in the - list of child nodes. Raises ValueError if refNode is not a child of + """Insert node as a child of the current node, before refNode in the + list of child nodes. Raises ValueError if refNode is not a child of the current node""" raise NotImplementedError @@ -65,11 +74,11 @@ class Node(object): raise NotImplementedError def reparentChildren(self, newParent): - """Move all the children of the current node to newParent. - This is needed so that trees that don't store text as nodes move the + """Move all the children of the current node to newParent. + This is needed so that trees that don't store text as nodes move the text in the correct way """ - #XXX - should this method be made more general? + # XXX - should this method be made more general? for child in self.childNodes: newParent.appendChild(child) self.childNodes = [] @@ -80,12 +89,12 @@ class Node(object): """ raise NotImplementedError - def hasContent(self): """Return true if the node has children or text, false otherwise """ raise NotImplementedError + class ActiveFormattingElements(list): def append(self, node): equalCount = 0 @@ -103,12 +112,13 @@ class ActiveFormattingElements(list): def nodesEqual(self, node1, node2): if not node1.nameTuple == node2.nameTuple: return False - + if not node1.attributes == node2.attributes: return False - + return True + class TreeBuilder(object): """Base treebuilder implementation documentClass - the class to use for the bottommost node of a document @@ -117,19 +127,19 @@ class TreeBuilder(object): doctypeClass - the class to use for doctypes """ - #Document class + # Document class documentClass = None - #The class to use for creating a node + # The class to use for creating a node elementClass = None - #The class to use for creating comments + # The class to use for creating comments commentClass = None - #The class to use for creating doctypes + # The class to use for creating doctypes doctypeClass = None - - #Fragment class + + # Fragment class fragmentClass = None def __init__(self, namespaceHTMLElements): @@ -138,12 +148,12 @@ class TreeBuilder(object): else: self.defaultNamespace = None self.reset() - + def reset(self): self.openElements = [] self.activeFormattingElements = ActiveFormattingElements() - #XXX - rename these to headElement, formElement + # XXX - rename these to headElement, formElement self.headPointer = None self.formPointer = None @@ -153,30 +163,20 @@ class TreeBuilder(object): def elementInScope(self, target, variant=None): - #If we pass a node in we match that. if we pass a string - #match any node with that name + # If we pass a node in we match that. if we pass a string + # match any node with that name exactNode = hasattr(target, "nameTuple") - listElementsMap = { - None:(scopingElements, False), - "button":(scopingElements | set([(namespaces["html"], "button")]), False), - "list":(scopingElements | set([(namespaces["html"], "ol"), - (namespaces["html"], "ul")]), False), - "table":(set([(namespaces["html"], "html"), - (namespaces["html"], "table")]), False), - "select":(set([(namespaces["html"], "optgroup"), - (namespaces["html"], "option")]), True) - } listElements, invert = listElementsMap[variant] for node in reversed(self.openElements): if (node.name == target and not exactNode or - node == target and exactNode): + node == target and exactNode): return True - elif (invert ^ (node.nameTuple in listElements)): + elif (invert ^ (node.nameTuple in listElements)): return False - assert False # We should never reach this point + assert False # We should never reach this point def reconstructActiveFormattingElements(self): # Within this algorithm the order of steps described in the @@ -196,7 +196,7 @@ class TreeBuilder(object): # Step 6 while entry != Marker and entry not in self.openElements: if i == 0: - #This will be reset to 0 below + # This will be reset to 0 below i = -1 break i -= 1 @@ -209,13 +209,13 @@ class TreeBuilder(object): # Step 8 entry = self.activeFormattingElements[i] - clone = entry.cloneNode() #Mainly to get a new copy of the attributes + clone = entry.cloneNode() # Mainly to get a new copy of the attributes # Step 9 - element = self.insertElement({"type":"StartTag", - "name":clone.name, - "namespace":clone.namespace, - "data":clone.attributes}) + element = self.insertElement({"type": "StartTag", + "name": clone.name, + "namespace": clone.namespace, + "data": clone.attributes}) # Step 10 self.activeFormattingElements[i] = element @@ -260,7 +260,7 @@ class TreeBuilder(object): if parent is None: parent = self.openElements[-1] parent.appendChild(self.commentClass(token["data"])) - + def createElement(self, token): """Create an element but don't insert it anywhere""" name = token["name"] @@ -282,10 +282,10 @@ class TreeBuilder(object): self.insertElement = self.insertElementNormal insertFromTable = property(_getInsertFromTable, _setInsertFromTable) - + def insertElementNormal(self, token): name = token["name"] - assert type(name) == unicode, "Element %s not unicode"%name + assert isinstance(name, text_type), "Element %s not unicode" % name namespace = token.get("namespace", self.defaultNamespace) element = self.elementClass(name, namespace) element.attributes = token["data"] @@ -294,13 +294,13 @@ class TreeBuilder(object): return element def insertElementTable(self, token): - """Create an element and insert it into the tree""" + """Create an element and insert it into the tree""" element = self.createElement(token) if self.openElements[-1].name not in tableInsertModeElements: return self.insertElementNormal(token) else: - #We should be in the InTable mode. This means we want to do - #special magic element rearranging + # We should be in the InTable mode. This means we want to do + # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() if insertBefore is None: parent.appendChild(element) @@ -315,7 +315,7 @@ class TreeBuilder(object): parent = self.openElements[-1] if (not self.insertFromTable or (self.insertFromTable and - self.openElements[-1].name + self.openElements[-1].name not in tableInsertModeElements)): parent.insertText(data) else: @@ -323,14 +323,14 @@ class TreeBuilder(object): # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() parent.insertText(data, insertBefore) - + def getTableMisnestedNodePosition(self): """Get the foster parent element, and sibling to insert before (or None) when inserting a misnested table node""" # The foster parent element is the one which comes before the most # recently opened table element # XXX - this is really inelegant - lastTable=None + lastTable = None fosterParent = None insertBefore = None for elm in self.openElements[::-1]: @@ -354,7 +354,7 @@ class TreeBuilder(object): name = self.openElements[-1].name # XXX td, th and tr are not actually needed if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) - and name != exclude): + and name != exclude): self.openElements.pop() # XXX This is not entirely what the specification says. We should # investigate it more closely. @@ -363,10 +363,10 @@ class TreeBuilder(object): def getDocument(self): "Return the final tree" return self.document - + def getFragment(self): "Return the final fragment" - #assert self.innerHTML + # assert self.innerHTML fragment = self.fragmentClass() self.openElements[0].reparentChildren(fragment) return fragment diff --git a/libs/html5lib/treebuilders/dom.py b/libs/html5lib/treebuilders/dom.py index 9578da2b..61e5ed79 100644 --- a/libs/html5lib/treebuilders/dom.py +++ b/libs/html5lib/treebuilders/dom.py @@ -1,45 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals -from xml.dom import minidom, Node, XML_NAMESPACE, XMLNS_NAMESPACE -try: - from types import ModuleType -except: - from new import module as ModuleType -import re + +from xml.dom import minidom, Node import weakref -import _base -from html5lib import constants, ihatexml -from html5lib.constants import namespaces +from . import _base +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory -moduleCache = {} - -def getDomModule(DomImplementation): - name = "_" + DomImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType(name) - objs = getDomBuilder(DomImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getDomBuilder(DomImplementation): Dom = DomImplementation + class AttrList(object): def __init__(self, element): self.element = element + def __iter__(self): - return self.element.attributes.items().__iter__() + return list(self.element.attributes.items()).__iter__() + def __setitem__(self, name, value): self.element.setAttribute(name, value) + def __len__(self): - return len(self.element.attributes.items()) + return len(list(self.element.attributes.items())) + def items(self): return [(item[0], item[1]) for item in - self.element.attributes.items()] + list(self.element.attributes.items())] + def keys(self): - return self.element.attributes.keys() + return list(self.element.attributes.keys()) + def __getitem__(self, name): return self.element.getAttribute(name) @@ -48,68 +41,68 @@ def getDomBuilder(DomImplementation): raise NotImplementedError else: return self.element.hasAttribute(name) - + class NodeBuilder(_base.Node): def __init__(self, element): _base.Node.__init__(self, element.nodeName) self.element = element - namespace = property(lambda self:hasattr(self.element, "namespaceURI") + namespace = property(lambda self: hasattr(self.element, "namespaceURI") and self.element.namespaceURI or None) def appendChild(self, node): node.parent = self self.element.appendChild(node.element) - + def insertText(self, data, insertBefore=None): text = self.element.ownerDocument.createTextNode(data) if insertBefore: self.element.insertBefore(text, insertBefore.element) else: self.element.appendChild(text) - + def insertBefore(self, node, refNode): self.element.insertBefore(node.element, refNode.element) node.parent = self - + def removeChild(self, node): if node.element.parentNode == self.element: self.element.removeChild(node.element) node.parent = None - + def reparentChildren(self, newParent): while self.element.hasChildNodes(): child = self.element.firstChild self.element.removeChild(child) newParent.element.appendChild(child) self.childNodes = [] - + def getAttributes(self): return AttrList(self.element) - + def setAttributes(self, attributes): if attributes: - for name, value in attributes.items(): + for name, value in list(attributes.items()): if isinstance(name, tuple): if name[0] is not None: qualifiedName = (name[0] + ":" + name[1]) else: qualifiedName = name[1] - self.element.setAttributeNS(name[2], qualifiedName, + self.element.setAttributeNS(name[2], qualifiedName, value) else: self.element.setAttribute( name, value) attributes = property(getAttributes, setAttributes) - + def cloneNode(self): return NodeBuilder(self.element.cloneNode(False)) - + def hasContent(self): return self.element.hasChildNodes() def getNameTuple(self): - if self.namespace == None: + if self.namespace is None: return namespaces["html"], self.name else: return self.namespace, self.name @@ -118,9 +111,9 @@ def getDomBuilder(DomImplementation): class TreeBuilder(_base.TreeBuilder): def documentClass(self): - self.dom = Dom.getDOMImplementation().createDocument(None,None,None) + self.dom = Dom.getDOMImplementation().createDocument(None, None, None) return weakref.proxy(self) - + def insertDoctype(self, token): name = token["name"] publicId = token["publicId"] @@ -131,7 +124,7 @@ def getDomBuilder(DomImplementation): self.document.appendChild(NodeBuilder(doctype)) if Dom == minidom: doctype.ownerDocument = self.dom - + def elementClass(self, name, namespace=None): if namespace is None and self.defaultNamespace is None: node = self.dom.createElement(name) @@ -139,70 +132,72 @@ def getDomBuilder(DomImplementation): node = self.dom.createElementNS(namespace, name) return NodeBuilder(node) - + def commentClass(self, data): return NodeBuilder(self.dom.createComment(data)) - + def fragmentClass(self): return NodeBuilder(self.dom.createDocumentFragment()) - + def appendChild(self, node): self.dom.appendChild(node.element) - + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): return self.dom - + def getFragment(self): return _base.TreeBuilder.getFragment(self).element - + def insertText(self, data, parent=None): - data=data - if parent <> self: + data = data + if parent != self: _base.TreeBuilder.insertText(self, data, parent) else: # HACK: allow text nodes as children of the document node if hasattr(self.dom, '_child_node_types'): if not Node.TEXT_NODE in self.dom._child_node_types: - self.dom._child_node_types=list(self.dom._child_node_types) + self.dom._child_node_types = list(self.dom._child_node_types) self.dom._child_node_types.append(Node.TEXT_NODE) self.dom.appendChild(self.dom.createTextNode(data)) - + + implementation = DomImplementation name = None - + def testSerializer(element): element.normalize() rv = [] + def serializeElement(element, indent=0): if element.nodeType == Node.DOCUMENT_TYPE_NODE: if element.name: if element.publicId or element.systemId: publicId = element.publicId or "" systemId = element.systemId or "" - rv.append( """|%s"""%( - ' '*indent, element.name, publicId, systemId)) + rv.append("""|%s""" % + (' ' * indent, element.name, publicId, systemId)) else: - rv.append("|%s"%(' '*indent, element.name)) + rv.append("|%s" % (' ' * indent, element.name)) else: - rv.append("|%s"%(' '*indent,)) + rv.append("|%s" % (' ' * indent,)) elif element.nodeType == Node.DOCUMENT_NODE: rv.append("#document") elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE: rv.append("#document-fragment") elif element.nodeType == Node.COMMENT_NODE: - rv.append("|%s"%(' '*indent, element.nodeValue)) + rv.append("|%s" % (' ' * indent, element.nodeValue)) elif element.nodeType == Node.TEXT_NODE: - rv.append("|%s\"%s\"" %(' '*indent, element.nodeValue)) + rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue)) else: if (hasattr(element, "namespaceURI") and - element.namespaceURI != None): - name = "%s %s"%(constants.prefixes[element.namespaceURI], - element.nodeName) + element.namespaceURI is not None): + name = "%s %s" % (constants.prefixes[element.namespaceURI], + element.nodeName) else: name = element.nodeName - rv.append("|%s<%s>"%(' '*indent, name)) + rv.append("|%s<%s>" % (' ' * indent, name)) if element.hasAttributes(): attributes = [] for i in range(len(element.attributes)): @@ -211,81 +206,22 @@ def getDomBuilder(DomImplementation): value = attr.value ns = attr.namespaceURI if ns: - name = "%s %s"%(constants.prefixes[ns], attr.localName) + name = "%s %s" % (constants.prefixes[ns], attr.localName) else: name = attr.nodeName attributes.append((name, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) indent += 2 for child in element.childNodes: serializeElement(child, indent) serializeElement(element, 0) - + return "\n".join(rv) - - def dom2sax(node, handler, nsmap={'xml':XML_NAMESPACE}): - if node.nodeType == Node.ELEMENT_NODE: - if not nsmap: - handler.startElement(node.nodeName, node.attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElement(node.nodeName) - else: - attributes = dict(node.attributes.itemsNS()) - - # gather namespace declarations - prefixes = [] - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if (attr.namespaceURI == XMLNS_NAMESPACE or - (attr.namespaceURI == None and attr.nodeName.startswith('xmlns'))): - prefix = (attr.nodeName != 'xmlns' and attr.nodeName or None) - handler.startPrefixMapping(prefix, attr.nodeValue) - prefixes.append(prefix) - nsmap = nsmap.copy() - nsmap[prefix] = attr.nodeValue - del attributes[(attr.namespaceURI, attr.nodeName)] - - # apply namespace declarations - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if attr.namespaceURI == None and ':' in attr.nodeName: - prefix = attr.nodeName.split(':')[0] - if nsmap.has_key(prefix): - del attributes[(attr.namespaceURI, attr.nodeName)] - attributes[(nsmap[prefix],attr.nodeName)]=attr.nodeValue - - # SAX events - ns = node.namespaceURI or nsmap.get(None,None) - handler.startElementNS((ns,node.nodeName), node.nodeName, attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElementNS((ns, node.nodeName), node.nodeName) - for prefix in prefixes: handler.endPrefixMapping(prefix) - - elif node.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: - handler.characters(node.nodeValue) - - elif node.nodeType == Node.DOCUMENT_NODE: - handler.startDocument() - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endDocument() - - elif node.nodeType == Node.DOCUMENT_FRAGMENT_NODE: - for child in node.childNodes: dom2sax(child, handler, nsmap) - - else: - # ATTRIBUTE_NODE - # ENTITY_NODE - # PROCESSING_INSTRUCTION_NODE - # COMMENT_NODE - # DOCUMENT_TYPE_NODE - # NOTATION_NODE - pass - + return locals() -# Keep backwards compatibility with things that directly load -# classes/functions from this module -for key, value in getDomModule(minidom).__dict__.items(): - globals()[key] = value + +# The actual means to get a module! +getDomModule = moduleFactoryFactory(getDomBuilder) diff --git a/libs/html5lib/treebuilders/etree.py b/libs/html5lib/treebuilders/etree.py index 95be4755..2c8ed19f 100755 --- a/libs/html5lib/treebuilders/etree.py +++ b/libs/html5lib/treebuilders/etree.py @@ -1,32 +1,21 @@ -try: - from types import ModuleType -except: - from new import module as ModuleType -import re -import types +from __future__ import absolute_import, division, unicode_literals +from six import text_type -import _base -from html5lib import ihatexml -from html5lib import constants -from html5lib.constants import namespaces +import re + +from . import _base +from .. import ihatexml +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation, fullTree=False): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation, fullTree) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation, fullTree=False): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag + class Element(_base.Node): def __init__(self, name, namespace=None): self._name = name @@ -45,16 +34,16 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) return etree_tag - + def _setName(self, name): self._name = name self._element.tag = self._getETreeTag(self._name, self._namespace) - + def _getName(self): return self._name - + name = property(_getName, _setName) def _setNamespace(self, namespace): @@ -65,81 +54,82 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): return self._namespace namespace = property(_getNamespace, _setNamespace) - + def _getAttributes(self): return self._element.attrib - + def _setAttributes(self, attributes): - #Delete existing attributes first - #XXX - there may be a better way to do this... - for key in self._element.attrib.keys(): + # Delete existing attributes first + # XXX - there may be a better way to do this... + for key in list(self._element.attrib.keys()): del self._element.attrib[key] - for key, value in attributes.iteritems(): + for key, value in attributes.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], key[1]) + name = "{%s}%s" % (key[2], key[1]) else: name = key self._element.set(name, value) - + attributes = property(_getAttributes, _setAttributes) - + def _getChildNodes(self): - return self._childNodes + return self._childNodes + def _setChildNodes(self, value): del self._element[:] self._childNodes = [] for element in value: self.insertChild(element) - + childNodes = property(_getChildNodes, _setChildNodes) - + def hasContent(self): """Return true if the node has children or text""" return bool(self._element.text or len(self._element)) - + def appendChild(self, node): self._childNodes.append(node) self._element.append(node._element) node.parent = self - + def insertBefore(self, node, refNode): index = list(self._element).index(refNode._element) self._element.insert(index, node._element) node.parent = self - + def removeChild(self, node): self._element.remove(node._element) - node.parent=None - + node.parent = None + def insertText(self, data, insertBefore=None): if not(len(self._element)): if not self._element.text: self._element.text = "" self._element.text += data elif insertBefore is None: - #Insert the text as the tail of the last child element + # Insert the text as the tail of the last child element if not self._element[-1].tail: self._element[-1].tail = "" self._element[-1].tail += data else: - #Insert the text before the specified node + # Insert the text before the specified node children = list(self._element) index = children.index(insertBefore._element) if index > 0: - if not self._element[index-1].tail: - self._element[index-1].tail = "" - self._element[index-1].tail += data + if not self._element[index - 1].tail: + self._element[index - 1].tail = "" + self._element[index - 1].tail += data else: if not self._element.text: self._element.text = "" self._element.text += data - + def cloneNode(self): element = type(self)(self.name, self.namespace) - for name, value in self.attributes.iteritems(): + for name, value in self.attributes.items(): element.attributes[name] = value return element - + def reparentChildren(self, newParent): if newParent.childNodes: newParent.childNodes[-1]._element.tail += self._element.text @@ -150,60 +140,60 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): newParent._element.text += self._element.text self._element.text = "" _base.Node.reparentChildren(self, newParent) - + class Comment(Element): def __init__(self, data): - #Use the superclass constructor to set all properties on the - #wrapper element + # Use the superclass constructor to set all properties on the + # wrapper element self._element = ElementTree.Comment(data) self.parent = None self._childNodes = [] self._flags = [] - + def _getData(self): return self._element.text - + def _setData(self, value): self._element.text = value - + data = property(_getData, _setData) - + class DocumentType(Element): def __init__(self, name, publicId, systemId): - Element.__init__(self, "") + Element.__init__(self, "") self._element.text = name self.publicId = publicId self.systemId = systemId def _getPublicId(self): - return self._element.get(u"publicId", "") + return self._element.get("publicId", "") def _setPublicId(self, value): if value is not None: - self._element.set(u"publicId", value) + self._element.set("publicId", value) publicId = property(_getPublicId, _setPublicId) - + def _getSystemId(self): - return self._element.get(u"systemId", "") + return self._element.get("systemId", "") def _setSystemId(self, value): if value is not None: - self._element.set(u"systemId", value) + self._element.set("systemId", value) systemId = property(_getSystemId, _setSystemId) - + class Document(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_ROOT") + class DocumentFragment(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_FRAGMENT") + def testSerializer(element): rv = [] - finalText = None + def serializeElement(element, indent=0): if not(hasattr(element, "tag")): element = element.getroot() @@ -211,20 +201,23 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": + rv.append("""""" % + (element.text, publicId, systemId)) + else: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": rv.append("#document") - if element.text: - rv.append("|%s\"%s\""%(' '*(indent+2), element.text)) - if element.tail: - finalText = element.tail - elif element.tag == ElementTree.Comment: - rv.append("|%s"%(' '*indent, element.text)) + if element.text is not None: + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + elif element.tag == ElementTreeCommentType: + rv.append("|%s" % (' ' * indent, element.text)) else: - assert type(element.tag) in types.StringTypes, "Expected unicode, got %s"%type(element.tag) + assert isinstance(element.tag, text_type), \ + "Expected unicode, got %s, %s" % (type(element.tag), element.tag) nsmatch = tag_regexp.match(element.tag) if nsmatch is None: @@ -232,113 +225,113 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): else: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - name = "%s %s"%(prefix, name) - rv.append("|%s<%s>"%(' '*indent, name)) + name = "%s %s" % (prefix, name) + rv.append("|%s<%s>" % (' ' * indent, name)) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: attr_string = name attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 for child in element: serializeElement(child, indent) if element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) - - if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) - + return "\n".join(rv) - + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] - finalText = None filter = ihatexml.InfosetFilter() + def serializeElement(element): - if type(element) == type(ElementTree.ElementTree): + if isinstance(element, ElementTree.ElementTree): element = element.getroot() - + if element.tag == "": if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": - if element.text: - rv.append(element.text) - if element.tail: - finalText = element.tail - - for child in element: - serializeElement(child) - - elif type(element.tag) == type(ElementTree.Comment): - rv.append(""%(element.text,)) - else: - #This is assumed to be an ordinary element - if not element.attrib: - rv.append("<%s>"%(filter.fromXmlName(element.tag),)) + rv.append("""""" % + (element.text, publicId, systemId)) else: - attr = " ".join(["%s=\"%s\""%( - filter.fromXmlName(name), value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) - if element.text: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": + if element.text is not None: rv.append(element.text) - + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + for child in element: serializeElement(child) - - rv.append("%s>"%(element.tag,)) - + + elif element.tag == ElementTreeCommentType: + rv.append("" % (element.text,)) + else: + # This is assumed to be an ordinary element + if not element.attrib: + rv.append("<%s>" % (filter.fromXmlName(element.tag),)) + else: + attr = " ".join(["%s=\"%s\"" % ( + filter.fromXmlName(name), value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) + if element.text: + rv.append(element.text) + + for child in element: + serializeElement(child) + + rv.append("%s>" % (element.tag,)) + if element.tail: rv.append(element.tail) - + serializeElement(element) - - if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) - + return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = Element commentClass = Comment fragmentClass = DocumentFragment - + implementation = ElementTreeImplementation + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): if fullTree: return self.document._element else: if self.defaultNamespace is not None: return self.document._element.find( - "{%s}html"%self.defaultNamespace) + "{%s}html" % self.defaultNamespace) else: return self.document._element.find("html") - + def getFragment(self): return _base.TreeBuilder.getFragment(self)._element - + return locals() + + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treebuilders/etree_lxml.py b/libs/html5lib/treebuilders/etree_lxml.py index eee1e3b2..35d08efa 100644 --- a/libs/html5lib/treebuilders/etree_lxml.py +++ b/libs/html5lib/treebuilders/etree_lxml.py @@ -1,20 +1,3 @@ -import warnings -import re - -import _base -from html5lib.constants import DataLossWarning -import html5lib.constants as constants -import etree as etree_builders -from html5lib import ihatexml - -try: - import lxml.etree as etree -except ImportError: - pass - -fullTree = True -tag_regexp = re.compile("{([^}]*)}(.*)") - """Module for supporting the lxml.etree library. The idea here is to use as much of the native library as possible, without using fragile hacks like custom element names that break between releases. The downside of this is that we cannot represent @@ -26,12 +9,34 @@ Docypes with no name When any of these things occur, we emit a DataLossWarning """ +from __future__ import absolute_import, division, unicode_literals + +import warnings +import re +import sys + +from . import _base +from ..constants import DataLossWarning +from .. import constants +from . import etree as etree_builders +from .. import ihatexml + +import lxml.etree as etree + + +fullTree = True +tag_regexp = re.compile("{([^}]*)}(.*)") + +comment_type = etree.Comment("asd").tag + + class DocumentType(object): def __init__(self, name, publicId, systemId): - self.name = name + self.name = name self.publicId = publicId self.systemId = systemId + class Document(object): def __init__(self): self._elementTree = None @@ -42,118 +47,126 @@ class Document(object): def _getChildNodes(self): return self._childNodes - + childNodes = property(_getChildNodes) + def testSerializer(element): rv = [] finalText = None - filter = ihatexml.InfosetFilter() + infosetFilter = ihatexml.InfosetFilter() + def serializeElement(element, indent=0): if not hasattr(element, "tag"): - if hasattr(element, "getroot"): - #Full tree case + if hasattr(element, "getroot"): + # Full tree case rv.append("#document") if element.docinfo.internalDTD: - if not (element.docinfo.public_id or + if not (element.docinfo.public_id or element.docinfo.system_url): - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name else: - dtd_str = """"""%( - element.docinfo.root_name, + dtd_str = """""" % ( + element.docinfo.root_name, element.docinfo.public_id, element.docinfo.system_url) - rv.append("|%s%s"%(' '*(indent+2), dtd_str)) + rv.append("|%s%s" % (' ' * (indent + 2), dtd_str)) next_element = element.getroot() while next_element.getprevious() is not None: next_element = next_element.getprevious() while next_element is not None: - serializeElement(next_element, indent+2) + serializeElement(next_element, indent + 2) next_element = next_element.getnext() - elif isinstance(element, basestring): - #Text in a fragment - rv.append("|%s\"%s\""%(' '*indent, element)) + elif isinstance(element, str) or isinstance(element, bytes): + # Text in a fragment + assert isinstance(element, str) or sys.version_info.major == 2 + rv.append("|%s\"%s\"" % (' ' * indent, element)) else: - #Fragment case + # Fragment case rv.append("#document-fragment") for next_element in element: - serializeElement(next_element, indent+2) - elif type(element.tag) == type(etree.Comment): - rv.append("|%s"%(' '*indent, element.text)) + serializeElement(next_element, indent + 2) + elif element.tag == comment_type: + rv.append("|%s" % (' ' * indent, element.text)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * indent, element.tail)) else: + assert isinstance(element, etree._Element) nsmatch = etree_builders.tag_regexp.match(element.tag) if nsmatch is not None: ns = nsmatch.group(1) tag = nsmatch.group(2) prefix = constants.prefixes[ns] - rv.append("|%s<%s %s>"%(' '*indent, prefix, - filter.fromXmlName(tag))) + rv.append("|%s<%s %s>" % (' ' * indent, prefix, + infosetFilter.fromXmlName(tag))) else: - rv.append("|%s<%s>"%(' '*indent, - filter.fromXmlName(element.tag))) + rv.append("|%s<%s>" % (' ' * indent, + infosetFilter.fromXmlName(element.tag))) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() - name = filter.fromXmlName(name) + name = infosetFilter.fromXmlName(name) prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: - attr_string = filter.fromXmlName(name) + attr_string = infosetFilter.fromXmlName(name) attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 - for child in element.getchildren(): + for child in element: serializeElement(child, indent) - if hasattr(element, "tail") and element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) + rv.append("|%s\"%s\"" % (' ' * 2, finalText)) return "\n".join(rv) + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] finalText = None + def serializeElement(element): if not hasattr(element, "tag"): if element.docinfo.internalDTD: if element.docinfo.doctype: dtd_str = element.docinfo.doctype else: - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name rv.append(dtd_str) serializeElement(element.getroot()) - - elif type(element.tag) == type(etree.Comment): - rv.append(""%(element.text,)) - + + elif element.tag == comment_type: + rv.append("" % (element.text,)) + else: - #This is assumed to be an ordinary element + # This is assumed to be an ordinary element if not element.attrib: - rv.append("<%s>"%(element.tag,)) + rv.append("<%s>" % (element.tag,)) else: - attr = " ".join(["%s=\"%s\""%(name, value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) + attr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) if element.text: rv.append(element.text) - for child in element.getchildren(): + for child in element: serializeElement(child) - rv.append("%s>"%(element.tag,)) + rv.append("%s>" % (element.tag,)) if hasattr(element, "tail") and element.tail: rv.append(element.tail) @@ -161,56 +174,57 @@ def tostring(element): serializeElement(element) if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) + rv.append("%s\"" % (' ' * 2, finalText)) return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = None commentClass = None - fragmentClass = Document + fragmentClass = Document + implementation = etree - def __init__(self, namespaceHTMLElements, fullTree = False): + def __init__(self, namespaceHTMLElements, fullTree=False): builder = etree_builders.getETreeModule(etree, fullTree=fullTree) - filter = self.filter = ihatexml.InfosetFilter() + infosetFilter = self.infosetFilter = ihatexml.InfosetFilter() self.namespaceHTMLElements = namespaceHTMLElements class Attributes(dict): def __init__(self, element, value={}): self._element = element dict.__init__(self, value) - for key, value in self.iteritems(): + for key, value in self.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value def __setitem__(self, key, value): dict.__setitem__(self, key, value) if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value class Element(builder.Element): def __init__(self, name, namespace): - name = filter.coerceElement(name) + name = infosetFilter.coerceElement(name) builder.Element.__init__(self, name, namespace=namespace) self._attributes = Attributes(self) def _setName(self, name): - self._name = filter.coerceElement(name) + self._name = infosetFilter.coerceElement(name) self._element.tag = self._getETreeTag( self._name, self._namespace) - + def _getName(self): - return filter.fromXmlName(self._name) - + return infosetFilter.fromXmlName(self._name) + name = property(_getName, _setName) def _getAttributes(self): @@ -218,24 +232,23 @@ class TreeBuilder(_base.TreeBuilder): def _setAttributes(self, attributes): self._attributes = Attributes(self, attributes) - + attributes = property(_getAttributes, _setAttributes) def insertText(self, data, insertBefore=None): - data = filter.coerceCharacters(data) + data = infosetFilter.coerceCharacters(data) builder.Element.insertText(self, data, insertBefore) def appendChild(self, child): builder.Element.appendChild(self, child) - class Comment(builder.Comment): def __init__(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) builder.Comment.__init__(self, data) def _setData(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) self._element.text = data def _getData(self): @@ -245,9 +258,9 @@ class TreeBuilder(_base.TreeBuilder): self.elementClass = Element self.commentClass = builder.Comment - #self.fragmentClass = builder.DocumentFragment + # self.fragmentClass = builder.DocumentFragment _base.TreeBuilder.__init__(self, namespaceHTMLElements) - + def reset(self): _base.TreeBuilder.reset(self) self.insertComment = self.insertCommentInitial @@ -262,13 +275,13 @@ class TreeBuilder(_base.TreeBuilder): return self.document._elementTree else: return self.document._elementTree.getroot() - + def getFragment(self): fragment = [] element = self.openElements[0]._element if element.text: fragment.append(element.text) - fragment.extend(element.getchildren()) + fragment.extend(list(element)) if element.tail: fragment.append(element.tail) return fragment @@ -278,59 +291,79 @@ class TreeBuilder(_base.TreeBuilder): publicId = token["publicId"] systemId = token["systemId"] - if not name or ihatexml.nonXmlNameBMPRegexp.search(name) or name[0] == '"': - warnings.warn("lxml cannot represent null or non-xml doctype", DataLossWarning) + if not name: + warnings.warn("lxml cannot represent empty doctype", DataLossWarning) + self.doctype = None + else: + coercedName = self.infosetFilter.coerceElement(name) + if coercedName != name: + warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning) + + doctype = self.doctypeClass(coercedName, publicId, systemId) + self.doctype = doctype - doctype = self.doctypeClass(name, publicId, systemId) - self.doctype = doctype - def insertCommentInitial(self, data, parent=None): self.initial_comments.append(data) - + + def insertCommentMain(self, data, parent=None): + if (parent == self.document and + self.document._elementTree.getroot()[-1].tag == comment_type): + warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning) + super(TreeBuilder, self).insertComment(data, parent) + def insertRoot(self, token): """Create the document root""" - #Because of the way libxml2 works, it doesn't seem to be possible to - #alter information like the doctype after the tree has been parsed. - #Therefore we need to use the built-in parser to create our iniial - #tree, after which we can add elements like normal + # Because of the way libxml2 works, it doesn't seem to be possible to + # alter information like the doctype after the tree has been parsed. + # Therefore we need to use the built-in parser to create our iniial + # tree, after which we can add elements like normal docStr = "" - if self.doctype and self.doctype.name and not self.doctype.name.startswith('"'): - docStr += "= 0 and sysid.find('"') >= 0: + warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning) + sysid = sysid.replace("'", 'U00027') + if sysid.find("'") >= 0: + docStr += '"%s"' % sysid + else: + docStr += "'%s'" % sysid + else: + docStr += "''" docStr += ">" + if self.doctype.name != token["name"]: + warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning) docStr += "" - - try: - root = etree.fromstring(docStr) - except etree.XMLSyntaxError: - print docStr - raise - - #Append the initial comments: + root = etree.fromstring(docStr) + + # Append the initial comments: for comment_token in self.initial_comments: root.addprevious(etree.Comment(comment_token["data"])) - - #Create the root document and add the ElementTree to it + + # Create the root document and add the ElementTree to it self.document = self.documentClass() self.document._elementTree = root.getroottree() - + # Give the root element the right name name = token["name"] namespace = token.get("namespace", self.defaultNamespace) if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) root.tag = etree_tag - - #Add the root element to the internal child/open data structures + + # Add the root element to the internal child/open data structures root_element = self.elementClass(name, namespace) root_element._element = root self.document._childNodes.append(root_element) self.openElements.append(root_element) - - #Reset to the default insert comment function - self.insertComment = super(TreeBuilder, self).insertComment + + # Reset to the default insert comment function + self.insertComment = self.insertCommentMain diff --git a/libs/html5lib/treebuilders/simpletree.py b/libs/html5lib/treebuilders/simpletree.py deleted file mode 100755 index 67fe7583..00000000 --- a/libs/html5lib/treebuilders/simpletree.py +++ /dev/null @@ -1,256 +0,0 @@ -import _base -from html5lib.constants import voidElements, namespaces, prefixes -from xml.sax.saxutils import escape - -# Really crappy basic implementation of a DOM-core like thing -class Node(_base.Node): - type = -1 - def __init__(self, name): - self.name = name - self.parent = None - self.value = None - self.childNodes = [] - self._flags = [] - - def __iter__(self): - for node in self.childNodes: - yield node - for item in node: - yield item - - def __unicode__(self): - return self.name - - def toxml(self): - raise NotImplementedError - - def printTree(self, indent=0): - tree = '\n|%s%s' % (' '* indent, unicode(self)) - for child in self.childNodes: - tree += child.printTree(indent + 2) - return tree - - def appendChild(self, node): - assert isinstance(node, Node) - if (isinstance(node, TextNode) and self.childNodes and - isinstance(self.childNodes[-1], TextNode)): - self.childNodes[-1].value += node.value - else: - self.childNodes.append(node) - node.parent = self - - def insertText(self, data, insertBefore=None): - assert isinstance(data, unicode), "data %s is of type %s expected unicode"%(repr(data), type(data)) - if insertBefore is None: - self.appendChild(TextNode(data)) - else: - self.insertBefore(TextNode(data), insertBefore) - - def insertBefore(self, node, refNode): - index = self.childNodes.index(refNode) - if (isinstance(node, TextNode) and index > 0 and - isinstance(self.childNodes[index - 1], TextNode)): - self.childNodes[index - 1].value += node.value - else: - self.childNodes.insert(index, node) - node.parent = self - - def removeChild(self, node): - try: - self.childNodes.remove(node) - except: - # XXX - raise - node.parent = None - - def cloneNode(self): - raise NotImplementedError - - def hasContent(self): - """Return true if the node has children or text""" - return bool(self.childNodes) - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class Document(Node): - type = 1 - def __init__(self): - Node.__init__(self, None) - - def __str__(self): - return "#document" - - def __unicode__(self): - return str(self) - - def appendChild(self, child): - Node.appendChild(self, child) - - def toxml(self, encoding="utf=8"): - result = "" - for child in self.childNodes: - result += child.toxml() - return result.encode(encoding) - - def hilite(self, encoding="utf-8"): - result = "" - for child in self.childNodes: - result += child.hilite() - return result.encode(encoding) + "" - - def printTree(self): - tree = unicode(self) - for child in self.childNodes: - tree += child.printTree(2) - return tree - - def cloneNode(self): - return Document() - -class DocumentFragment(Document): - type = 2 - def __str__(self): - return "#document-fragment" - - def __unicode__(self): - return str(self) - - def cloneNode(self): - return DocumentFragment() - -class DocumentType(Node): - type = 3 - def __init__(self, name, publicId, systemId): - Node.__init__(self, name) - self.publicId = publicId - self.systemId = systemId - - def __unicode__(self): - if self.publicId or self.systemId: - publicId = self.publicId or "" - systemId = self.systemId or "" - return """"""%( - self.name, publicId, systemId) - - else: - return u"" % self.name - - - toxml = __unicode__ - - def hilite(self): - return '<!DOCTYPE %s>' % self.name - - def cloneNode(self): - return DocumentType(self.name, self.publicId, self.systemId) - -class TextNode(Node): - type = 4 - def __init__(self, value): - Node.__init__(self, None) - self.value = value - - def __unicode__(self): - return u"\"%s\"" % self.value - - def toxml(self): - return escape(self.value) - - hilite = toxml - - def cloneNode(self): - return TextNode(self.value) - -class Element(Node): - type = 5 - def __init__(self, name, namespace=None): - Node.__init__(self, name) - self.namespace = namespace - self.attributes = {} - - def __unicode__(self): - if self.namespace == None: - return u"<%s>" % self.name - else: - return u"<%s %s>"%(prefixes[self.namespace], self.name) - - def toxml(self): - result = '<' + self.name - if self.attributes: - for name,value in self.attributes.iteritems(): - result += u' %s="%s"' % (name, escape(value,{'"':'"'})) - if self.childNodes: - result += '>' - for child in self.childNodes: - result += child.toxml() - result += u'%s>' % self.name - else: - result += u'/>' - return result - - def hilite(self): - result = '<%s' % self.name - if self.attributes: - for name, value in self.attributes.iteritems(): - result += ' %s="%s"' % (name, escape(value, {'"':'"'})) - if self.childNodes: - result += ">" - for child in self.childNodes: - result += child.hilite() - elif self.name in voidElements: - return result + ">" - return result + '</%s>' % self.name - - def printTree(self, indent): - tree = '\n|%s%s' % (' '*indent, unicode(self)) - indent += 2 - if self.attributes: - for name, value in sorted(self.attributes.iteritems()): - if isinstance(name, tuple): - name = "%s %s"%(name[0], name[1]) - tree += '\n|%s%s="%s"' % (' ' * indent, name, value) - for child in self.childNodes: - tree += child.printTree(indent) - return tree - - def cloneNode(self): - newNode = Element(self.name) - if hasattr(self, 'namespace'): - newNode.namespace = self.namespace - for attr, value in self.attributes.iteritems(): - newNode.attributes[attr] = value - return newNode - -class CommentNode(Node): - type = 6 - def __init__(self, data): - Node.__init__(self, None) - self.data = data - - def __unicode__(self): - return "" % self.data - - def toxml(self): - return "" % self.data - - def hilite(self): - return '<!--%s-->' % escape(self.data) - - def cloneNode(self): - return CommentNode(self.data) - -class TreeBuilder(_base.TreeBuilder): - documentClass = Document - doctypeClass = DocumentType - elementClass = Element - commentClass = CommentNode - fragmentClass = DocumentFragment - - def testSerializer(self, node): - return node.printTree() diff --git a/libs/html5lib/treebuilders/soup.py b/libs/html5lib/treebuilders/soup.py deleted file mode 100644 index 9bc5ff0e..00000000 --- a/libs/html5lib/treebuilders/soup.py +++ /dev/null @@ -1,236 +0,0 @@ -import warnings - -warnings.warn("BeautifulSoup 3.x (as of 3.1) is not fully compatible with html5lib and support will be removed in the future", DeprecationWarning) - -from BeautifulSoup import BeautifulSoup, Tag, NavigableString, Comment, Declaration - -import _base -from html5lib.constants import namespaces, DataLossWarning - -class AttrList(object): - def __init__(self, element): - self.element = element - self.attrs = dict(self.element.attrs) - def __iter__(self): - return self.attrs.items().__iter__() - def __setitem__(self, name, value): - "set attr", name, value - self.element[name] = value - def items(self): - return self.attrs.items() - def keys(self): - return self.attrs.keys() - def __getitem__(self, name): - return self.attrs[name] - def __contains__(self, name): - return name in self.attrs.keys() - def __eq__(self, other): - if len(self.keys()) != len(other.keys()): - return False - for item in self.keys(): - if item not in other: - return False - if self[item] != other[item]: - return False - return True - -class Element(_base.Node): - def __init__(self, element, soup, namespace): - _base.Node.__init__(self, element.name) - self.element = element - self.soup = soup - self.namespace = namespace - - def _nodeIndex(self, node, refNode): - # Finds a node by identity rather than equality - for index in range(len(self.element.contents)): - if id(self.element.contents[index]) == id(refNode.element): - return index - return None - - def appendChild(self, node): - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[-1].__class__ == NavigableString): - # Concatenate new text onto old text node - # (TODO: This has O(n^2) performance, for input like "aaa...") - newStr = NavigableString(self.element.contents[-1]+node.element) - - # Remove the old text node - # (Can't simply use .extract() by itself, because it fails if - # an equal text node exists within the parent node) - oldElement = self.element.contents[-1] - del self.element.contents[-1] - oldElement.parent = None - oldElement.extract() - - self.element.insert(len(self.element.contents), newStr) - else: - self.element.insert(len(self.element.contents), node.element) - node.parent = self - - def getAttributes(self): - return AttrList(self.element) - - def setAttributes(self, attributes): - if attributes: - for name, value in attributes.items(): - self.element[name] = value - - attributes = property(getAttributes, setAttributes) - - def insertText(self, data, insertBefore=None): - text = TextNode(NavigableString(data), self.soup) - if insertBefore: - self.insertBefore(text, insertBefore) - else: - self.appendChild(text) - - def insertBefore(self, node, refNode): - index = self._nodeIndex(node, refNode) - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[index-1].__class__ == NavigableString): - # (See comments in appendChild) - newStr = NavigableString(self.element.contents[index-1]+node.element) - oldNode = self.element.contents[index-1] - del self.element.contents[index-1] - oldNode.parent = None - oldNode.extract() - - self.element.insert(index-1, newStr) - else: - self.element.insert(index, node.element) - node.parent = self - - def removeChild(self, node): - index = self._nodeIndex(node.parent, node) - del node.parent.element.contents[index] - node.element.parent = None - node.element.extract() - node.parent = None - - def reparentChildren(self, newParent): - while self.element.contents: - child = self.element.contents[0] - child.extract() - if isinstance(child, Tag): - newParent.appendChild(Element(child, self.soup, namespaces["html"])) - else: - newParent.appendChild(TextNode(child, self.soup)) - - def cloneNode(self): - node = Element(Tag(self.soup, self.element.name), self.soup, self.namespace) - for key,value in self.attributes: - node.attributes[key] = value - return node - - def hasContent(self): - return self.element.contents - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class TextNode(Element): - def __init__(self, element, soup): - _base.Node.__init__(self, None) - self.element = element - self.soup = soup - - def cloneNode(self): - raise NotImplementedError - -class TreeBuilder(_base.TreeBuilder): - def __init__(self, namespaceHTMLElements): - if namespaceHTMLElements: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - _base.TreeBuilder.__init__(self, namespaceHTMLElements) - - def documentClass(self): - self.soup = BeautifulSoup("") - return Element(self.soup, self.soup, None) - - def insertDoctype(self, token): - name = token["name"] - publicId = token["publicId"] - systemId = token["systemId"] - - if publicId: - self.soup.insert(0, Declaration("DOCTYPE %s PUBLIC \"%s\" \"%s\""%(name, publicId, systemId or ""))) - elif systemId: - self.soup.insert(0, Declaration("DOCTYPE %s SYSTEM \"%s\""% - (name, systemId))) - else: - self.soup.insert(0, Declaration("DOCTYPE %s"%name)) - - def elementClass(self, name, namespace): - if namespace is not None: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - return Element(Tag(self.soup, name), self.soup, namespace) - - def commentClass(self, data): - return TextNode(Comment(data), self.soup) - - def fragmentClass(self): - self.soup = BeautifulSoup("") - self.soup.name = "[document_fragment]" - return Element(self.soup, self.soup, None) - - def appendChild(self, node): - self.soup.insert(len(self.soup.contents), node.element) - - def testSerializer(self, element): - return testSerializer(element) - - def getDocument(self): - return self.soup - - def getFragment(self): - return _base.TreeBuilder.getFragment(self).element - -def testSerializer(element): - import re - rv = [] - def serializeElement(element, indent=0): - if isinstance(element, Declaration): - doctype_regexp = r'DOCTYPE\s+(?P[^\s]*)( PUBLIC "(?P.*)" "(?P.*)"| SYSTEM "(?P.*)")?' - m = re.compile(doctype_regexp).match(element.string) - assert m is not None, "DOCTYPE did not match expected format" - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') or "" - else: - systemId = m.group('systemId2') - - if publicId is not None or systemId is not None: - rv.append("""|%s"""% - (' '*indent, name, publicId or "", systemId or "")) - else: - rv.append("|%s"%(' '*indent, name)) - - elif isinstance(element, BeautifulSoup): - if element.name == "[document_fragment]": - rv.append("#document-fragment") - else: - rv.append("#document") - - elif isinstance(element, Comment): - rv.append("|%s"%(' '*indent, element.string)) - elif isinstance(element, unicode): - rv.append("|%s\"%s\"" %(' '*indent, element)) - else: - rv.append("|%s<%s>"%(' '*indent, element.name)) - if element.attrs: - for name, value in sorted(element.attrs): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) - indent += 2 - if hasattr(element, "contents"): - for child in element.contents: - serializeElement(child, indent) - serializeElement(element, 0) - - return "\n".join(rv) diff --git a/libs/html5lib/treewalkers/__init__.py b/libs/html5lib/treewalkers/__init__.py index 3a606a8b..18124e75 100644 --- a/libs/html5lib/treewalkers/__init__.py +++ b/libs/html5lib/treewalkers/__init__.py @@ -8,23 +8,27 @@ implements a 'serialize' method taking a tree as sole argument and returning an iterator generating tokens. """ +from __future__ import absolute_import, division, unicode_literals + +import sys + +from ..utils import default_etree + treeWalkerCache = {} + def getTreeWalker(treeType, implementation=None, **kwargs): """Get a TreeWalker class for various types of tree with built-in support treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" + values are: - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. "dom" - The xml.dom.minidom DOM implementation "pulldom" - The xml.dom.pulldom event stream "etree" - A generic walker for tree implementations exposing an elementtree-like interface (known to work with ElementTree, cElementTree and lxml.etree). "lxml" - Optimized walker for lxml.etree - "beautifulsoup" - Beautiful soup (if installed) "genshi" - a Genshi stream implementation - (Currently applies to the "etree" tree type only). A module @@ -33,20 +37,21 @@ def getTreeWalker(treeType, implementation=None, **kwargs): treeType = treeType.lower() if treeType not in treeWalkerCache: - if treeType in ("dom", "pulldom", "simpletree"): - mod = __import__(treeType, globals()) + if treeType in ("dom", "pulldom"): + name = "%s.%s" % (__name__, treeType) + __import__(name) + mod = sys.modules[name] treeWalkerCache[treeType] = mod.TreeWalker elif treeType == "genshi": - import genshistream + from . import genshistream treeWalkerCache[treeType] = genshistream.TreeWalker - elif treeType == "beautifulsoup": - import soup - treeWalkerCache[treeType] = soup.TreeWalker elif treeType == "lxml": - import lxmletree + from . import lxmletree treeWalkerCache[treeType] = lxmletree.TreeWalker elif treeType == "etree": - import etree + from . import etree + if implementation is None: + implementation = default_etree # XXX: NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeWalker return treeWalkerCache.get(treeType) diff --git a/libs/html5lib/treewalkers/_base.py b/libs/html5lib/treewalkers/_base.py index 5929ba05..34252e50 100644 --- a/libs/html5lib/treewalkers/_base.py +++ b/libs/html5lib/treewalkers/_base.py @@ -1,94 +1,9 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type, string_types + import gettext _ = gettext.gettext -from html5lib.constants import voidElements, spaceCharacters -spaceCharacters = u"".join(spaceCharacters) - -class TreeWalker(object): - def __init__(self, tree): - self.tree = tree - - def __iter__(self): - raise NotImplementedError - - def error(self, msg): - return {"type": "SerializeError", "data": msg} - - def normalizeAttrs(self, attrs): - newattrs = {} - if attrs: - #TODO: treewalkers should always have attrs - for (namespace,name),value in attrs.iteritems(): - namespace = unicode(namespace) if namespace else None - name = unicode(name) - value = unicode(value) - newattrs[(namespace,name)] = value - return newattrs - - def emptyTag(self, namespace, name, attrs, hasChildren=False): - yield {"type": "EmptyTag", "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - if hasChildren: - yield self.error(_("Void element has children")) - - def startTag(self, namespace, name, attrs): - return {"type": "StartTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - - def endTag(self, namespace, name): - return {"type": "EndTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": {}} - - def text(self, data): - data = unicode(data) - middle = data.lstrip(spaceCharacters) - left = data[:len(data)-len(middle)] - if left: - yield {"type": "SpaceCharacters", "data": left} - data = middle - middle = data.rstrip(spaceCharacters) - right = data[len(middle):] - if middle: - yield {"type": "Characters", "data": middle} - if right: - yield {"type": "SpaceCharacters", "data": right} - - def comment(self, data): - return {"type": "Comment", "data": unicode(data)} - - def doctype(self, name, publicId=None, systemId=None, correct=True): - return {"type": "Doctype", - "name": name is not None and unicode(name) or u"", - "publicId": publicId, - "systemId": systemId, - "correct": correct} - - def entity(self, name): - return {"type": "Entity", "name": unicode(name)} - - def unknown(self, nodeType): - return self.error(_("Unknown node type: ") + nodeType) - -class RecursiveTreeWalker(TreeWalker): - def walkChildren(self, node): - raise NodeImplementedError - - def element(self, node, namespace, name, attrs, hasChildren): - if name in voidElements: - for token in self.emptyTag(namespace, name, attrs, hasChildren): - yield token - else: - yield self.startTag(name, attrs) - if hasChildren: - for token in self.walkChildren(node): - yield token - yield self.endTag(name) - from xml.dom import Node DOCUMENT = Node.DOCUMENT_NODE @@ -99,16 +14,127 @@ COMMENT = Node.COMMENT_NODE ENTITY = Node.ENTITY_NODE UNKNOWN = "<#UNKNOWN#>" +from ..constants import voidElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +def to_text(s, blank_if_none=True): + """Wrapper around six.text_type to convert None to empty string""" + if s is None: + if blank_if_none: + return "" + else: + return None + elif isinstance(s, text_type): + return s + else: + return text_type(s) + + +def is_text_or_none(string): + """Wrapper around isinstance(string_types) or is None""" + return string is None or isinstance(string, string_types) + + +class TreeWalker(object): + def __init__(self, tree): + self.tree = tree + + def __iter__(self): + raise NotImplementedError + + def error(self, msg): + return {"type": "SerializeError", "data": msg} + + def emptyTag(self, namespace, name, attrs, hasChildren=False): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + yield {"type": "EmptyTag", "name": to_text(name, False), + "namespace": to_text(namespace), + "data": attrs} + if hasChildren: + yield self.error(_("Void element has children")) + + def startTag(self, namespace, name, attrs): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + return {"type": "StartTag", + "name": text_type(name), + "namespace": to_text(namespace), + "data": dict(((to_text(namespace, False), to_text(name)), + to_text(value, False)) + for (namespace, name), value in attrs.items())} + + def endTag(self, namespace, name): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(namespace) + + return {"type": "EndTag", + "name": to_text(name, False), + "namespace": to_text(namespace), + "data": {}} + + def text(self, data): + assert isinstance(data, string_types), type(data) + + data = to_text(data) + middle = data.lstrip(spaceCharacters) + left = data[:len(data) - len(middle)] + if left: + yield {"type": "SpaceCharacters", "data": left} + data = middle + middle = data.rstrip(spaceCharacters) + right = data[len(middle):] + if middle: + yield {"type": "Characters", "data": middle} + if right: + yield {"type": "SpaceCharacters", "data": right} + + def comment(self, data): + assert isinstance(data, string_types), type(data) + + return {"type": "Comment", "data": text_type(data)} + + def doctype(self, name, publicId=None, systemId=None, correct=True): + assert is_text_or_none(name), type(name) + assert is_text_or_none(publicId), type(publicId) + assert is_text_or_none(systemId), type(systemId) + + return {"type": "Doctype", + "name": to_text(name), + "publicId": to_text(publicId), + "systemId": to_text(systemId), + "correct": to_text(correct)} + + def entity(self, name): + assert isinstance(name, string_types), type(name) + + return {"type": "Entity", "name": text_type(name)} + + def unknown(self, nodeType): + return self.error(_("Unknown node type: ") + nodeType) + + class NonRecursiveTreeWalker(TreeWalker): def getNodeDetails(self, node): raise NotImplementedError - + def getFirstChild(self, node): raise NotImplementedError - + def getNextSibling(self, node): raise NotImplementedError - + def getParentNode(self, node): raise NotImplementedError @@ -118,7 +144,6 @@ class NonRecursiveTreeWalker(TreeWalker): details = self.getNodeDetails(currentNode) type, details = details[0], details[1:] hasChildren = False - endTag = None if type == DOCTYPE: yield self.doctype(*details) @@ -130,12 +155,11 @@ class NonRecursiveTreeWalker(TreeWalker): elif type == ELEMENT: namespace, name, attributes, hasChildren = details if name in voidElements: - for token in self.emptyTag(namespace, name, attributes, + for token in self.emptyTag(namespace, name, attributes, hasChildren): yield token hasChildren = False else: - endTag = name yield self.startTag(namespace, name, attributes) elif type == COMMENT: @@ -149,12 +173,12 @@ class NonRecursiveTreeWalker(TreeWalker): else: yield self.unknown(details[0]) - + if hasChildren: firstChild = self.getFirstChild(currentNode) else: firstChild = None - + if firstChild is not None: currentNode = firstChild else: diff --git a/libs/html5lib/treewalkers/dom.py b/libs/html5lib/treewalkers/dom.py index 383b46cb..a01287a9 100644 --- a/libs/html5lib/treewalkers/dom.py +++ b/libs/html5lib/treewalkers/dom.py @@ -1,10 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom import Node import gettext _ = gettext.gettext -import _base -from html5lib.constants import voidElements +from . import _base + class TreeWalker(_base.NonRecursiveTreeWalker): def getNodeDetails(self, node): @@ -16,10 +18,13 @@ class TreeWalker(_base.NonRecursiveTreeWalker): elif node.nodeType == Node.ELEMENT_NODE: attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value - return (_base.ELEMENT, node.namespaceURI, node.nodeName, + if attr.namespaceURI: + attrs[(attr.namespaceURI, attr.localName)] = attr.value + else: + attrs[(None, attr.name)] = attr.value + return (_base.ELEMENT, node.namespaceURI, node.nodeName, attrs, node.hasChildNodes()) elif node.nodeType == Node.COMMENT_NODE: diff --git a/libs/html5lib/treewalkers/etree.py b/libs/html5lib/treewalkers/etree.py index 13b03194..fd8a9cc9 100644 --- a/libs/html5lib/treewalkers/etree.py +++ b/libs/html5lib/treewalkers/etree.py @@ -1,33 +1,28 @@ +from __future__ import absolute_import, division, unicode_literals + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + OrderedDict = dict import gettext _ = gettext.gettext -try: - from types import ModuleType -except: - from new import module as ModuleType -import copy import re -import _base -from html5lib.constants import voidElements +from six import text_type + +from . import _base +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag class TreeWalker(_base.NonRecursiveTreeWalker): """Given the particular ElementTree representation, this implementation, @@ -35,16 +30,16 @@ def getETreeBuilder(ElementTreeImplementation): content: 1. The current element - + 2. The index of the element relative to its parent - + 3. A stack of ancestor elements - + 4. A flag "text", "tail" or None to indicate if the current node is a text node; either the text or tail of the current element (1) """ def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Element + if isinstance(node, tuple): # It might be the root Element elt, key, parents, flag = node if flag in ("text", "tail"): return _base.TEXT, getattr(elt, flag) @@ -54,41 +49,41 @@ def getETreeBuilder(ElementTreeImplementation): if not(hasattr(node, "tag")): node = node.getroot() - if node.tag in ("", ""): + if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"): return (_base.DOCUMENT,) elif node.tag == "": - return (_base.DOCTYPE, node.text, + return (_base.DOCTYPE, node.text, node.get("publicId"), node.get("systemId")) - elif node.tag == ElementTree.Comment: + elif node.tag == ElementTreeCommentType: return _base.COMMENT, node.text else: - assert type(node.tag) in (str, unicode), type(node.tag) - #This is assumed to be an ordinary element + assert type(node.tag) == text_type, type(node.tag) + # This is assumed to be an ordinary element match = tag_regexp.match(node.tag) if match: namespace, tag = match.groups() else: namespace = None tag = node.tag - attrs = {} - for name, value in node.attrib.items(): + attrs = OrderedDict() + for name, value in list(node.attrib.items()): match = tag_regexp.match(name) if match: - attrs[(match.group(1),match.group(2))] = value + attrs[(match.group(1), match.group(2))] = value else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, tag, + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, tag, attrs, len(node) or node.text) - + def getFirstChild(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: element, key, parents, flag = node, None, [], None - + if flag in ("text", "tail"): return None else: @@ -99,13 +94,13 @@ def getETreeBuilder(ElementTreeImplementation): return element[0], 0, parents, None else: return None - + def getNextSibling(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if len(element): parents.append(element) @@ -116,16 +111,16 @@ def getETreeBuilder(ElementTreeImplementation): if element.tail and flag != "tail": return element, key, parents, "tail" elif key < len(parents[-1]) - 1: - return parents[-1][key+1], key+1, parents, None + return parents[-1][key + 1], key + 1, parents, None else: return None - + def getParentNode(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if not parents: return element @@ -139,3 +134,5 @@ def getETreeBuilder(ElementTreeImplementation): return parent, list(parents[-1]).index(parent), parents, None return locals() + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treewalkers/genshistream.py b/libs/html5lib/treewalkers/genshistream.py index ef71a83e..f559c45d 100644 --- a/libs/html5lib/treewalkers/genshistream.py +++ b/libs/html5lib/treewalkers/genshistream.py @@ -1,50 +1,49 @@ +from __future__ import absolute_import, division, unicode_literals + +from genshi.core import QName from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT -from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -from genshi.output import NamespaceFlattener +from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -import _base +from . import _base + +from ..constants import voidElements, namespaces -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): - depth = 0 - ignore_until = None + # Buffer the events so we can pass in the following one previous = None for event in self.tree: if previous is not None: - if previous[0] == START: - depth += 1 - if ignore_until <= depth: - ignore_until = None - if ignore_until is None: - for token in self.tokens(previous, event): - yield token - if token["type"] == "EmptyTag": - ignore_until = depth - if previous[0] == END: - depth -= 1 - previous = event - if previous is not None: - if ignore_until is None or ignore_until <= depth: - for token in self.tokens(previous, None): + for token in self.tokens(previous, event): yield token - elif ignore_until is not None: - raise ValueError("Illformed DOM event stream: void element without END_ELEMENT") + previous = event + + # Don't forget the final event! + if previous is not None: + for token in self.tokens(previous, None): + yield token def tokens(self, event, next): kind, data, pos = event if kind == START: - tag, attrib = data + tag, attribs = data name = tag.localname namespace = tag.namespace - if tag in voidElements: - for token in self.emptyTag(namespace, name, list(attrib), - not next or next[0] != END + converted_attribs = {} + for k, v in attribs: + if isinstance(k, QName): + converted_attribs[(k.namespace, k.localname)] = v + else: + converted_attribs[(None, k)] = v + + if namespace == namespaces["html"] and name in voidElements: + for token in self.emptyTag(namespace, name, converted_attribs, + not next or next[0] != END or next[1] != tag): yield token else: - yield self.startTag(namespace, name, list(attrib)) + yield self.startTag(namespace, name, converted_attribs) elif kind == END: name = data.localname @@ -62,8 +61,8 @@ class TreeWalker(_base.TreeWalker): elif kind == DOCTYPE: yield self.doctype(*data) - elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, \ - START_CDATA, END_CDATA, PI): + elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, + START_CDATA, END_CDATA, PI): pass else: diff --git a/libs/html5lib/treewalkers/lxmletree.py b/libs/html5lib/treewalkers/lxmletree.py index 46f4908c..375cc2e8 100644 --- a/libs/html5lib/treewalkers/lxmletree.py +++ b/libs/html5lib/treewalkers/lxmletree.py @@ -1,186 +1,208 @@ -from lxml import etree -from html5lib.treebuilders.etree import tag_regexp - -from gettext import gettext -_ = gettext - -import _base - -from html5lib.constants import voidElements -from html5lib import ihatexml - -class Root(object): - def __init__(self, et): - self.elementtree = et - self.children = [] - if et.docinfo.internalDTD: - self.children.append(Doctype(self, et.docinfo.root_name, - et.docinfo.public_id, - et.docinfo.system_url)) - root = et.getroot() - node = root - - while node.getprevious() is not None: - node = node.getprevious() - while node is not None: - self.children.append(node) - node = node.getnext() - - self.text = None - self.tail = None - - def __getitem__(self, key): - return self.children[key] - - def getnext(self): - return None - - def __len__(self): - return 1 - -class Doctype(object): - def __init__(self, root_node, name, public_id, system_id): - self.root_node = root_node - self.name = name - self.public_id = public_id - self.system_id = system_id - - self.text = None - self.tail = None - - def getnext(self): - return self.root_node.children[1] - -class FragmentRoot(Root): - def __init__(self, children): - self.children = [FragmentWrapper(self, child) for child in children] - self.text = self.tail = None - - def getnext(self): - return None - -class FragmentWrapper(object): - def __init__(self, fragment_root, obj): - self.root_node = fragment_root - self.obj = obj - if hasattr(self.obj, 'text'): - self.text = self.obj.text - else: - self.text = None - if hasattr(self.obj, 'tail'): - self.tail = self.obj.tail - else: - self.tail = None - self.isstring = isinstance(obj, basestring) - - def __getattr__(self, name): - return getattr(self.obj, name) - - def getnext(self): - siblings = self.root_node.children - idx = siblings.index(self) - if idx < len(siblings) - 1: - return siblings[idx + 1] - else: - return None - - def __getitem__(self, key): - return self.obj[key] - - def __nonzero__(self): - return bool(self.obj) - - def getparent(self): - return None - - def __str__(self): - return str(self.obj) - - def __unicode__(self): - return unicode(self.obj) - - def __len__(self): - return len(self.obj) - - -class TreeWalker(_base.NonRecursiveTreeWalker): - def __init__(self, tree): - if hasattr(tree, "getroot"): - tree = Root(tree) - elif isinstance(tree, list): - tree = FragmentRoot(tree) - _base.NonRecursiveTreeWalker.__init__(self, tree) - self.filter = ihatexml.InfosetFilter() - def getNodeDetails(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - return _base.TEXT, getattr(node, key) - - elif isinstance(node, Root): - return (_base.DOCUMENT,) - - elif isinstance(node, Doctype): - return _base.DOCTYPE, node.name, node.public_id, node.system_id - - elif isinstance(node, FragmentWrapper) and node.isstring: - return _base.TEXT, node - - elif node.tag == etree.Comment: - return _base.COMMENT, node.text - - elif node.tag == etree.Entity: - return _base.ENTITY, node.text[1:-1] # strip &; - - else: - #This is assumed to be an ordinary element - match = tag_regexp.match(node.tag) - if match: - namespace, tag = match.groups() - else: - namespace = None - tag = node.tag - attrs = {} - for name, value in node.attrib.items(): - match = tag_regexp.match(name) - if match: - attrs[(match.group(1),match.group(2))] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), - attrs, len(node) > 0 or node.text) - - def getFirstChild(self, node): - assert not isinstance(node, tuple), _("Text nodes have no children") - - assert len(node) or node.text, "Node has no children" - if node.text: - return (node, "text") - else: - return node[0] - - def getNextSibling(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - # XXX: we cannot use a "bool(node) and node[0] or None" construct here - # because node[0] might evaluate to False if it has no child element - if len(node): - return node[0] - else: - return None - else: # tail - return node.getnext() - - return node.tail and (node, "tail") or node.getnext() - - def getParentNode(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - return node - # else: fallback to "normal" processing - - return node.getparent() +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from lxml import etree +from ..treebuilders.etree import tag_regexp + +from gettext import gettext +_ = gettext + +from . import _base + +from .. import ihatexml + + +def ensure_str(s): + if s is None: + return None + elif isinstance(s, text_type): + return s + else: + return s.decode("utf-8", "strict") + + +class Root(object): + def __init__(self, et): + self.elementtree = et + self.children = [] + if et.docinfo.internalDTD: + self.children.append(Doctype(self, + ensure_str(et.docinfo.root_name), + ensure_str(et.docinfo.public_id), + ensure_str(et.docinfo.system_url))) + root = et.getroot() + node = root + + while node.getprevious() is not None: + node = node.getprevious() + while node is not None: + self.children.append(node) + node = node.getnext() + + self.text = None + self.tail = None + + def __getitem__(self, key): + return self.children[key] + + def getnext(self): + return None + + def __len__(self): + return 1 + + +class Doctype(object): + def __init__(self, root_node, name, public_id, system_id): + self.root_node = root_node + self.name = name + self.public_id = public_id + self.system_id = system_id + + self.text = None + self.tail = None + + def getnext(self): + return self.root_node.children[1] + + +class FragmentRoot(Root): + def __init__(self, children): + self.children = [FragmentWrapper(self, child) for child in children] + self.text = self.tail = None + + def getnext(self): + return None + + +class FragmentWrapper(object): + def __init__(self, fragment_root, obj): + self.root_node = fragment_root + self.obj = obj + if hasattr(self.obj, 'text'): + self.text = ensure_str(self.obj.text) + else: + self.text = None + if hasattr(self.obj, 'tail'): + self.tail = ensure_str(self.obj.tail) + else: + self.tail = None + self.isstring = isinstance(obj, str) or isinstance(obj, bytes) + # Support for bytes here is Py2 + if self.isstring: + self.obj = ensure_str(self.obj) + + def __getattr__(self, name): + return getattr(self.obj, name) + + def getnext(self): + siblings = self.root_node.children + idx = siblings.index(self) + if idx < len(siblings) - 1: + return siblings[idx + 1] + else: + return None + + def __getitem__(self, key): + return self.obj[key] + + def __bool__(self): + return bool(self.obj) + + def getparent(self): + return None + + def __str__(self): + return str(self.obj) + + def __unicode__(self): + return str(self.obj) + + def __len__(self): + return len(self.obj) + + +class TreeWalker(_base.NonRecursiveTreeWalker): + def __init__(self, tree): + if hasattr(tree, "getroot"): + tree = Root(tree) + elif isinstance(tree, list): + tree = FragmentRoot(tree) + _base.NonRecursiveTreeWalker.__init__(self, tree) + self.filter = ihatexml.InfosetFilter() + + def getNodeDetails(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + return _base.TEXT, ensure_str(getattr(node, key)) + + elif isinstance(node, Root): + return (_base.DOCUMENT,) + + elif isinstance(node, Doctype): + return _base.DOCTYPE, node.name, node.public_id, node.system_id + + elif isinstance(node, FragmentWrapper) and node.isstring: + return _base.TEXT, node.obj + + elif node.tag == etree.Comment: + return _base.COMMENT, ensure_str(node.text) + + elif node.tag == etree.Entity: + return _base.ENTITY, ensure_str(node.text)[1:-1] # strip &; + + else: + # This is assumed to be an ordinary element + match = tag_regexp.match(ensure_str(node.tag)) + if match: + namespace, tag = match.groups() + else: + namespace = None + tag = ensure_str(node.tag) + attrs = {} + for name, value in list(node.attrib.items()): + name = ensure_str(name) + value = ensure_str(value) + match = tag_regexp.match(name) + if match: + attrs[(match.group(1), match.group(2))] = value + else: + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), + attrs, len(node) > 0 or node.text) + + def getFirstChild(self, node): + assert not isinstance(node, tuple), _("Text nodes have no children") + + assert len(node) or node.text, "Node has no children" + if node.text: + return (node, "text") + else: + return node[0] + + def getNextSibling(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + # XXX: we cannot use a "bool(node) and node[0] or None" construct here + # because node[0] might evaluate to False if it has no child element + if len(node): + return node[0] + else: + return None + else: # tail + return node.getnext() + + return (node, "tail") if node.tail else node.getnext() + + def getParentNode(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + return node + # else: fallback to "normal" processing + + return node.getparent() diff --git a/libs/html5lib/treewalkers/pulldom.py b/libs/html5lib/treewalkers/pulldom.py index 1f8b95b8..0b0f515f 100644 --- a/libs/html5lib/treewalkers/pulldom.py +++ b/libs/html5lib/treewalkers/pulldom.py @@ -1,9 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom.pulldom import START_ELEMENT, END_ELEMENT, \ COMMENT, IGNORABLE_WHITESPACE, CHARACTERS -import _base +from . import _base + +from ..constants import voidElements -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): @@ -11,7 +14,7 @@ class TreeWalker(_base.TreeWalker): previous = None for event in self.tree: if previous is not None and \ - (ignore_until is None or previous[1] is ignore_until): + (ignore_until is None or previous[1] is ignore_until): if previous[1] is ignore_until: ignore_until = None for token in self.tokens(previous, event): @@ -31,9 +34,9 @@ class TreeWalker(_base.TreeWalker): name = node.nodeName namespace = node.namespaceURI attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value + attrs[(attr.namespaceURI, attr.localName)] = attr.value if name in voidElements: for token in self.emptyTag(namespace, name, diff --git a/libs/html5lib/treewalkers/simpletree.py b/libs/html5lib/treewalkers/simpletree.py deleted file mode 100644 index 9e6bd4c5..00000000 --- a/libs/html5lib/treewalkers/simpletree.py +++ /dev/null @@ -1,78 +0,0 @@ -import gettext -_ = gettext.gettext - -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - """Given that simpletree has no performant way of getting a node's - next sibling, this implementation returns "nodes" as tuples with the - following content: - - 1. The parent Node (Element, Document or DocumentFragment) - - 2. The child index of the current node in its parent's children list - - 3. A list used as a stack of all ancestors. It is a pair tuple whose - first item is a parent Node and second item is a child index. - """ - - def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - node = parent.childNodes[idx] - - # testing node.type allows us not to import treebuilders.simpletree - if node.type in (1, 2): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif node.type == 3: # DocumentType - return _base.DOCTYPE, node.name, node.publicId, node.systemId - - elif node.type == 4: # TextNode - return _base.TEXT, node.value - - elif node.type == 5: # Element - attrs = {} - for name, value in node.attributes.items(): - if isinstance(name, tuple): - attrs[(name[2],name[1])] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, node.namespace, node.name, - attrs, node.hasContent()) - - elif node.type == 6: # CommentNode - return _base.COMMENT, node.data - - else: - return _node.UNKNOWN, node.type - - def getFirstChild(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - parents.append((parent, idx)) - node = parent.childNodes[idx] - else: - parents = [] - - assert node.hasContent(), "Node has no children" - return (node, 0, parents) - - def getNextSibling(self, node): - assert isinstance(node, tuple), "Node is not a tuple: " + str(node) - parent, idx, parents = node - idx += 1 - if len(parent.childNodes) > idx: - return (parent, idx, parents) - else: - return None - - def getParentNode(self, node): - assert isinstance(node, tuple) - parent, idx, parents = node - if parents: - parent, idx = parents.pop() - return parent, idx, parents - else: - # HACK: We could return ``parent`` but None will stop the algorithm the same way - return None diff --git a/libs/html5lib/treewalkers/soup.py b/libs/html5lib/treewalkers/soup.py deleted file mode 100644 index fca65ecb..00000000 --- a/libs/html5lib/treewalkers/soup.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -import gettext -_ = gettext.gettext - -from BeautifulSoup import BeautifulSoup, Declaration, Comment, Tag -from html5lib.constants import namespaces -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - doctype_regexp = re.compile( - r'DOCTYPE\s+(?P[^\s]*)(\s*PUBLIC\s*"(?P.*)"\s*"(?P.*)"|\s*SYSTEM\s*"(?P.*)")?') - def getNodeDetails(self, node): - if isinstance(node, BeautifulSoup): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif isinstance(node, Declaration): # DocumentType - string = unicode(node.string) - #Slice needed to remove markup added during unicode conversion, - #but only in some versions of BeautifulSoup/Python - if string.startswith(''): - string = string[2:-1] - m = self.doctype_regexp.match(string) - #This regexp approach seems wrong and fragile - #but beautiful soup stores the doctype as a single thing and we want the seperate bits - #It should work as long as the tree is created by html5lib itself but may be wrong if it's - #been modified at all - #We could just feed to it a html5lib tokenizer, I guess... - assert m is not None, "DOCTYPE did not match expected format" - - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') - else: - systemId = m.group('systemId2') - return _base.DOCTYPE, name, publicId or "", systemId or "" - - elif isinstance(node, Comment): - string = unicode(node.string) - if string.startswith(''): - string = string[4:-3] - return _base.COMMENT, string - - elif isinstance(node, unicode): # TextNode - return _base.TEXT, node - - elif isinstance(node, Tag): # Element - return (_base.ELEMENT, namespaces["html"], node.name, - dict(node.attrs).items(), node.contents) - else: - return _base.UNKNOWN, node.__class__.__name__ - - def getFirstChild(self, node): - return node.contents[0] - - def getNextSibling(self, node): - return node.nextSibling - - def getParentNode(self, node): - return node.parent diff --git a/libs/html5lib/trie/__init__.py b/libs/html5lib/trie/__init__.py new file mode 100644 index 00000000..a8cca8a9 --- /dev/null +++ b/libs/html5lib/trie/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + +from .py import Trie as PyTrie + +Trie = PyTrie + +try: + from .datrie import Trie as DATrie +except ImportError: + pass +else: + Trie = DATrie diff --git a/libs/html5lib/trie/_base.py b/libs/html5lib/trie/_base.py new file mode 100644 index 00000000..724486b1 --- /dev/null +++ b/libs/html5lib/trie/_base.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division, unicode_literals + +from collections import Mapping + + +class Trie(Mapping): + """Abstract base class for tries""" + + def keys(self, prefix=None): + keys = super().keys() + + if prefix is None: + return set(keys) + + # Python 2.6: no set comprehensions + return set([x for x in keys if x.startswith(prefix)]) + + def has_keys_with_prefix(self, prefix): + for key in self.keys(): + if key.startswith(prefix): + return True + + return False + + def longest_prefix(self, prefix): + if prefix in self: + return prefix + + for i in range(1, len(prefix) + 1): + if prefix[:-i] in self: + return prefix[:-i] + + raise KeyError(prefix) + + def longest_prefix_item(self, prefix): + lprefix = self.longest_prefix(prefix) + return (lprefix, self[lprefix]) diff --git a/libs/html5lib/trie/datrie.py b/libs/html5lib/trie/datrie.py new file mode 100644 index 00000000..51f3d046 --- /dev/null +++ b/libs/html5lib/trie/datrie.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from datrie import Trie as DATrie +from six import text_type + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + chars = set() + for key in data.keys(): + if not isinstance(key, text_type): + raise TypeError("All keys must be strings") + for char in key: + chars.add(char) + + self._data = DATrie("".join(chars)) + for key, value in data.items(): + self._data[key] = value + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + raise NotImplementedError() + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + return self._data.keys(prefix) + + def has_keys_with_prefix(self, prefix): + return self._data.has_keys_with_prefix(prefix) + + def longest_prefix(self, prefix): + return self._data.longest_prefix(prefix) + + def longest_prefix_item(self, prefix): + return self._data.longest_prefix_item(prefix) diff --git a/libs/html5lib/trie/py.py b/libs/html5lib/trie/py.py new file mode 100644 index 00000000..c2ba3da7 --- /dev/null +++ b/libs/html5lib/trie/py.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from bisect import bisect_left + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + if not all(isinstance(x, text_type) for x in data.keys()): + raise TypeError("All keys must be strings") + + self._data = data + self._keys = sorted(data.keys()) + self._cachestr = "" + self._cachepoints = (0, len(data)) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + if prefix is None or prefix == "" or not self._keys: + return set(self._keys) + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + start = i = bisect_left(self._keys, prefix, lo, hi) + else: + start = i = bisect_left(self._keys, prefix) + + keys = set() + if start == len(self._keys): + return keys + + while self._keys[i].startswith(prefix): + keys.add(self._keys[i]) + i += 1 + + self._cachestr = prefix + self._cachepoints = (start, i) + + return keys + + def has_keys_with_prefix(self, prefix): + if prefix in self._data: + return True + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + i = bisect_left(self._keys, prefix, lo, hi) + else: + i = bisect_left(self._keys, prefix) + + if i == len(self._keys): + return False + + return self._keys[i].startswith(prefix) diff --git a/libs/html5lib/utils.py b/libs/html5lib/utils.py index d53f6788..2f41f4df 100644 --- a/libs/html5lib/utils.py +++ b/libs/html5lib/utils.py @@ -1,9 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals + +from types import ModuleType + try: - frozenset -except NameError: - #Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset + import xml.etree.cElementTree as default_etree +except ImportError: + import xml.etree.ElementTree as default_etree + + +__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", + "surrogatePairToCodepoint", "moduleFactoryFactory"] + class MethodDispatcher(dict): """Dict with 2 special properties: @@ -23,7 +30,7 @@ class MethodDispatcher(dict): # twice as fast. Please do careful performance testing before changing # anything here. _dictEntries = [] - for name,value in items: + for name, value in items: if type(name) in (list, tuple, frozenset, set): for item in name: _dictEntries.append((item, value)) @@ -35,141 +42,41 @@ class MethodDispatcher(dict): def __getitem__(self, key): return dict.get(self, key, self.default) -#Pure python implementation of deque taken from the ASPN Python Cookbook -#Original code by Raymond Hettinger -class deque(object): +# Some utility functions to dal with weirdness around UCS2 vs UCS4 +# python builds - def __init__(self, iterable=(), maxsize=-1): - if not hasattr(self, 'data'): - self.left = self.right = 0 - self.data = {} - self.maxsize = maxsize - self.extend(iterable) - - def append(self, x): - self.data[self.right] = x - self.right += 1 - if self.maxsize != -1 and len(self) > self.maxsize: - self.popleft() - - def appendleft(self, x): - self.left -= 1 - self.data[self.left] = x - if self.maxsize != -1 and len(self) > self.maxsize: - self.pop() - - def pop(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - self.right -= 1 - elem = self.data[self.right] - del self.data[self.right] - return elem - - def popleft(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - elem = self.data[self.left] - del self.data[self.left] - self.left += 1 - return elem - - def clear(self): - self.data.clear() - self.left = self.right = 0 - - def extend(self, iterable): - for elem in iterable: - self.append(elem) - - def extendleft(self, iterable): - for elem in iterable: - self.appendleft(elem) - - def rotate(self, n=1): - if self: - n %= len(self) - for i in xrange(n): - self.appendleft(self.pop()) - - def __getitem__(self, i): - if i < 0: - i += len(self) - try: - return self.data[i + self.left] - except KeyError: - raise IndexError - - def __setitem__(self, i, value): - if i < 0: - i += len(self) - try: - self.data[i + self.left] = value - except KeyError: - raise IndexError - - def __delitem__(self, i): - size = len(self) - if not (-size <= i < size): - raise IndexError - data = self.data - if i < 0: - i += size - for j in xrange(self.left+i, self.right-1): - data[j] = data[j+1] - self.pop() - - def __len__(self): - return self.right - self.left - - def __cmp__(self, other): - if type(self) != type(other): - return cmp(type(self), type(other)) - return cmp(list(self), list(other)) - - def __repr__(self, _track=[]): - if id(self) in _track: - return '...' - _track.append(id(self)) - r = 'deque(%r)' % (list(self),) - _track.remove(id(self)) - return r - - def __getstate__(self): - return (tuple(self),) - - def __setstate__(self, s): - self.__init__(s[0]) - - def __hash__(self): - raise TypeError - - def __copy__(self): - return self.__class__(self) - - def __deepcopy__(self, memo={}): - from copy import deepcopy - result = self.__class__() - memo[id(self)] = result - result.__init__(deepcopy(tuple(self), memo)) - return result - -#Some utility functions to dal with weirdness around UCS2 vs UCS4 -#python builds - -def encodingType(): - if len() == 2: - return "UCS2" - else: - return "UCS4" - -def isSurrogatePair(data): +def isSurrogatePair(data): return (len(data) == 2 and ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF) + def surrogatePairToCodepoint(data): - char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + + char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + (ord(data[1]) - 0xDC00)) return char_val + +# Module Factory Factory (no, this isn't Java, I know) +# Here to stop this being duplicated all over the place. + + +def moduleFactoryFactory(factory): + moduleCache = {} + + def moduleFactory(baseModule, *args, **kwargs): + if isinstance(ModuleType.__name__, type("")): + name = "_%s_factory" % baseModule.__name__ + else: + name = b"_%s_factory" % baseModule.__name__ + + if name in moduleCache: + return moduleCache[name] + else: + mod = ModuleType(name) + objs = factory(baseModule, *args, **kwargs) + mod.__dict__.update(objs) + moduleCache[name] = mod + return mod + + return moduleFactory diff --git a/libs/httplib2/__init__.py b/libs/httplib2/__init__.py index 01151f7f..9780d4e5 100644 --- a/libs/httplib2/__init__.py +++ b/libs/httplib2/__init__.py @@ -3,7 +3,7 @@ from __future__ import generators httplib2 A caching http interface that supports ETags and gzip -to conserve bandwidth. +to conserve bandwidth. Requires Python 2.3 or later @@ -15,17 +15,17 @@ Changelog: __author__ = "Joe Gregorio (joe@bitworking.org)" __copyright__ = "Copyright 2006, Joe Gregorio" __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", - "James Antill", - "Xavier Verges Farrero", - "Jonathan Feinberg", - "Blair Zajac", - "Sam Ruby", - "Louis Nyffenegger"] + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] __license__ = "MIT" -__version__ = "$Rev$" +__version__ = "0.8" -import re -import sys +import re +import sys import email import email.Utils import email.Message @@ -35,6 +35,7 @@ import gzip import zlib import httplib import urlparse +import urllib import base64 import os import copy @@ -42,10 +43,10 @@ import calendar import time import random import errno -# remove depracated warning in python2.6 try: from hashlib import sha1 as _sha, md5 as _md5 except ImportError: + # prior to Python 2.5, these were separate modules import sha import md5 _sha = sha.new @@ -54,21 +55,38 @@ import hmac from gettext import gettext as _ import socket -# Try using local version, followed by system, and none if neither are found try: - import lib.socks as socks + from httplib2 import socks except ImportError: try: - import socks as socks - except ImportError: + import socks + except (ImportError, AttributeError): socks = None # Build the appropriate socket wrapper for ssl try: import ssl # python 2.6 - _ssl_wrap_socket = ssl.wrap_socket -except ImportError: - def _ssl_wrap_socket(sock, key_file, cert_file): + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") ssl_sock = socket.ssl(sock, key_file, cert_file) return httplib.FakeSocket(sock, ssl_sock) @@ -84,15 +102,19 @@ def has_timeout(timeout): # python 2.6 return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) return (timeout is not None) -__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', - 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', - 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', - 'debuglevel'] +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] # The httplib debug level, set to a non-zero value to get debug output debuglevel = 0 +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 # Python 2.3 support if sys.version_info < (2,4): @@ -113,8 +135,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'): # All exceptions raised here derive from HttpLib2Error class HttpLib2Error(Exception): pass -# Some exceptions can be caught and optionally -# be turned back into responses. +# Some exceptions can be caught and optionally +# be turned back into responses. class HttpLib2ErrorWithResponse(HttpLib2Error): def __init__(self, desc, response, content): self.response = response @@ -127,8 +149,18 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class MalformedHeader(HttpLib2Error): pass class RelativeURIError(HttpLib2Error): pass class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert # Open Items: # ----------- @@ -152,6 +184,16 @@ class ServerNotFoundError(HttpLib2Error): pass # requesting that URI again. DEFAULT_MAX_REDIRECTS = 5 +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + # Which headers are hop-by-hop headers by default HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] @@ -176,7 +218,7 @@ def urlnorm(uri): raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) authority = authority.lower() scheme = scheme.lower() - if not path: + if not path: path = "/" # Could do syntax based normalization of the URI before # computing the digest. See Section 6.2.2 of Std 66. @@ -228,7 +270,7 @@ def _parse_cache_control(headers): parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] retval = dict(parts_with_args + parts_wo_args) - return retval + return retval # Whether to use a strict mode to parse WWW-Authenticate headers # Might lead to bad results in case of ill-formed header value, @@ -249,25 +291,30 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): per auth_scheme.""" retval = {} if headers.has_key(headername): - authenticate = headers[headername].strip() - www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED - while authenticate: - # Break off the scheme at the beginning of the line - if headername == 'authentication-info': - (auth_scheme, the_rest) = ('digest', authenticate) - else: - (auth_scheme, the_rest) = authenticate.split(" ", 1) - # Now loop over all the key value pairs that come after the scheme, - # being careful not to roll into the next scheme - match = www_auth.search(the_rest) - auth_params = {} - while match: - if match and len(match.groups()) == 3: - (key, value, the_rest) = match.groups() - auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + try: + + authenticate = headers[headername].strip() + www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED + while authenticate: + # Break off the scheme at the beginning of the line + if headername == 'authentication-info': + (auth_scheme, the_rest) = ('digest', authenticate) + else: + (auth_scheme, the_rest) = authenticate.split(" ", 1) + # Now loop over all the key value pairs that come after the scheme, + # being careful not to roll into the next scheme match = www_auth.search(the_rest) - retval[auth_scheme.lower()] = auth_params - authenticate = the_rest.strip() + auth_params = {} + while match: + if match and len(match.groups()) == 3: + (key, value, the_rest) = match.groups() + auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + match = www_auth.search(the_rest) + retval[auth_scheme.lower()] = auth_params + authenticate = the_rest.strip() + + except ValueError: + raise MalformedHeader("WWW-Authenticate") return retval @@ -279,17 +326,17 @@ def _entry_disposition(response_headers, request_headers): 1. Cache-Control: max-stale 2. Age: headers are not used in the calculations. - Not that this algorithm is simpler than you might think + Not that this algorithm is simpler than you might think because we are operating as a private (non-shared) cache. This lets us ignore 's-maxage'. We can also ignore 'proxy-invalidate' since we aren't a proxy. - We will never return a stale document as - fresh as a design decision, and thus the non-implementation - of 'max-stale'. This also lets us safely ignore 'must-revalidate' + We will never return a stale document as + fresh as a design decision, and thus the non-implementation + of 'max-stale'. This also lets us safely ignore 'must-revalidate' since we operate as if every server has sent 'must-revalidate'. Since we are private we get to ignore both 'public' and 'private' parameters. We also ignore 'no-transform' since - we don't do any transformations. + we don't do any transformations. The 'no-store' parameter is handled at a higher level. So the only Cache-Control parameters we look at are: @@ -298,7 +345,7 @@ def _entry_disposition(response_headers, request_headers): max-age min-fresh """ - + retval = "STALE" cc = _parse_cache_control(request_headers) cc_response = _parse_cache_control(response_headers) @@ -340,10 +387,10 @@ def _entry_disposition(response_headers, request_headers): min_fresh = int(cc['min-fresh']) except ValueError: min_fresh = 0 - current_age += min_fresh + current_age += min_fresh if freshness_lifetime > current_age: retval = "FRESH" - return retval + return retval def _decompressContent(response, new_content): content = new_content @@ -391,7 +438,7 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey): if status == 304: status = 200 - status_header = 'status: %d\r\n' % response_headers.status + status_header = 'status: %d\r\n' % status header_str = info.as_string() @@ -408,10 +455,10 @@ def _wsse_username_token(cnonce, iso_now, password): return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() -# For credentials we need two things, first +# For credentials we need two things, first # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # Then we also need a list of URIs that have already demanded authentication -# That list is tricky since sub-URIs can take the same auth, or the +# That list is tricky since sub-URIs can take the same auth, or the # auth scheme may change as you descend the tree. # So we also need each Auth instance to be able to tell us # how close to the 'top' it is. @@ -435,7 +482,7 @@ class Authentication(object): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate - Authorization header. Over-rise this in sub-classes.""" + Authorization header. Over-ride this in sub-classes.""" pass def response(self, response, content): @@ -443,7 +490,7 @@ class Authentication(object): or such returned from the last authorized response. Over-rise this in sub-classes if necessary. - Return TRUE is the request is to be retried, for + Return TRUE is the request is to be retried, for example Digest may return stale=true. """ return False @@ -461,7 +508,7 @@ class BasicAuthentication(Authentication): class DigestAuthentication(Authentication): - """Only do qop='auth' and MD5, since that + """Only do qop='auth' and MD5, since that is all Apache currently implements""" def __init__(self, credentials, host, request_uri, headers, response, content, http): Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) @@ -474,7 +521,7 @@ class DigestAuthentication(Authentication): self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() if self.challenge['algorithm'] != 'MD5': raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) - self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) + self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.challenge['nc'] = 1 def request(self, method, request_uri, headers, content, cnonce = None): @@ -482,23 +529,24 @@ class DigestAuthentication(Authentication): H = lambda x: _md5(x).hexdigest() KD = lambda s, d: H("%s:%s" % (s, d)) A2 = "".join([method, ":", request_uri]) - self.challenge['cnonce'] = cnonce or _cnonce() - request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], - '%08x' % self.challenge['nc'], - self.challenge['cnonce'], - self.challenge['qop'], H(A2) - )) - headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( - self.credentials[0], + self.challenge['cnonce'] = cnonce or _cnonce() + request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % ( + self.challenge['nonce'], + '%08x' % self.challenge['nc'], + self.challenge['cnonce'], + self.challenge['qop'], H(A2))) + headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['nonce'], - request_uri, + request_uri, self.challenge['algorithm'], request_digest, self.challenge['qop'], self.challenge['nc'], - self.challenge['cnonce'], - ) + self.challenge['cnonce']) + if self.challenge.get('opaque'): + headers['authorization'] += ', opaque="%s"' % self.challenge['opaque'] self.challenge['nc'] += 1 def response(self, response, content): @@ -506,14 +554,14 @@ class DigestAuthentication(Authentication): challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return True else: updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) if updated_challenge.has_key('nextnonce'): self.challenge['nonce'] = updated_challenge['nextnonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return False @@ -547,9 +595,8 @@ class HmacDigestAuthentication(Authentication): else: self.pwhashmod = _sha self.key = "".join([self.credentials[0], ":", - self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), - ":", self.challenge['realm'] - ]) + self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), + ":", self.challenge['realm']]) self.key = self.pwhashmod.new(self.key).hexdigest().lower() def request(self, method, request_uri, headers, content): @@ -561,16 +608,15 @@ class HmacDigestAuthentication(Authentication): cnonce = _cnonce() request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() - headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( - self.credentials[0], + headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['snonce'], cnonce, - request_uri, + request_uri, created, request_digest, - keylist, - ) + keylist) def response(self, response, content): challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {}) @@ -583,7 +629,7 @@ class WsseAuthentication(Authentication): """This is thinly tested and should not be relied upon. At this time there isn't any third party server to test against. Blogger and TypePad implemented this algorithm at one point - but Blogger has since switched to Basic over HTTPS and + but Blogger has since switched to Basic over HTTPS and TypePad has implemented it wrong, by never issuing a 401 challenge but instead requiring your client to telepathically know that their endpoint is expecting WSSE profile="UsernameToken".""" @@ -593,7 +639,7 @@ class WsseAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['Authorization'] = 'WSSE profile="UsernameToken"' + headers['authorization'] = 'WSSE profile="UsernameToken"' iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) cnonce = _cnonce() password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1]) @@ -629,7 +675,7 @@ class GoogleLoginAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth AUTH_SCHEME_CLASSES = { @@ -644,13 +690,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] class FileCache(object): """Uses a local directory as a store for cached files. - Not really safe to use if multiple threads or processes are going to + Not really safe to use if multiple threads or processes are going to be running on the same cache. """ def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior self.cache = cache self.safe = safe - if not os.path.exists(cache): + if not os.path.exists(cache): os.makedirs(self.cache) def get(self, key): @@ -660,7 +706,7 @@ class FileCache(object): f = file(cacheFullPath, "rb") retval = f.read() f.close() - except IOError, e: + except IOError: pass return retval @@ -688,34 +734,127 @@ class Credentials(object): def iter(self, domain): for (cdomain, name, password) in self.credentials: if cdomain == "" or domain == cdomain: - yield (name, password) + yield (name, password) class KeyCerts(Credentials): """Identical to Credentials except that name/password are mapped to key/cert.""" pass +class AllHosts(object): + pass class ProxyInfo(object): - """Collect information required to use a proxy.""" - def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None): - """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX - constants. For example: + """Collect information required to use a proxy.""" + bypass_hosts = () -p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000) - """ - self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: - def astuple(self): - return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, - self.proxy_user, self.proxy_pass) + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass - def isgood(self): - return socks and (self.proxy_host != None) and (self.proxy_port != None) + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) class HTTPConnectionWithTimeout(httplib.HTTPConnection): - """HTTPConnection subclass that supports timeouts""" + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): httplib.HTTPConnection.__init__(self, host, port, strict) @@ -725,27 +864,46 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): def connect(self): """Connect to the host and port specified in __init__.""" # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') msg = "getaddrinfo returns an empty list" - for res in socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM): + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: - if self.proxy_info and self.proxy_info.isgood(): + if use_proxy: self.sock = socks.socksocket(af, socktype, proto) - self.sock.setproxy(*self.proxy_info.astuple()) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) else: self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Different from httplib: support timeouts. if has_timeout(self.timeout): self.sock.settimeout(self.timeout) # End of difference from httplib. if self.debuglevel > 0: - print "connect: (%s, %s)" % (self.host, self.port) + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) - self.sock.connect(sa) + self.sock.connect((self.host, self.port) + sa[2:]) except socket.error, msg: if self.debuglevel > 0: - print 'connect fail:', (self.host, self.port) + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) if self.sock: self.sock.close() self.sock = None @@ -755,56 +913,265 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): raise socket.error, msg class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): - "This class allows communication via SSL." + """ + This class allows communication via SSL. + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=None, proxy_info=None): - httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file, - cert_file=cert_file, strict=strict) + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) self.timeout = timeout self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False def connect(self): "Connect to a host on a given (SSL) port." + msg = "getaddrinfo returns an empty list" if self.proxy_info and self.proxy_info.isgood(): - sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) - sock.setproxy(*self.proxy_info.astuple()) + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() else: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - if has_timeout(self.timeout): - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) - self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file) + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=5): + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass class Http(object): """An HTTP client that handles: -- all methods -- caching -- ETags -- compression, -- HTTPS -- Basic -- Digest -- WSSE -and more. + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. """ - def __init__(self, cache=None, timeout=None, proxy_info=None): - """The value of proxy_info is a ProxyInfo instance. + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. -If 'cache' is a string then it is used as a directory name -for a disk cache. Otherwise it must be an object that supports -the same interface as FileCache.""" + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + # Map domain name to an httplib connection self.connections = {} # The location of the cache, for now a directory # where cached responses are held. - if cache and isinstance(cache, str): + if cache and isinstance(cache, basestring): self.cache = FileCache(cache) else: self.cache = cache @@ -820,10 +1187,10 @@ the same interface as FileCache.""" # If set to False then no redirects are followed, even safe ones. self.follow_redirects = True - + # Which HTTP methods do we apply optimistic concurrency to, i.e. # which methods get an "if-match:" etag header added to them. - self.optimistic_concurrency_methods = ["PUT"] + self.optimistic_concurrency_methods = ["PUT", "PATCH"] # If 'follow_redirects' is True, and this is set to True then # all redirecs are followed, including unsafe ones. @@ -831,10 +1198,27 @@ the same interface as FileCache.""" self.ignore_etag = False - self.force_exception_to_status_code = False + self.force_exception_to_status_code = False self.timeout = timeout + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + def _auth_from_challenge(self, host, request_uri, headers, response, content): """A generator that creates Authorization objects that can be applied to requests. @@ -862,37 +1246,56 @@ the same interface as FileCache.""" self.authorizations = [] def _conn_request(self, conn, request_uri, method, body, headers): - for i in range(2): + for i in range(RETRIES): try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() conn.request(method, request_uri, body, headers) + except socket.timeout: + raise except socket.gaierror: conn.close() raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise except socket.error, e: - if not hasattr(e, 'errno'): # I don't know what this is so lets raise it if it happens + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused raise - elif e.errno == errno.ECONNREFUSED: # Connection refused - raise - # Just because the server closed the connection doesn't apparently mean - # that the server didn't send a response. - pass except httplib.HTTPException: # Just because the server closed the connection doesn't apparently mean # that the server didn't send a response. - pass + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue try: response = conn.getresponse() except (socket.error, httplib.HTTPException): - if i == 0: + if i < RETRIES-1: conn.close() conn.connect() continue else: + conn.close() raise else: content = "" if method == "HEAD": - response.close() + conn.close() else: content = response.read() response = Response(response) @@ -908,12 +1311,12 @@ the same interface as FileCache.""" auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auth = auths and sorted(auths)[0][1] or None - if auth: + if auth: auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers) - if auth: + if auth: if auth.response(response, body): auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers ) @@ -921,7 +1324,7 @@ the same interface as FileCache.""" if response.status == 401: for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): - authorization.request(method, request_uri, headers, body) + authorization.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) if response.status != 401: self.authorizations.append(authorization) @@ -944,26 +1347,31 @@ the same interface as FileCache.""" if response.status == 301 and method in ["GET", "HEAD"]: response['-x-permanent-redirect-url'] = response['location'] if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) if headers.has_key('if-none-match'): del headers['if-none-match'] if headers.has_key('if-modified-since'): del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] if response.has_key('location'): location = response['location'] old_response = copy.deepcopy(response) if not old_response.has_key('content-location'): - old_response['content-location'] = absolute_uri - redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) response.previous = old_response else: - raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) - elif response.status in [200, 203] and method == "GET": + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: # Don't cache 206's since we aren't going to handle byte range requests if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) return (response, content) @@ -978,24 +1386,25 @@ the same interface as FileCache.""" def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): """ Performs a single HTTP request. -The 'uri' is the URI of the HTTP resource and can begin -with either 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. -There is no restriction on the methods allowed. + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'body' is the entity body to be sent with the request. It is a string -object. + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. -Any extra headers that are to be sent with the request should be provided in the -'headers' dictionary. + The 'body' is the entity body to be sent with the request. It is a + string object. -The maximum number of redirect to follow before raising an -exception is 'redirections. The default is 5. + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. -The return value is a tuple of (response, content), the first -being and instance of the 'Response' class, the second being -a string that contains the response entity body. + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. """ try: if headers is None: @@ -1004,7 +1413,7 @@ a string that contains the response entity body. headers = self._normalize_headers(headers) if not headers.has_key('user-agent'): - headers['user-agent'] = "Python-httplib2/%s" % __version__ + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ uri = iri2uri(uri) @@ -1014,21 +1423,38 @@ a string that contains the response entity body. scheme = 'https' authority = domain_port[0] + proxy_info = self._get_proxy_info(scheme, authority) + conn_key = scheme+":"+authority if conn_key in self.connections: conn = self.connections[conn_key] else: if not connection_type: - connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout + connection_type = SCHEME_TO_CONNECTION[scheme] certs = list(self.certificates.iter(authority)) - if scheme == 'https' and certs: - conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], - cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) else: - conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info) + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) conn.set_debuglevel(debuglevel) - if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers: + if 'range' not in headers and 'accept-encoding' not in headers: headers['accept-encoding'] = 'gzip, deflate' info = email.Message.Message() @@ -1048,7 +1474,7 @@ a string that contains the response entity body. feedparser.feed(info) info = feedparser.close() feedparser._parse = None - except IndexError, ValueError: + except (IndexError, ValueError): self.cache.delete(cachekey) cachekey = None cached_value = None @@ -1071,13 +1497,15 @@ a string that contains the response entity body. for header in vary_headers: key = '-varied-%s' % header value = info[key] - if headers.get(header, '') != value: - cached_value = None - break + if headers.get(header, None) != value: + cached_value = None + break if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: if info.has_key('-x-permanent-redirect-url'): # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) response.previous = Response(info) response.previous.fromcache = True @@ -1085,13 +1513,13 @@ a string that contains the response entity body. # Determine our course of action: # Is the cached entry fresh or stale? # Has the client requested a non-cached response? - # - # There seems to be three possible answers: + # + # There seems to be three possible answers: # 1. [FRESH] Return the cache entry w/o doing a GET # 2. [STALE] Do the GET (but add in cache validators if available) # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request - entry_disposition = _entry_disposition(info, headers) - + entry_disposition = _entry_disposition(info, headers) + if entry_disposition == "FRESH": if not cached_value: info['status'] = '504' @@ -1113,7 +1541,7 @@ a string that contains the response entity body. if response.status == 304 and method == "GET": # Rewrite the cache entry with the new end-to-end headers - # Take all headers that are in response + # Take all headers that are in response # and overwrite their values in info. # unless they are hop-by-hop, or are listed in the connection header. @@ -1125,14 +1553,14 @@ a string that contains the response entity body. _updateCache(headers, merged_response, content, self.cache, cachekey) response = merged_response response.status = 200 - response.fromcache = True + response.fromcache = True elif response.status == 200: content = new_content else: self.cache.delete(cachekey) - content = new_content - else: + content = new_content + else: cc = _parse_cache_control(headers) if cc.has_key('only-if-cached'): info['status'] = '504' @@ -1146,34 +1574,47 @@ a string that contains the response entity body. response = e.response content = e.content response.status = 500 - response.reason = str(e) - elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): + response.reason = str(e) + elif isinstance(e, socket.timeout): content = "Request Timeout" - response = Response( { - "content-type": "text/plain", - "status": "408", - "content-length": len(content) - }) + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) response.reason = "Request Timeout" else: - content = str(e) - response = Response( { - "content-type": "text/plain", - "status": "400", - "content-length": len(content) - }) - response.reason = "Bad Request" + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" else: raise - + return (response, content) - + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + class Response(dict): """An object more like email.Message than httplib.HTTPResponse.""" - + """Is this response from our local cache""" fromcache = False @@ -1189,27 +1630,28 @@ class Response(dict): previous = None def __init__(self, info): - # info is either an email.Message or + # info is either an email.Message or # an httplib.HTTPResponse object. if isinstance(info, httplib.HTTPResponse): - for key, value in info.getheaders(): - self[key.lower()] = value + for key, value in info.getheaders(): + self[key.lower()] = value self.status = info.status self['status'] = str(self.status) self.reason = info.reason self.version = info.version elif isinstance(info, email.Message.Message): - for key, value in info.items(): - self[key] = value + for key, value in info.items(): + self[key.lower()] = value self.status = int(self['status']) else: - for key, value in info.iteritems(): - self[key] = value + for key, value in info.iteritems(): + self[key.lower()] = value self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) def __getattr__(self, name): if name == 'dict': - return self - else: - raise AttributeError, name + return self + else: + raise AttributeError, name diff --git a/libs/httplib2/cacerts.txt b/libs/httplib2/cacerts.txt new file mode 100644 index 00000000..d8a0027c --- /dev/null +++ b/libs/httplib2/cacerts.txt @@ -0,0 +1,739 @@ +# Certifcate Authority certificates for validating SSL connections. +# +# This file contains PEM format certificates generated from +# http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Netscape security libraries. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1994-2000 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +Verisign/RSA Secure Server CA +============================= + +-----BEGIN CERTIFICATE----- +MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD +VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0 +MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV +BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy +dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ +ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII +0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI +uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI +hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3 +YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc +1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA== +-----END CERTIFICATE----- + +Thawte Personal Basic CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDITCCAoqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFBlcnNvbmFsIEJhc2lj +IENBMSgwJgYJKoZIhvcNAQkBFhlwZXJzb25hbC1iYXNpY0B0aGF3dGUuY29tMB4X +DTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgcsxCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQZXJzb25hbCBCYXNpYyBD +QTEoMCYGCSqGSIb3DQEJARYZcGVyc29uYWwtYmFzaWNAdGhhd3RlLmNvbTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53 +dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK +wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7 +G1sY0b8jkyECAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQF +AAOBgQAt4plrsD16iddZopQBHyvdEktTwq1/qqcAXJFAVyVKOKqEcLnZgA+le1z7 +c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P +9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ== +-----END CERTIFICATE----- + +Thawte Personal Premium CA +========================== + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBzzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEjMCEGA1UEAxMaVGhhd3RlIFBlcnNvbmFsIFByZW1p +dW0gQ0ExKjAoBgkqhkiG9w0BCQEWG3BlcnNvbmFsLXByZW1pdW1AdGhhd3RlLmNv +bTAeFw05NjAxMDEwMDAwMDBaFw0yMDEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlDYXBlIFRvd24xGjAY +BgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9u +IFNlcnZpY2VzIERpdmlzaW9uMSMwIQYDVQQDExpUaGF3dGUgUGVyc29uYWwgUHJl +bWl1bSBDQTEqMCgGCSqGSIb3DQEJARYbcGVyc29uYWwtcHJlbWl1bUB0aGF3dGUu +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJZtn4B0TPuYwu8KHvE0Vs +Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI +Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD +ZicRFTuqW/KY3TZCstqIdQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH +b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh +KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGJ +-----END CERTIFICATE----- + +Thawte Personal Freemail CA +=========================== + +-----BEGIN CERTIFICATE----- +MIIDLTCCApagAwIBAgIBADANBgkqhkiG9w0BAQQFADCB0TELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEkMCIGA1UEAxMbVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIENBMSswKQYJKoZIhvcNAQkBFhxwZXJzb25hbC1mcmVlbWFpbEB0aGF3dGUu +Y29tMB4XDTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgdExCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEa +MBgGA1UEChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBG +cmVlbWFpbCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhh +d3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1GnX1LCUZFtx6UfY +DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E +rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq +uzgkCGqYx7Hao5iR/Xnb5VrEHLkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zAN +BgkqhkiG9w0BAQQFAAOBgQDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP +MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa +/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei +gQ== +-----END CERTIFICATE----- + +Thawte Server CA +================ + +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +Thawte Premium Server CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +Equifax Secure CA +================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05 +NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD +VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp +bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N +H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR +4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN +BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo +EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5 +FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx +lA== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh +YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7 +FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg +J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc +r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM +HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK +qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj +cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y +cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP +T8qAkbYp +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Equifax Secure Global eBusiness CA +================================== + +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 1 +============================= + +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 2 +============================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +Thawte Time Stamping CA +======================= + +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBizELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzAN +BgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAd +BgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNOTcwMTAxMDAwMDAwWhcN +MjAxMjMxMjM1OTU5WjCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4g +Q2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsG +A1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1l +c3RhbXBpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYrWHhhRYZT +6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa +Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL +8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEEBQADgYEAZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC +9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ +pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnRUEzGLhQOEkbdYATAUOK8oyvyxUBkZ +CayJSdM= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== + +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +Entrust.net Secure Server Certification Authority +================================================= + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +Go Daddy Certification Authority Root Certificate Bundle +======================================================== + +-----BEGIN CERTIFICATE----- +MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx +ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw +MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH +QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j +b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j +b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H +KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm +VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR +SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT +cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ +6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu +MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS +kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB +BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f +BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv +c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH +AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO +BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG +OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU +A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o +0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX +RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH +qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV +U+4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh +bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu +Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g +QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe +BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX +DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE +YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC +ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q +N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO +r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN +f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH +U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU +TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb +VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg +SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv +biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg +MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw +AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv +ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu +Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd +IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv +bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1 +QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O +WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf +SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +GeoTrust Global CA +================== + +-----BEGIN CERTIFICATE----- +MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 +aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw +WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE +AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m +OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu +T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c +JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR +Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz +PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm +aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM +TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g +LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO +BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv +dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB +AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL +NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W +b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S +-----END CERTIFICATE----- + diff --git a/libs/httplib2/iri2uri.py b/libs/httplib2/iri2uri.py index 70667edf..d88c91fd 100644 --- a/libs/httplib2/iri2uri.py +++ b/libs/httplib2/iri2uri.py @@ -16,7 +16,7 @@ import urlparse # Convert an IRI to a URI following the rules in RFC 3987 -# +# # The characters we need to enocde and escape are defined in the spec: # # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD @@ -28,28 +28,28 @@ import urlparse # / %xD0000-DFFFD / %xE1000-EFFFD escape_range = [ - (0xA0, 0xD7FF ), - (0xE000, 0xF8FF ), - (0xF900, 0xFDCF ), - (0xFDF0, 0xFFEF), - (0x10000, 0x1FFFD ), - (0x20000, 0x2FFFD ), - (0x30000, 0x3FFFD), - (0x40000, 0x4FFFD ), - (0x50000, 0x5FFFD ), - (0x60000, 0x6FFFD), - (0x70000, 0x7FFFD ), - (0x80000, 0x8FFFD ), - (0x90000, 0x9FFFD), - (0xA0000, 0xAFFFD ), - (0xB0000, 0xBFFFD ), - (0xC0000, 0xCFFFD), - (0xD0000, 0xDFFFD ), - (0xE1000, 0xEFFFD), - (0xF0000, 0xFFFFD ), - (0x100000, 0x10FFFD) + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), ] - + def encode(c): retval = c i = ord(c) @@ -63,19 +63,19 @@ def encode(c): def iri2uri(uri): - """Convert an IRI to a URI. Note that IRIs must be + """Convert an IRI to a URI. Note that IRIs must be passed in a unicode strings. That is, do not utf-8 encode - the IRI before passing it into the function.""" + the IRI before passing it into the function.""" if isinstance(uri ,unicode): (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) authority = authority.encode('idna') # For each character in 'ucschar' or 'iprivate' # 1. encode as utf-8 - # 2. then %-encode each octet of that utf-8 + # 2. then %-encode each octet of that utf-8 uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = "".join([encode(c) for c in uri]) return uri - + if __name__ == "__main__": import unittest @@ -83,7 +83,7 @@ if __name__ == "__main__": def test_uris(self): """Test that URIs are invariant under the transformation.""" - invariant = [ + invariant = [ u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"http://www.ietf.org/rfc/rfc2396.txt", u"ldap://[2001:db8::7]/c=GB?objectClass?one", @@ -94,7 +94,7 @@ if __name__ == "__main__": u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] for uri in invariant: self.assertEqual(uri, iri2uri(uri)) - + def test_iri(self): """ Test that the right type of escaping is done for each part of the URI.""" self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) @@ -107,4 +107,4 @@ if __name__ == "__main__": unittest.main() - + diff --git a/libs/httplib2/socks.py b/libs/httplib2/socks.py new file mode 100644 index 00000000..0991f4cf --- /dev/null +++ b/libs/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/libs/pyasn1/__init__.py b/libs/pyasn1/__init__.py index 7de39fe5..88aff79c 100644 --- a/libs/pyasn1/__init__.py +++ b/libs/pyasn1/__init__.py @@ -1 +1,8 @@ -majorVersionId = '1' +import sys + +# http://www.python.org/dev/peps/pep-0396/ +__version__ = '0.1.7' + +if sys.version_info[:2] < (2, 4): + raise RuntimeError('PyASN1 requires Python 2.4 or later') + diff --git a/libs/pyasn1/codec/__init__.py b/libs/pyasn1/codec/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/__init__.py +++ b/libs/pyasn1/codec/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/__init__.py b/libs/pyasn1/codec/ber/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/ber/__init__.py +++ b/libs/pyasn1/codec/ber/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/decoder.py b/libs/pyasn1/codec/ber/decoder.py index ae9311cb..be0cf490 100644 --- a/libs/pyasn1/codec/ber/decoder.py +++ b/libs/pyasn1/codec/ber/decoder.py @@ -1,21 +1,24 @@ # BER decoder from pyasn1.type import tag, base, univ, char, useful, tagmap from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import oct2int, octs2ints -from pyasn1 import error +from pyasn1.compat.octets import oct2int, octs2ints, isOctetsType +from pyasn1 import debug, error class AbstractDecoder: protoComponent = None def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Decoder not implemented for %s' % (tagSet,)) def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % (tagSet,)) class AbstractSimpleDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatSimple,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(value, tagSet) elif value is None: @@ -24,7 +27,10 @@ class AbstractSimpleDecoder(AbstractDecoder): return asn1Spec.clone(value) class AbstractConstructedDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatConstructed,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(tagSet) else: @@ -32,19 +38,34 @@ class AbstractConstructedDecoder(AbstractDecoder): class EndOfOctetsDecoder(AbstractSimpleDecoder): def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return eoo.endOfOctets, substrate[:length] + length, state, decodeFun, substrateFun): + return eoo.endOfOctets, substrate[length:] class ExplicitTagDecoder(AbstractSimpleDecoder): + protoComponent = univ.Any('') + tagFormats = (tag.tagFormatConstructed,) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return decodeFun(substrate[:length], asn1Spec, tagSet, length) + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) + head, tail = substrate[:length], substrate[length:] + value, _ = decodeFun(head, asn1Spec, tagSet, length) + return value, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) value, substrate = decodeFun(substrate, asn1Spec, tagSet, length) terminator, substrate = decodeFun(substrate) - if terminator == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(terminator) and \ + terminator == eoo.endOfOctets: return value, substrate else: raise error.PyAsn1Error('Missing end-of-octets terminator') @@ -71,79 +92,71 @@ class IntegerDecoder(AbstractSimpleDecoder): '\xfb': -5 } - def _valueFilter(self, value): - try: - return int(value) - except OverflowError: - return value - def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: - raise error.PyAsn1Error('Empty substrate') - if substrate in self.precomputedValues: - value = self.precomputedValues[substrate] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0), tail + if head in self.precomputedValues: + value = self.precomputedValues[head] else: - firstOctet = oct2int(substrate[0]) + firstOctet = oct2int(head[0]) if firstOctet & 0x80: value = -1 else: value = 0 - for octet in substrate: + for octet in head: value = value << 8 | oct2int(octet) - value = self._valueFilter(value) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class BooleanDecoder(IntegerDecoder): protoComponent = univ.Boolean(0) - def _valueFilter(self, value): - if value: - return 1 - else: - return 0 + def _createComponent(self, asn1Spec, tagSet, value=None): + return IntegerDecoder._createComponent(self, asn1Spec, tagSet, value and 1 or 0) class BitStringDecoder(AbstractSimpleDecoder): protoComponent = univ.BitString(()) + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - if not substrate: - raise error.PyAsn1Error('Missing initial octet') - trailingBits = oct2int(substrate[0]) + if not head: + raise error.PyAsn1Error('Empty substrate') + trailingBits = oct2int(head[0]) if trailingBits > 7: raise error.PyAsn1Error( 'Trailing bits overflow %s' % trailingBits ) - substrate = substrate[1:] - lsb = p = 0; l = len(substrate)-1; b = () + head = head[1:] + lsb = p = 0; l = len(head)-1; b = () while p <= l: if p == l: lsb = trailingBits j = 7 - o = oct2int(substrate[p]) + o = oct2int(head[p]) while j >= lsb: b = b + ((o>>j)&0x01,) j = j - 1 p = p + 1 - return self._createComponent(asn1Spec, tagSet, b), '' + return self._createComponent(asn1Spec, tagSet, b), tail r = self._createComponent(asn1Spec, tagSet, ()) - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -154,27 +167,29 @@ class BitStringDecoder(AbstractSimpleDecoder): class OctetStringDecoder(AbstractSimpleDecoder): protoComponent = univ.OctetString('') + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - return self._createComponent(asn1Spec, tagSet, substrate), '' + return self._createComponent(asn1Spec, tagSet, head), tail r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -186,93 +201,89 @@ class OctetStringDecoder(AbstractSimpleDecoder): class NullDecoder(AbstractSimpleDecoder): protoComponent = univ.Null('') def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if substrate: - raise error.PyAsn1Error('Unexpected substrate for Null') - return r, substrate + if head: + raise error.PyAsn1Error('Unexpected %d-octet substrate for Null' % length) + return r, tail class ObjectIdentifierDecoder(AbstractSimpleDecoder): protoComponent = univ.ObjectIdentifier(()) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - oid = (); index = 0 - # Get the first subid - subId = oct2int(substrate[index]) - oid = oid + divmod(subId, 40) - index = index + 1 - substrateLen = len(substrate) - + # Get the first subid + subId = oct2int(head[0]) + oid = divmod(subId, 40) + + index = 1 + substrateLen = len(head) while index < substrateLen: - subId = oct2int(substrate[index]) - if subId < 128: - oid = oid + (subId,) - index = index + 1 - else: + subId = oct2int(head[index]) + index = index + 1 + if subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in sub-ID OID + # encoding, tolerating it opens a vulnerability. + # See http://www.cosic.esat.kuleuven.be/publications/article-1432.pdf page 7 + raise error.PyAsn1Error('Invalid leading 0x80 in sub-OID') + elif subId > 128: # Construct subid from a number of octets nextSubId = subId subId = 0 - while nextSubId >= 128 and index < substrateLen: + while nextSubId >= 128: subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (oid,) + ) + nextSubId = oct2int(head[index]) index = index + 1 - nextSubId = oct2int(substrate[index]) - if index == substrateLen: - raise error.SubstrateUnderrunError( - 'Short substrate for OID %s' % oid - ) subId = (subId << 7) + nextSubId - oid = oid + (subId,) - index = index + 1 - return self._createComponent(asn1Spec, tagSet, oid), substrate[index:] + oid = oid + (subId,) + return self._createComponent(asn1Spec, tagSet, oid), tail class RealDecoder(AbstractSimpleDecoder): protoComponent = univ.Real() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] - if not length: - raise error.SubstrateUnderrunError('Short substrate for Real') - fo = oct2int(substrate[0]); substrate = substrate[1:] - if fo & 0x40: # infinite value - value = fo & 0x01 and '-inf' or 'inf' - elif fo & 0x80: # binary enoding - if fo & 0x11 == 0: - n = 1 - elif fo & 0x01: - n = 2 - elif fo & 0x02: - n = 3 - else: - n = oct2int(substrate[0]) - eo, substrate = substrate[:n], substrate[n:] - if not eo or not substrate: + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0.0), tail + fo = oct2int(head[0]); head = head[1:] + if fo & 0x80: # binary enoding + n = (fo & 0x03) + 1 + if n == 4: + n = oct2int(head[0]) + eo, head = head[:n], head[n:] + if not eo or not head: raise error.PyAsn1Error('Real exponent screwed') - e = 0 + e = oct2int(eo[0]) & 0x80 and -1 or 0 while eo: # exponent e <<= 8 e |= oct2int(eo[0]) eo = eo[1:] p = 0 - while substrate: # value + while head: # value p <<= 8 - p |= oct2int(substrate[0]) - substrate = substrate[1:] + p |= oct2int(head[0]) + head = head[1:] if fo & 0x40: # sign bit p = -p value = (p, 2, e) + elif fo & 0x40: # infinite value + value = fo & 0x01 and '-inf' or 'inf' elif fo & 0xc0 == 0: # character encoding try: if fo & 0x3 == 0x1: # NR1 - value = (int(substrate), 10, 0) + value = (int(head), 10, 0) elif fo & 0x3 == 0x2: # NR2 - value = float(substrate) + value = float(head) elif fo & 0x3 == 0x3: # NR3 - value = float(substrate) + value = float(head) else: raise error.SubstrateUnderrunError( 'Unknown NR (tag %s)' % fo @@ -281,13 +292,11 @@ class RealDecoder(AbstractSimpleDecoder): raise error.SubstrateUnderrunError( 'Bad character Real syntax' ) - elif fo & 0xc0 == 0x40: # special real value - pass else: raise error.SubstrateUnderrunError( 'Unknown encoding (tag %s)' % fo ) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class SequenceDecoder(AbstractConstructedDecoder): protoComponent = univ.Sequence() @@ -301,17 +310,15 @@ class SequenceDecoder(AbstractConstructedDecoder): return r.getComponentPositionNearType(t, idx) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) idx = 0 - if not decodeFun: - return r, substrate - while substrate: + if substrateFun: + return substrateFun(r, substrate, length) + while head: asn1Spec = self._getComponentTagMap(r, idx) - component, substrate = decodeFun( - substrate, asn1Spec - ) + component, head = decodeFun(head, asn1Spec) idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx ) @@ -319,18 +326,19 @@ class SequenceDecoder(AbstractConstructedDecoder): idx = idx + 1 r.setDefaultComponents() r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) idx = 0 while substrate: asn1Spec = self._getComponentTagMap(r, idx) - if not decodeFun: - return r, substrate component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx @@ -348,32 +356,31 @@ class SequenceDecoder(AbstractConstructedDecoder): class SequenceOfDecoder(AbstractConstructedDecoder): protoComponent = univ.SequenceOf() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun( - substrate, asn1Spec - ) + while head: + component, head = decodeFun(head, asn1Spec) r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 @@ -401,43 +408,68 @@ class SetOfDecoder(SequenceOfDecoder): class ChoiceDecoder(AbstractConstructedDecoder): protoComponent = univ.Choice() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) if r.getTagSet() == tagSet: # explicitly tagged Choice - component, substrate = decodeFun( - substrate, r.getComponentTagMap() + component, head = decodeFun( + head, r.getComponentTagMap() ) else: - component, substrate = decodeFun( - substrate, r.getComponentTagMap(), tagSet, length, state + component, head = decodeFun( + head, r.getComponentTagMap(), tagSet, length, state ) if isinstance(component, univ.Choice): effectiveTagSet = component.getEffectiveTagSet() else: effectiveTagSet = component.getTagSet() r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) + return r, tail + + def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, + length, state, decodeFun, substrateFun): + r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) + if r.getTagSet() == tagSet: # explicitly tagged Choice + component, substrate = decodeFun(substrate, r.getComponentTagMap()) + eooMarker, substrate = decodeFun(substrate) # eat up EOO marker + if not eoo.endOfOctets.isSameTypeWith(eooMarker) or \ + eooMarker != eoo.endOfOctets: + raise error.PyAsn1Error('No EOO seen before substrate ends') + else: + component, substrate= decodeFun( + substrate, r.getComponentTagMap(), tagSet, length, state + ) + if isinstance(component, univ.Choice): + effectiveTagSet = component.getEffectiveTagSet() + else: + effectiveTagSet = component.getTagSet() + r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) return r, substrate - indefLenValueDecoder = valueDecoder - class AnyDecoder(AbstractSimpleDecoder): protoComponent = univ.Any() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is None or \ asn1Spec is not None and tagSet != asn1Spec.getTagSet(): # untagged Any container, recover inner header substrate length = length + len(fullSubstrate) - len(substrate) substrate = fullSubstrate - substrate = substrate[:length] - return self._createComponent(asn1Spec, tagSet, value=substrate), '' + if substrateFun: + return substrateFun(self._createComponent(asn1Spec, tagSet), + substrate, length) + head, tail = substrate[:length], substrate[length:] + return self._createComponent(asn1Spec, tagSet, value=head), tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is not None and tagSet == asn1Spec.getTagSet(): # tagged Any type -- consume header substrate header = '' @@ -450,11 +482,12 @@ class AnyDecoder(AbstractSimpleDecoder): # Any components do not inherit initial tag asn1Spec = self.protoComponent - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -550,7 +583,10 @@ class Decoder: self.__tagSetCache = {} def __call__(self, substrate, asn1Spec=None, tagSet=None, - length=None, state=stDecodeTag, recursiveFlag=1): + length=None, state=stDecodeTag, recursiveFlag=1, + substrateFun=None): + if debug.logger & debug.flagDecoder: + debug.logger('decoder called at scope %s with state %d, working with up to %d octets of substrate: %s' % (debug.scope, state, len(substrate), debug.hexdump(substrate))) fullSubstrate = substrate while state != stStop: if state == stDecodeTag: @@ -559,6 +595,9 @@ class Decoder: raise error.SubstrateUnderrunError( 'Short octet stream on tag decoding' ) + if not isOctetsType(substrate) and \ + not isinstance(substrate, univ.OctetString): + raise error.PyAsn1Error('Bad octet stream type') firstOctet = substrate[0] substrate = substrate[1:] @@ -598,6 +637,7 @@ class Decoder: else: tagSet = lastTag + tagSet state = stDecodeLength + debug.logger and debug.logger & debug.flagDecoder and debug.logger('tag decoded into %r, decoding length' % tagSet) if state == stDecodeLength: # Decode length if not substrate: @@ -625,12 +665,13 @@ class Decoder: for char in lengthString: length = (length << 8) | oct2int(char) size = size + 1 - state = stGetValueDecoder substrate = substrate[size:] if length != -1 and len(substrate) < length: raise error.SubstrateUnderrunError( '%d-octet short' % (length - len(substrate)) ) + state = stGetValueDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value length decoded into %d, payload substrate is: %s' % (length, debug.hexdump(length == -1 and substrate or substrate[:length]))) if state == stGetValueDecoder: if asn1Spec is None: state = stGetValueDecoderByTag @@ -669,14 +710,27 @@ class Decoder: state = stDecodeValue else: state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by a built-in type, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(concreteDecoder is None and '?' or concreteDecoder.protoComponent.__class__.__name__) if state == stGetValueDecoderByAsn1Spec: if isinstance(asn1Spec, (dict, tagmap.TagMap)): if tagSet in asn1Spec: __chosenSpec = asn1Spec[tagSet] else: __chosenSpec = None + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('candidate ASN.1 spec is a map of:') + for t, v in asn1Spec.getPosMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + if asn1Spec.getNegMap(): + debug.logger('but neither of: ') + for i in asn1Spec.getNegMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + debug.logger('new candidate ASN.1 spec is %s, chosen by %r' % (__chosenSpec is None and '' or __chosenSpec.__class__.__name__, tagSet)) else: __chosenSpec = asn1Spec + debug.logger and debug.logger & debug.flagDecoder and debug.logger('candidate ASN.1 spec is %s' % asn1Spec.__class__.__name__) if __chosenSpec is not None and ( tagSet == __chosenSpec.getTagSet() or \ tagSet in __chosenSpec.getTagMap() @@ -687,9 +741,11 @@ class Decoder: __chosenSpec.typeId in self.__typeMap: # ambiguous type concreteDecoder = self.__typeMap[__chosenSpec.typeId] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen for an ambiguous type by type ID %s' % (__chosenSpec.typeId,)) elif baseTagSet in self.__tagMap: # base type or tagged subtype concreteDecoder = self.__tagMap[baseTagSet] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen by base %r' % (baseTagSet,)) else: concreteDecoder = None if concreteDecoder: @@ -700,8 +756,13 @@ class Decoder: elif tagSet == self.__endOfOctetsTagSet: concreteDecoder = self.__tagMap[tagSet] state = stDecodeValue + debug.logger and debug.logger & debug.flagDecoder and debug.logger('end-of-octets found') else: + concreteDecoder = None state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by ASN.1 spec, decoding %s' % (state == stDecodeValue and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(__chosenSpec is None and '?' or __chosenSpec.__class__.__name__) if state == stTryAsExplicitTag: if tagSet and \ tagSet[0][1] == tag.tagFormatConstructed and \ @@ -710,34 +771,35 @@ class Decoder: concreteDecoder = explicitTagDecoder state = stDecodeValue else: + concreteDecoder = None state = self.defaultErrorState + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as failure')) if state == stDumpRawValue: concreteDecoder = self.defaultRawDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding value' % concreteDecoder.__class__.__name__) state = stDecodeValue if state == stDecodeValue: - if recursiveFlag: - decodeFun = self - else: - decodeFun = None + if recursiveFlag == 0 and not substrateFun: # legacy + substrateFun = lambda a,b,c: (a,b[:c]) if length == -1: # indef length value, substrate = concreteDecoder.indefLenValueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) else: - value, _substrate = concreteDecoder.valueDecoder( + value, substrate = concreteDecoder.valueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) - if recursiveFlag: - substrate = substrate[length:] - else: - substrate = _substrate state = stStop + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s yields type %s, value:\n%s\n...remaining substrate is: %s' % (concreteDecoder.__class__.__name__, value.__class__.__name__, value.prettyPrint(), substrate and debug.hexdump(substrate) or '')) if state == stErrorCondition: raise error.PyAsn1Error( '%r not in asn1Spec: %r' % (tagSet, asn1Spec) ) + if debug.logger and debug.logger & debug.flagDecoder: + debug.scope.pop() + debug.logger('decoder left scope %s, call completed' % debug.scope) return value, substrate decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/ber/encoder.py b/libs/pyasn1/codec/ber/encoder.py index 2149b0ba..173949d0 100644 --- a/libs/pyasn1/codec/ber/encoder.py +++ b/libs/pyasn1/codec/ber/encoder.py @@ -1,8 +1,8 @@ # BER encoder from pyasn1.type import base, tag, univ, char, useful from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import int2oct, ints2octs, null, str2octs -from pyasn1 import error +from pyasn1.compat.octets import int2oct, oct2int, ints2octs, null, str2octs +from pyasn1 import debug, error class Error(Exception): pass @@ -78,9 +78,24 @@ class ExplicitlyTaggedItemEncoder(AbstractItemEncoder): explicitlyTaggedItemEncoder = ExplicitlyTaggedItemEncoder() +class BooleanEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 + _true = ints2octs((1,)) + _false = ints2octs((0,)) + def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + return value and self._true or self._false, 0 + class IntegerEncoder(AbstractItemEncoder): supportIndefLenMode = 0 + supportCompactZero = False def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + if value == 0: # shortcut for zero value + if self.supportCompactZero: + # this seems to be a correct way for encoding zeros + return null, 0 + else: + # this seems to be a widespread way for encoding zeros + return ints2octs((0,)), 0 octets = [] value = int(value) # to save on ops on asn1 type while 1: @@ -149,18 +164,15 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): index = 5 else: if len(oid) < 2: - raise error.PyAsn1Error('Short OID %s' % value) + raise error.PyAsn1Error('Short OID %s' % (value,)) # Build the first twos - index = 0 - subid = oid[index] * 40 - subid = subid + oid[index+1] - if subid < 0 or subid > 0xff: + if oid[0] > 6 or oid[1] > 39 or oid[0] == 6 and oid[1] > 15: raise error.PyAsn1Error( - 'Initial sub-ID overflow %s in OID %s' % (oid[index:], value) + 'Initial sub-ID overflow %s in OID %s' % (oid[:2], value) ) - octets = (subid,) - index = index + 2 + octets = (oid[0] * 40 + oid[1],) + index = 2 # Cycle through subids for subid in oid[index:]: @@ -184,6 +196,7 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): return ints2octs(octets), 0 class RealEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 def encodeValue(self, encodeFun, value, defMode, maxChunkSize): if value.isPlusInfinity(): return int2oct(0x40), 0 @@ -206,9 +219,11 @@ class RealEncoder(AbstractItemEncoder): m >>= 1 e += 1 eo = null - while e: + while e not in (0, -1): eo = int2oct(e&0xff) + eo e >>= 8 + if e == 0 and eo and oct2int(eo[0]) & 0x80: + eo = int2oct(0) + eo n = len(eo) if n > 0xff: raise error.PyAsn1Error('Real exponent overflow') @@ -268,7 +283,7 @@ class AnyEncoder(OctetStringEncoder): tagMap = { eoo.endOfOctets.tagSet: EndOfOctetsEncoder(), - univ.Boolean.tagSet: IntegerEncoder(), + univ.Boolean.tagSet: BooleanEncoder(), univ.Integer.tagSet: IntegerEncoder(), univ.BitString.tagSet: BitStringEncoder(), univ.OctetString.tagSet: OctetStringEncoder(), @@ -313,6 +328,7 @@ class Encoder: self.__typeMap = typeMap def __call__(self, value, defMode=1, maxChunkSize=0): + debug.logger & debug.flagEncoder and debug.logger('encoder called in %sdef mode, chunk size %s for type %s, value:\n%s' % (not defMode and 'in' or '', maxChunkSize, value.__class__.__name__, value.prettyPrint())) tagSet = value.getTagSet() if len(tagSet) > 1: concreteEncoder = explicitlyTaggedItemEncoder @@ -322,13 +338,16 @@ class Encoder: elif tagSet in self.__tagMap: concreteEncoder = self.__tagMap[tagSet] else: - baseTagSet = value.baseTagSet - if baseTagSet in self.__tagMap: - concreteEncoder = self.__tagMap[baseTagSet] + tagSet = value.baseTagSet + if tagSet in self.__tagMap: + concreteEncoder = self.__tagMap[tagSet] else: - raise Error('No encoder for %s' % value) - return concreteEncoder.encode( + raise Error('No encoder for %s' % (value,)) + debug.logger & debug.flagEncoder and debug.logger('using value codec %s chosen by %r' % (concreteEncoder.__class__.__name__, tagSet)) + substrate = concreteEncoder.encode( self, value, defMode, maxChunkSize ) + debug.logger & debug.flagEncoder and debug.logger('built %s octets of substrate: %s\nencoder completed' % (len(substrate), debug.hexdump(substrate))) + return substrate encode = Encoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/cer/__init__.py b/libs/pyasn1/codec/cer/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/cer/__init__.py +++ b/libs/pyasn1/codec/cer/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/cer/decoder.py b/libs/pyasn1/codec/cer/decoder.py index 71395d22..9fd37c13 100644 --- a/libs/pyasn1/codec/cer/decoder.py +++ b/libs/pyasn1/codec/cer/decoder.py @@ -7,22 +7,25 @@ from pyasn1 import error class BooleanDecoder(decoder.AbstractSimpleDecoder): protoComponent = univ.Boolean(0) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - byte = oct2int(substrate[0]) + byte = oct2int(head[0]) + # CER/DER specifies encoding of TRUE as 0xFF and FALSE as 0x0, while + # BER allows any non-zero value as TRUE; cf. sections 8.2.2. and 11.1 + # in http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf if byte == 0xff: value = 1 elif byte == 0x00: value = 0 else: raise error.PyAsn1Error('Boolean CER violation: %s' % byte) - return self._createComponent(asn1Spec, tagSet, value), substrate[1:] + return self._createComponent(asn1Spec, tagSet, value), tail tagMap = decoder.tagMap.copy() tagMap.update({ - univ.Boolean.tagSet: BooleanDecoder(), + univ.Boolean.tagSet: BooleanDecoder() }) typeMap = decoder.typeMap diff --git a/libs/pyasn1/codec/der/__init__.py b/libs/pyasn1/codec/der/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/der/__init__.py +++ b/libs/pyasn1/codec/der/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/der/decoder.py b/libs/pyasn1/codec/der/decoder.py index 0f5a24ca..604abec2 100644 --- a/libs/pyasn1/codec/der/decoder.py +++ b/libs/pyasn1/codec/der/decoder.py @@ -2,4 +2,8 @@ from pyasn1.type import univ from pyasn1.codec.cer import decoder -decode = decoder.Decoder(decoder.tagMap, decoder.typeMap) +tagMap = decoder.tagMap +typeMap = decoder.typeMap +Decoder = decoder.Decoder + +decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/compat/__init__.py b/libs/pyasn1/compat/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/compat/__init__.py +++ b/libs/pyasn1/compat/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/compat/octets.py b/libs/pyasn1/compat/octets.py index d0303eaa..f7f2a29b 100644 --- a/libs/pyasn1/compat/octets.py +++ b/libs/pyasn1/compat/octets.py @@ -8,6 +8,7 @@ if version_info[0] <= 2: octs2ints = lambda s: [ oct2int(x) for x in s ] str2octs = lambda x: x octs2str = lambda x: x + isOctetsType = lambda s: isinstance(s, str) else: ints2octs = bytes int2oct = lambda x: ints2octs((x,)) @@ -16,3 +17,4 @@ else: octs2ints = lambda s: [ x for x in s ] str2octs = lambda x: x.encode() octs2str = lambda x: x.decode() + isOctetsType = lambda s: isinstance(s, bytes) diff --git a/libs/pyasn1/debug.py b/libs/pyasn1/debug.py new file mode 100644 index 00000000..c27cb1d4 --- /dev/null +++ b/libs/pyasn1/debug.py @@ -0,0 +1,65 @@ +import sys +from pyasn1.compat.octets import octs2ints +from pyasn1 import error +from pyasn1 import __version__ + +flagNone = 0x0000 +flagEncoder = 0x0001 +flagDecoder = 0x0002 +flagAll = 0xffff + +flagMap = { + 'encoder': flagEncoder, + 'decoder': flagDecoder, + 'all': flagAll + } + +class Debug: + defaultPrinter = sys.stderr.write + def __init__(self, *flags): + self._flags = flagNone + self._printer = self.defaultPrinter + self('running pyasn1 version %s' % __version__) + for f in flags: + if f not in flagMap: + raise error.PyAsn1Error('bad debug flag %s' % (f,)) + self._flags = self._flags | flagMap[f] + self('debug category \'%s\' enabled' % f) + + def __str__(self): + return 'logger %s, flags %x' % (self._printer, self._flags) + + def __call__(self, msg): + self._printer('DBG: %s\n' % msg) + + def __and__(self, flag): + return self._flags & flag + + def __rand__(self, flag): + return flag & self._flags + +logger = 0 + +def setLogger(l): + global logger + logger = l + +def hexdump(octets): + return ' '.join( + [ '%s%.2X' % (n%16 == 0 and ('\n%.5d: ' % n) or '', x) + for n,x in zip(range(len(octets)), octs2ints(octets)) ] + ) + +class Scope: + def __init__(self): + self._list = [] + + def __str__(self): return '.'.join(self._list) + + def push(self, token): + self._list.append(token) + + def pop(self): + return self._list.pop() + +scope = Scope() diff --git a/libs/pyasn1/type/__init__.py b/libs/pyasn1/type/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/type/__init__.py +++ b/libs/pyasn1/type/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/type/base.py b/libs/pyasn1/type/base.py index db31671e..40873719 100644 --- a/libs/pyasn1/type/base.py +++ b/libs/pyasn1/type/base.py @@ -120,7 +120,12 @@ class AbstractSimpleAsn1Item(Asn1ItemBase): def prettyIn(self, value): return value def prettyOut(self, value): return str(value) - def prettyPrint(self, scope=0): return self.prettyOut(self._value) + def prettyPrint(self, scope=0): + if self._value is noValue: + return '' + else: + return self.prettyOut(self._value) + # XXX Compatibility stub def prettyPrinter(self, scope=0): return self.prettyPrint(scope) diff --git a/libs/pyasn1/type/namedtype.py b/libs/pyasn1/type/namedtype.py index aa9c5678..48967a5f 100644 --- a/libs/pyasn1/type/namedtype.py +++ b/libs/pyasn1/type/namedtype.py @@ -60,12 +60,12 @@ class NamedTypes: tagMap = self.__namedTypes[idx].getType().getTagMap() for t in tagMap.getPosMap(): if t in self.__tagToPosIdx: - raise error.PyAsn1Error('Duplicate type %s' % t) + raise error.PyAsn1Error('Duplicate type %s' % (t,)) self.__tagToPosIdx[t] = idx try: return self.__tagToPosIdx[tagSet] except KeyError: - raise error.PyAsn1Error('Type %s not found' % tagSet) + raise error.PyAsn1Error('Type %s not found' % (tagSet,)) def getNameByPosition(self, idx): try: @@ -79,12 +79,12 @@ class NamedTypes: idx = idx - 1 n = self.__namedTypes[idx].getName() if n in self.__nameToPosIdx: - raise error.PyAsn1Error('Duplicate name %s' % n) + raise error.PyAsn1Error('Duplicate name %s' % (n,)) self.__nameToPosIdx[n] = idx try: return self.__nameToPosIdx[name] except KeyError: - raise error.PyAsn1Error('Name %s not found' % name) + raise error.PyAsn1Error('Name %s not found' % (name,)) def __buildAmbigiousTagMap(self): ambigiousTypes = () diff --git a/libs/pyasn1/type/namedval.py b/libs/pyasn1/type/namedval.py index 815e2d42..d0fea7cc 100644 --- a/libs/pyasn1/type/namedval.py +++ b/libs/pyasn1/type/namedval.py @@ -15,10 +15,10 @@ class NamedValues: name = namedValue val = automaticVal if name in self.nameToValIdx: - raise error.PyAsn1Error('Duplicate name %s' % name) + raise error.PyAsn1Error('Duplicate name %s' % (name,)) self.nameToValIdx[name] = val if val in self.valToNameIdx: - raise error.PyAsn1Error('Duplicate value %s' % name) + raise error.PyAsn1Error('Duplicate value %s=%s' % (name, val)) self.valToNameIdx[val] = name self.namedValues = self.namedValues + ((name, val),) automaticVal = automaticVal + 1 diff --git a/libs/pyasn1/type/tag.py b/libs/pyasn1/type/tag.py index 0cf67ebd..1144907f 100644 --- a/libs/pyasn1/type/tag.py +++ b/libs/pyasn1/type/tag.py @@ -18,7 +18,7 @@ class Tag: def __init__(self, tagClass, tagFormat, tagId): if tagId < 0: raise error.PyAsn1Error( - 'Negative tag ID (%s) not allowed' % tagId + 'Negative tag ID (%s) not allowed' % (tagId,) ) self.__tag = (tagClass, tagFormat, tagId) self.uniq = (tagClass, tagId) diff --git a/libs/pyasn1/type/tagmap.py b/libs/pyasn1/type/tagmap.py index 53e1791a..7cec3a10 100644 --- a/libs/pyasn1/type/tagmap.py +++ b/libs/pyasn1/type/tagmap.py @@ -28,7 +28,7 @@ class TagMap: def clone(self, parentType, tagMap, uniq=False): if self.__defType is not None and tagMap.getDef() is not None: - raise error.PyAsn1Error('Duplicate default value at %s' % self) + raise error.PyAsn1Error('Duplicate default value at %s' % (self,)) if tagMap.getDef() is not None: defType = tagMap.getDef() else: @@ -37,7 +37,7 @@ class TagMap: posMap = self.__posMap.copy() for k in tagMap.getPosMap(): if uniq and k in posMap: - raise error.PyAsn1Error('Duplicate positive key %s' % k) + raise error.PyAsn1Error('Duplicate positive key %s' % (k,)) posMap[k] = parentType negMap = self.__negMap.copy() diff --git a/libs/pyasn1/type/univ.py b/libs/pyasn1/type/univ.py index cb4f49b7..9cd16f8a 100644 --- a/libs/pyasn1/type/univ.py +++ b/libs/pyasn1/type/univ.py @@ -69,13 +69,18 @@ class Integer(base.AbstractSimpleAsn1Item): def prettyIn(self, value): if not isinstance(value, str): - return int(value) + try: + return int(value) + except: + raise error.PyAsn1Error( + 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) + ) r = self.__namedValues.getValue(value) if r is not None: return r try: return int(value) - except ValueError: + except: raise error.PyAsn1Error( 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) ) @@ -224,14 +229,14 @@ class BitString(base.AbstractSimpleAsn1Item): return tuple(r) else: raise error.PyAsn1Error( - 'Bad BIT STRING value notation %s' % value + 'Bad BIT STRING value notation %s' % (value,) ) else: for i in value.split(','): j = self.__namedValues.getValue(i) if j is None: raise error.PyAsn1Error( - 'Unknown bit identifier \'%s\'' % i + 'Unknown bit identifier \'%s\'' % (i,) ) if j >= len(r): r.extend([0]*(j-len(r)+1)) @@ -528,7 +533,7 @@ class Real(base.AbstractSimpleAsn1Item): ) if value[1] not in (2, 10): raise error.PyAsn1Error( - 'Prohibited base for Real value: %s' % value[1] + 'Prohibited base for Real value: %s' % (value[1],) ) if value[1] == 10: value = self.__normalizeBase10(value) @@ -648,7 +653,7 @@ class SetOf(base.AbstractConstructedAsn1Item): def _verifyComponent(self, idx, value): if self._componentType is not None and \ not self._componentType.isSuperTypeOf(value): - raise error.PyAsn1Error('Component type error %s' % value) + raise error.PyAsn1Error('Component type error %s' % (value,)) def getComponentByPosition(self, idx): return self._componentValues[idx] def setComponentByPosition(self, idx, value=None, verifyConstraints=True): @@ -924,9 +929,9 @@ class Choice(Set): return self._componentValues[self._currentIdx] >= other return NotImplemented if sys.version_info[0] <= 2: - def __nonzero__(self, other): return bool(self._componentValues) + def __nonzero__(self): return bool(self._componentValues) else: - def __bool__(self, other): return bool(self._componentValues) + def __bool__(self): return bool(self._componentValues) def __len__(self): return self._currentIdx is not None and 1 or 0 diff --git a/libs/pyutil/_version.py b/libs/pyutil/_version.py index 617d2205..376b2b9b 100644 --- a/libs/pyutil/_version.py +++ b/libs/pyutil/_version.py @@ -6,7 +6,7 @@ # pyutil.version_class for a description of what the different fields mean. __pkgname__ = "pyutil" -verstr = "1.9.3" +verstr = "1.9.7" try: from pyutil.version_class import Version as pyutil_Version __version__ = pyutil_Version(verstr) diff --git a/libs/pyutil/benchutil.py b/libs/pyutil/benchutil.py index 3e773a63..6c286346 100644 --- a/libs/pyutil/benchutil.py +++ b/libs/pyutil/benchutil.py @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -21,10 +21,10 @@ the second, e.g.: >>> rep_bench(fib, 25, UNITS_PER_SECOND=1000) best: 1.968e+00, 3th-best: 1.987e+00, mean: 2.118e+00, 3th-worst: 2.175e+00, worst: 2.503e+00 (of 10) -The output is reporting the number of milliseconds that executing the function -took, divided by N, from ten different invocations of fib(). It reports the -best, worst, M-th best, M-th worst, and mean, where "M" is the natural log of -the number of invocations (in this case 10). +The output is reporting the number of milliseconds that executing the +function took, divided by N, from ten different invocations of +fib(). It reports the best, worst, M-th best, M-th worst, and mean, +where "M" is 1/4 of the number of invocations (in this case 10). 2. Now run it with different values of N and look for patterns: @@ -74,10 +74,12 @@ and the main function is to make them be methods of the same object, e.g.: 4. Things to fix: - a. I used to have it hooked up to use the "hotshot" profiler on the code being - measured. I recently tried to change it to use the newer cProfile profiler - instead, but I don't understand the interface to cProfiler so it just gives an - exception if you pass profile=True. Please fix this and send me a patch. + a. I used to have it hooked up to use the "hotshot" profiler on the + code being measured. I recently tried to change it to use the newer + cProfile profiler instead, but I don't understand the interface to + cProfiler so it just gives an exception if you pass + profile=True. Please fix this and send me a patch. xxx change it to + statprof b. Wouldn't it be great if this script emitted results in a json format that was understood by a tool to make pretty interactive explorable graphs? The @@ -122,7 +124,7 @@ def mult(a, b): except TypeError: return to_decimal(a) * to_decimal(b) -def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): +def rep_bench(func, n, runtime=1.0, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME (wall-clock time) has already elapsed (unless MAXTIME is None). @@ -130,33 +132,43 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p @param quiet Don't print anything--just return the results dict. """ assert isinstance(n, int), (n, type(n)) + global worstemptymeasure + emsta = clock() + do_nothing(2**32) + emstop = clock() + empty = emstop - emsta + if empty > worstemptymeasure: + worstemptymeasure = empty + if (worstemptymeasure*2) >= runtime: + raise BadMeasure("Apparently simply invoking an empty Python function can take as long as %0.10f seconds, and we were running iterations for only about %0.10f seconds. So the measurement of the runtime of the code under benchmark is not reliable. Please pass a higher number for the 'runtime' argument to bench_it().") + startwallclocktime = time.time() - tls = [] # elapsed time in seconds + tls = [] # (elapsed time per iter in seconds, iters) bmes = [] while ((len(tls) < MAXREPS) or (MAXREPS is None)) and ((MAXTIME is None) or ((time.time() - startwallclocktime) < MAXTIME)): if initfunc: initfunc(n) try: - tl = bench_it(func, n, profile=profile, profresults=profresults) + tl, iters = bench_it(func, n, runtime=runtime, profile=profile, profresults=profresults) except BadMeasure, bme: bmes.append(bme) else: - tls.append(tl) + tls.append((tl, iters)) if len(tls) == 0: raise Exception("Couldn't get any measurements within time limits or number-of-attempts limits. Maybe something is wrong with your clock? %s" % (bmes,)) - sumtls = reduce(operator.__add__, tls) + sumtls = sum([tl for (tl, iters) in tls]) mean = sumtls / len(tls) tls.sort() - worst = tls[-1] - best = tls[0] - _assert(best > worstemptymeasure*MARGINOFERROR, "%s(n=%s) took %0.10f seconds, but we cannot measure times much less than about %0.10f seconds. Try a more time-consuming variant (such as higher n)." % (func, n, best, worstemptymeasure*MARGINOFERROR,)) + worst = tls[-1][0] + best = tls[0][0] + m = len(tls)/4 if m > 0: - mthbest = tls[m-1] - mthworst = tls[-m] + mthbest = tls[m-1][0] + mthworst = tls[-m][0] else: - mthbest = tls[0] - mthworst = tls[-1] + mthbest = tls[0][0] + mthworst = tls[-1][0] # The +/-0 index is the best/worst, the +/-1 index is the 2nd-best/worst, # etc, so we use mp1 to name it. @@ -196,26 +208,22 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=1.0, profile=False, profresults="pyutil-benchutil.prof"): if profile: - st = clock() - cProfile.run('func(n)', profresults) - sto = clock() + raise NotImplementedException() else: + iters = 0 st = clock() - func(n) + deadline = st + runtime sto = clock() + while sto < deadline: + func(n) + iters += 1 + sto = clock() timeelapsed = sto - st - if timeelapsed <= 0: - raise BadMeasure(timeelapsed) - global worstemptymeasure - emsta = clock() - do_nothing(2**32) - emstop = clock() - empty = emstop - emsta - if empty > worstemptymeasure: - worstemptymeasure = empty - return timeelapsed + if (timeelapsed <= 0) or (iters == 0): + raise BadMeasure((timeelapsed, iters)) + return (timeelapsed / iters, iters) def bench(func, initfunc=None, TOPXP=21, MAXREPS=5, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", outputjson=False, jsonresultsfname="pyutil-benchutil-results.json", UNITS_PER_SECOND=1): BSIZES = [] diff --git a/libs/pyutil/benchutil.py~ b/libs/pyutil/benchutil.py~ index 3ec323eb..a33111e8 100644 --- a/libs/pyutil/benchutil.py~ +++ b/libs/pyutil/benchutil.py~ @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -104,6 +104,24 @@ def makeg(func): func() return blah +def to_decimal(x): + """ + See if D(x) returns something. If instead it raises TypeError, x must have been a float, so convert it to Decimal by way of string. (In Python >= 2.7, D(x) does this automatically. + """ + try: + return D(x) + except TypeError: + return D("%0.54f" % (x,)) + +def mult(a, b): + """ + If we get TypeError from * (possibly because one is float and the other is Decimal), then promote them both to Decimal. + """ + try: + return a * b + except TypeError: + return to_decimal(a) * to_decimal(b) + def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME @@ -144,12 +162,12 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p # etc, so we use mp1 to name it. mp1 = m+1 res = { - 'worst': (worst*UNITS_PER_SECOND)/n, - 'best': (best*UNITS_PER_SECOND)/n, + 'worst': mult(worst, UNITS_PER_SECOND)/n, + 'best': mult(best, UNITS_PER_SECOND)/n, 'mp1': mp1, - 'mth-best': (mthbest*UNITS_PER_SECOND)/n, - 'mth-worst': (mthworst*UNITS_PER_SECOND)/n, - 'mean': (mean*UNITS_PER_SECOND)/n, + 'mth-best': mult(mthbest, UNITS_PER_SECOND)/n, + 'mth-worst': mult(mthworst, UNITS_PER_SECOND)/n, + 'mean': mult(mean, UNITS_PER_SECOND)/n, 'num': len(tls), } @@ -178,7 +196,10 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=0.1, profile=False, profresults="pyutil-benchutil.prof"): + """ + runtime is how many seconds to + """ if profile: st = clock() cProfile.run('func(n)', profresults) diff --git a/libs/pyutil/data/wordlist.txt b/libs/pyutil/data/wordlist.txt new file mode 100644 index 00000000..e1048b99 --- /dev/null +++ b/libs/pyutil/data/wordlist.txt @@ -0,0 +1,7248 @@ +fawn +yellow +four +prefix +payoff +scold +outwit +lore +lord +swivel +deli +pigment +foul +fur +disturb +prize +broiler +wooden +satchel +crotch +fritter +charter +tired +miller +bacon +second +tether +ruthless +thunder +fossil +succumb +cull +specialist +hero +avert +herb +splinter +here +herd +china +dogwood +cult +shriek +chink +pancreas +robin +neurologist +climber +diplomat +golden +gridiron +lengthen +summons +remnant +stern +unit +spoke +exhort +statesmanship +music +bedrock +passport +strike +teaspoon +relay +relax +hurt +meteorologist +glass +hurl +hole +hold +unpack +sweeten +blade +locker +locket +plunger +wand +wane +unjust +household +digit +malign +caution +want +rayon +hog +hoe +travel +copious +cutback +revisit +how +hot +hop +cheetah +diagram +possum +modest +antonym +pigtail +revolt +alias +decoy +wing +squint +wine +feedback +misdemeanor +kickoff +foodstuff +butcher +dreamer +fir +bowlder +fix +fib +fig +fin +undercut +enrich +slate +interrupt +sixteen +silver +scholar +thyme +seamstress +debut +arrow +debug +volcano +burial +whim +concord +knockout +garment +allah +spider +crocus +turnip +yiddish +fortnight +allay +whir +whip +diction +smirk +mason +semiconductor +re +adapt +outburst +knit +scruff +silicon +miaow +thumbtack +shopper +wasp +wash +instruct +rhododendron +tango +master +architect +bitter +listen +wisdom +swish +sulphur +crawl +trek +peril +outlay +coward +tree +shower +pneumonia +sheen +acclaim +entail +girder +runner +spectrum +headland +increment +quay +dozen +kidnap +gripe +hum +greenback +tipi +matriarch +stirrup +object +toil +microsecond +mouth +addict +letter +fluster +drought +thriller +expound +singer +upend +grove +professor +camp +detriment +nineteenth +scream +marvel +bomb +reactor +heckler +ulcer +caper +layout +menu +bust +cougar +bush +bliss +rich +mend +rice +plate +pocket +cushion +fetish +relish +jaguar +boarder +pretzel +patch +hasten +respond +fair +heirloom +radium +radius +result +fail +crouch +clef +best +irk +yogurt +ire +wage +extend +vestment +souvenir +extent +wheelbarrow +carbon +debt +roller +accident +trickster +veer +disdain +cup +logic +genus +rehash +gopher +canyon +bewilder +chrome +onomatopoeia +advert +grapefruit +stadium +jackass +counterattack +life +retrospect +spit +worker +wish +lift +toboggan +chile +child +chili +spin +wildcat +dissect +employ +calcium +delicatessen +locksmith +letdown +player +elicit +eighteen +violin +doorman +specter +hone +toaster +honk +rebellion +split +bid +european +typhoid +boiler +ownership +supper +tuna +tune +furlough +noblewoman +unhook +abound +bellow +beset +plight +brandish +previous +ham +hag +hay +prison +falter +east +hat +quirk +birth +shadow +gangplank +remind +pavement +battlefield +attorney +right +old +creek +crowd +creed +crown +glove +billboard +creep +chorus +okra +bottom +circumvent +inhuman +fox +foe +fog +binder +yoke +slither +recollect +despair +rebut +eightieth +sob +sod +overshadow +honeymoon +overgrow +sop +sow +wrap +fabric +panorama +support +tame +avail +width +hothead +call +overhand +overhang +telegraph +offer +thesaurus +beech +squalid +safeguard +otter +duel +misinform +paprika +vanguard +pest +duet +proud +tournament +proven +exist +quintuplet +dealer +leer +floor +glacier +actor +flood +role +entomologist +sunset +smell +leek +intend +glutton +ointment +asterisk +taurus +intent +cleaver +entrust +windscreen +puss +lowdown +time +push +gown +chain +viaduct +skate +chair +midst +millisecond +ballet +uneven +vex +crater +oversight +jerk +ameba +embark +flora +mourn +knapsack +southpaw +exact +epic +judaism +tear +teas +teat +crustacean +subway +team +skewer +prevent +meadow +gremlin +attic +sigh +milligram +heavyweight +crescent +playpen +crackpot +melt +current +boost +abscond +gnaw +splice +address +brilliant +endow +queue +influx +love +radish +prefer +piranha +fake +instal +forefront +sky +homesick +turret +wicker +wicket +scope +prosecutor +wicked +afford +refrain +visual +appendix +behalf +mascot +lumberjack +pretend +descriptor +dispossess +stole +winter +savor +sputter +meddler +slush +spot +textual +date +suck +dove +pulley +stress +conscious +bluster +wheelchair +quadrant +mango +so +skirmish +truce +drunken +archeologist +footstep +yearn +jig +disconnect +thumb +accordion +nearsighted +councillor +hubbub +suspicion +thump +apron +civilian +insomnia +nation +amulet +twilight +ketchup +handiwork +revert +fisherman +quarter +quartet +receipt +fireproof +breakthrough +sponsor +troll +naked +canvas +onrush +trauma +formula +dumbfound +million +envelop +vicious +disrespect +mime +plea +byte +workmanship +punk +wrong +ostrich +punt +footwear +neglect +gunshot +potter +one +reopen +chide +conifer +vote +paleontologist +languish +boulevard +wrath +convent +bite +extortion +shiver +draft +cite +starfish +shawl +artifact +snatch +antic +boyfriend +iceberg +rival +stammer +counselor +janitor +prospect +sac +greyhound +argument +alley +sad +say +borough +saw +handicraft +tulip +general +knead +zoo +note +take +destroy +printer +buffer +squalor +compress +buffet +crochet +knee +byway +lawn +enamel +blockhead +sale +cocoanut +wind +axe +salt +cobra +homespun +lotus +friction +bright +slot +slow +slop +unkind +gourd +transact +cloak +debunk +slog +hockey +slob +robe +clank +dissimilar +psychiatrist +clang +outlet +prime +artist +saliva +borrow +soloist +carrion +handcuff +primp +landlord +tortilla +where +xmas +vision +gout +gangster +cheesecloth +diver +bugler +mutton +plummet +bootleg +teacup +bureau +mope +vender +jumper +spars +screen +dome +supermarket +adept +jovial +spare +spark +quack +oust +fit +madcap +mane +flipper +backpack +twin +boar +supervisor +extinct +twig +boat +companionship +stretch +west +breath +reflex +gist +thousand +photon +cloudburst +turtleneck +former +jute +scarlet +straighten +spotlight +girth +brow +canon +dubious +monk +blab +fame +spunk +breakdown +hideaway +deft +barber +disown +booster +driftwood +veal +pewter +dimension +scholarship +summer +manifold +poach +disconcert +slime +rest +invalid +alarmist +mandolin +instrument +overthrow +stopwatch +haystack +joyful +sportsmanship +rejoin +dart +dark +brazier +snarl +traffic +cranium +vacuum +world +snare +dare +clan +clam +stranger +shutter +glamor +clay +claw +inter +kennel +clap +auditorium +obstruct +grub +potion +lobster +racial +endeavor +tote +tube +moslem +tuba +nook +exit +refer +zest +ration +leadership +standpoint +stone +ace +slender +meal +tumor +neighbor +act +mean +invert +braggart +homeless +wade +hypnotist +racquet +hew +burglar +her +gleam +glean +mindless +harpsichord +italic +hem +hen +defrost +epilog +pull +regimen +darken +wafer +rage +hooray +tripe +ruse +flirt +reprimand +whiz +torso +pulp +rust +ohm +gong +ad +fright +certain +epaulet +catchup +hoodlum +ay +ax +tranquil +jargon +slobber +cream +yoga +collector +abolish +tight +backgammon +congress +annex +slant +midget +brotherhood +slang +rostrum +neuter +thorn +groom +mask +kilogram +mash +mimic +mast +mass +ringworm +waiver +retch +gingham +influenza +consider +neigh +upkeep +taxicab +tinsel +to +tail +smile +norm +debit +baton +candid +salesperson +cobalt +strand +laud +pedant +sand +adjust +small +mammal +peon +ninetieth +plaid +past +burnish +gossip +canvass +healer +hick +offbeat +clock +section +succinct +method +contrast +full +hash +lobbyist +saleswoman +dramatist +backlash +brutal +prior +hamster +skyrocket +social +action +welder +raze +depart +vie +sherbet +regiment +captor +coercion +entrap +select +casket +enliven +petroleum +maltreat +pearl +sitter +morn +ballad +more +teen +teem +door +tester +signpost +nomad +doom +cunning +fatal +malt +chisel +patriarch +knocker +midstream +mall +learn +grope +male +stewardess +prompt +taunt +gallop +scab +accept +autumn +gallon +scar +rustler +condemn +huge +speedboat +fruition +cling +clink +plant +anoint +blotter +variant +unsound +plane +waver +flutter +pucker +wrench +trellis +patio +pant +instep +trade +paper +pang +brim +mislay +hearsay +buttercup +epoch +coarsen +bypass +motley +sucker +gadget +consign +imperil +skipper +harrow +nugget +fount +found +lantern +status +eyelash +clockwork +scribe +penicillin +lipstick +research +highway +bungler +belief +porcelain +bedlam +cockpit +loafer +suntan +acorn +riser +reproach +prefab +drivel +sicken +bumper +testament +clump +major +purport +limerick +number +feeder +slipper +footprint +florist +glitter +guess +guest +jet +swipe +vocalist +saint +gnash +relationship +tightwad +typhoon +mural +consult +grace +frock +getaway +vocal +video +defect +waft +pedestrian +graffito +caress +blond +gasket +sell +ballerina +ragamuffin +tarnish +spaghetti +self +trowel +poplar +brace +bobbin +kneecap +hypochondriac +blackboard +nasal +twine +raucous +virus +plan +wive +foyer +oyster +unequal +arson +covet +cover +barren +barrel +bulletin +chowder +golf +cruiser +affix +session +freight +impact +condor +writer +peculiar +condom +factor +downpour +dandelion +streamer +resent +actress +compass +banner +tumult +sojourn +caramel +enema +weaver +river +outlaw +prospectus +manger +set +creator +overwhelm +jade +sex +see +sea +contour +analog +project +urchin +fission +crossword +pickup +crosswalk +kneel +candor +mildew +hardship +disallow +incident +dividend +pagan +scatterbrain +lass +last +thou +opal +feminist +amoeba +lash +whole +load +loaf +electrician +pendulum +bell +loam +loan +hollow +scallop +church +psychoanalyst +underlay +napalm +airfield +devil +filth +imbed +proprietor +veneer +firm +sweetheart +champion +fire +infect +upstart +fund +deport +hostess +straight +budget +error +outskirt +real +pound +moth +vow +chasm +vanish +chase +starlight +seek +shorten +wasteland +specimen +commune +snail +teeter +cigar +epithet +alert +opinion +stack +recent +expend +clime +person +sixtieth +crayfish +telegram +aroma +belabor +amp +demerit +sandal +goblet +chest +eager +horseradish +homeland +wrongdoer +input +limp +cordon +format +bureaucrat +quest +cataclysm +blackjack +falcon +abduct +flannel +spine +consensus +crescendo +spring +beckon +palm +pall +sight +curious +sprint +battalion +pale +gruel +benefit +religion +be +odor +agreement +carol +by +scepter +coexist +hatchet +sacrament +ambush +biennial +repair +contributor +next +span +sock +submit +custom +spay +suit +spar +spat +blueprint +perplex +poster +lint +slump +pastor +overbear +link +atom +line +up +slander +foist +hornet +insignia +genial +aerial +nationalist +haunt +char +chap +chat +parsec +breather +phantom +paradox +tuft +uranium +scrape +parakeet +swirl +freighter +tart +tedium +scapegoat +trouser +scrub +gardenia +hackney +lane +land +fighter +algorithm +scotch +age +feud +summit +walker +fresh +crowbar +rescuer +hello +essay +code +partial +serviceman +scratch +broaden +totem +soften +leggin +renown +prim +flashback +young +send +moor +tremor +garret +armament +garden +quadruplet +llama +precinct +wipe +magic +harbor +eve +anxious +race +rack +mishap +crook +croon +odd +ode +victor +index +yock +sauerkraut +apparatus +indian +proffer +bird +inspect +leg +punch +acquit +let +fifteen +vinegar +great +casino +screech +scatter +survey +insulin +grandchild +buss +popcorn +mussel +maker +grower +sire +disobey +causal +zip +archbishop +theme +aeon +eleven +doubt +yardstick +midday +pencil +babe +shipwreck +patrol +rubbish +central +hoard +pout +pour +thin +drill +coffin +cherubim +bent +pawn +process +lock +slim +high +slit +bend +slip +pelvis +martyr +trumpet +weaken +rhubarb +delay +blackhead +luster +stow +halter +singular +await +wristwatch +notebook +tier +marrow +hawk +autograph +tomato +counter +robot +element +writ +allot +allow +alloy +thigh +mute +insight +spatula +comma +mutt +warren +perfect +decay +shudder +garnet +derelict +prosper +python +belch +bat +launder +dock +snake +kiss +bar +cage +wrangler +truth +scorch +subset +bump +static +thirteen +mete +jagged +disco +tenth +wander +matrix +bag +fatherland +venom +czar +oblong +lob +shut +perish +tempo +graze +tempt +shun +embarrass +minstrel +chilli +mainland +spill +length +stickler +scare +scarf +manuscript +scene +cobweb +owner +scent +prank +lop +opossum +sergeant +spaceship +painful +stomach +chagrin +vouch +rotunda +haven +steel +wet +bother +aggressor +psalm +disband +unman +steep +torrent +misunderstand +beggar +viewer +partnership +correspond +tonight +receptionist +fourteenth +mischief +depict +soak +bacterium +bassoon +hammer +adjunct +lilt +soap +soar +calculus +manor +raindrop +cipher +vise +segment +fervent +instil +locust +enlist +soprano +fiasco +brew +fact +bring +brine +bedroom +rough +asylum +trivial +brink +redirect +disillusion +planter +jay +jaw +jar +jam +tape +bourbon +flinch +hope +jackpot +move +familiar +scorn +sinus +wring +antagonist +smash +shaver +summon +stuff +rein +withstand +pronoun +packer +frame +packet +bellhop +airmail +dungeon +wire +mien +partisan +unravel +piston +pistol +email +browbeat +fetich +physicist +courtyard +lawsuit +tantrum +drum +quitter +ramp +drug +doorway +puff +roughen +medallion +revamp +migrant +distil +javelin +indict +chromium +lectern +mailman +gondola +quaver +blatant +feather +ballast +sheepish +crisscross +federalist +mannequin +altruism +banish +laser +runway +bathtub +maul +groin +ripe +lush +site +lust +mockingbird +tenor +passbook +ransom +tattoo +inquest +terrorist +buffoon +outbreak +android +balm +ball +balk +dusk +fiesta +bale +bald +dust +broccoli +mosaic +audit +off +shotgun +polyp +command +diphtheria +audio +maggot +compel +glut +glue +rambler +web +generous +clergyman +wee +wed +arrest +crack +scoundrel +government +chancellor +crux +haul +cedar +desk +password +recurs +placenta +crisp +onion +resin +alkali +stagger +imprison +nymph +sprain +overcast +foray +habitat +thief +daylight +flush +wisecrack +ballot +transport +henchman +disbelief +hoarder +avoid +disk +doer +passion +saucepan +stairway +putt +drift +ornithologist +stage +iris +sister +adverb +peal +ingest +union +artefact +parsley +assess +lung +mere +muck +commission +caviar +watchman +stamina +much +function +funnel +cosmopolitan +frisk +shyness +grate +rectum +count +congresswoman +smooth +monument +problem +baptism +cordial +kowtow +sirloin +retina +inn +replica +ink +anesthesia +furl +sexual +saviour +behold +reckless +chum +monday +repeal +veil +vein +ghost +eon +rule +dynamo +torrid +pension +tryout +abhor +buoy +inning +dote +rapid +mansion +defraud +voter +spew +bludgeon +bike +daze +regal +chill +regalia +whack +ale +compassion +blanket +distort +mania +chauvinist +chapel +whisk +daydream +pinch +scalar +handout +roadblock +unblock +math +triumph +chew +paperback +phoney +speck +heliport +sabbath +horn +chef +aristocrat +panda +stardom +lizard +walkout +toll +crunch +dustpan +pursuit +paraffin +sorceress +hairdo +daughter +envoy +adopt +tankard +smoke +loincloth +bunker +anarchist +envious +sanatorium +infield +spigot +thrust +hindsight +total +bra +plot +plow +plop +sweater +gloss +ploy +insult +plod +knoll +beeswax +solicit +award +yard +tariff +overrun +word +err +crest +work +grovel +tinder +era +elbow +spendthrift +quiver +serpent +flunk +impair +liter +chameleon +sever +moron +disappoint +beach +pizza +fever +lad +ladder +lag +lab +lay +law +arch +cosmonaut +retort +greet +greek +green +south +worst +order +greed +salon +gumption +devote +muffin +misconduct +mayor +sheaf +avocado +valor +carton +shear +then +fragment +safe +break +band +bang +coffer +overprint +tzar +bank +bread +crock +gallows +lisp +iguana +schemer +transient +prawn +sled +flock +slew +hostel +burlap +network +diesel +fellowship +amethyst +marigold +barrier +veto +standard +stencil +lollipop +morass +drench +ticket +maniac +raisin +flawless +renew +sprig +regress +vanquish +kin +render +system +hamstring +chopper +disembark +comic +overs +neck +upshot +tourniquet +kiwi +emblem +luncheon +cereal +rebuff +minibus +guild +target +tavern +hike +medley +iota +guilt +iron +minus +pessimist +lull +ponder +strength +realm +widen +silversmith +latter +hamper +transmit +curfew +maiden +boxcar +sue +negro +phase +proverb +grave +deacon +swamp +bracket +aunt +rickshaw +oppress +mitten +crust +boyhood +nephew +toast +geyser +layman +geologist +predecessor +do +ardor +ecologist +roundabout +slack +rebirth +runt +rune +rung +crucifixion +steak +steal +steam +ghoul +reread +misdirect +christian +goulash +pastel +gentleman +cellist +contraband +drawl +accord +unfold +kitchen +cop +cot +cow +brat +excrement +ill +cob +brag +cod +cog +bran +coo +con +emporium +eyesight +tone +spear +royal +trunk +nonconformist +infirm +speak +charisma +scarecrow +warmth +leech +baud +hacksaw +millionth +hoist +spellbind +gracious +physician +inhibit +gnu +launcher +air +aim +ail +abrupt +thrash +aid +stink +have +sticker +sting +throat +brake +cone +hebrew +uplift +stint +descent +perform +descend +decibel +wheel +raid +fuss +nil +swell +hang +evil +hand +fuse +nip +nit +scenario +drip +ragged +client +mamma +kinship +indigo +photo +victim +extol +thyroid +exalt +shout +cognac +board +zillion +righteous +plasma +intercom +fusion +boxer +cape +retreat +cooler +night +flatter +born +rile +flatten +bore +orchid +cede +humor +peek +peel +pose +confer +peer +peep +chafe +foreskin +chaff +diner +coral +visa +banker +horizon +cherish +gingerbread +octopus +croak +faint +dilemma +tetanus +float +profession +bound +curios +sedan +loin +beet +piggyback +wag +bookend +wad +frill +sovereign +fight +gybe +way +wax +burro +war +fizz +peninsula +holdup +boon +true +reset +absent +nursemaid +smidgeon +maximum +crystal +veterinarian +emir +emit +aorta +flat +abstract +molt +flaw +postscript +subsist +prayer +cacao +face +mold +mole +stake +shrine +test +upholster +unwilling +frolic +shrink +heyday +chairperson +hemophilia +faze +affidavit +loyal +longshoreman +igloo +concept +matron +consul +fulcrum +datum +horseback +supplement +toothpick +varnish +grape +zone +mallet +flask +graph +hump +flash +manicurist +glad +rhythm +tusk +terror +idealist +southwest +brown +congest +kitten +blast +brows +ophthalmologist +gun +gum +gut +guy +diarrhoea +upper +brave +regret +bravo +thinker +cost +helpless +tempest +cargo +appear +economist +menthol +medal +havoc +uniform +tarantula +appeal +caveat +genes +gawk +jester +disclaim +goldfish +teacher +buck +merriment +fogey +precursor +plotter +eke +disavow +trial +convertor +pillow +bolt +extra +paragon +keeper +marker +firearm +market +streetcar +prove +subvert +live +matador +club +cluck +clue +logarithm +prepay +graphic +slogan +car +cap +caw +cat +meow +can +cab +heart +hears +chip +sake +bridesmaid +abort +chin +chic +serum +bankrupt +freezer +write +lobe +storeroom +criterion +entrench +afternoon +product +dive +southern +bawl +motorway +pave +drastic +flourish +crepe +grandson +explicit +offend +barnyard +forfeit +haircut +ledger +brain +nitrogen +cold +braid +ethic +willow +theorem +window +artisan +factual +tiara +gizzard +nought +halt +fling +nod +rake +overcrowd +dishearten +hale +half +recap +courtship +taillight +provision +discuss +halo +wont +concerto +servant +drop +domain +supplant +year +operand +wavelength +happen +album +accomplish +space +thirst +rational +thong +carp +cart +virtuoso +quart +rebel +marina +prospector +card +care +fungus +tomahawk +british +honest +nonprofit +profess +blind +madam +blink +rink +rind +ring +drove +tomorrow +size +sheep +sheer +sheet +silent +bookmark +breed +callous +traction +checker +tragic +heartburn +friend +pomp +courier +that +peck +scalpel +rugged +recruit +magnesium +optimist +extinguish +angel +slay +slat +premier +slap +racetrack +slam +anger +breakfast +recover +slab +upbeat +veteran +shore +snout +siesta +begin +prick +halibut +price +foothold +dream +tooth +aerosol +washcloth +fifth +ground +gnat +snack +ratio +stair +proportion +jolt +stain +juror +shrill +pumpkin +cannon +loath +stroll +leather +thermal +husband +druggist +concert +burst +spore +whitewash +unfit +staunch +sport +incisor +concern +crawfish +glaze +complexion +import +clench +pluck +blame +broil +impromptu +whisker +guffaw +pertain +priestess +temper +aura +comet +evict +adroit +dispatch +exploit +semicolon +lioness +harmless +rebuild +toss +sisterhood +textbook +bloodhound +crumb +these +trick +scum +cherub +fool +marksman +zenith +mucus +soil +agnostic +laggard +bias +eras +bestial +beaver +waterway +petunia +helium +develop +media +pester +poetic +document +sweeper +finish +thunderbolt +foal +foam +cymbal +fruit +volley +trawler +smelt +quartz +theater +framework +patchwork +demean +taxi +livestock +fester +battleship +typhus +neon +touch +speed +death +refurbish +treatment +baloney +momentum +lade +ream +hover +frown +spectacular +larva +read +ruler +swig +leapfrog +earnest +detract +stunt +execution +reap +hovel +rear +postcard +incest +roll +engross +oblivion +output +downward +falsehood +laugh +verbal +landslid +squirm +garland +putter +cleanser +deficit +squirt +wheat +sadden +throb +sixth +tuition +strainer +bazaar +throw +comparison +placard +hiatus +chop +fell +wolf +parson +chow +ruff +assassin +processor +heater +outlook +earring +watchdog +your +restless +stare +grater +log +area +start +stealth +low +lot +wigwam +groan +pitcher +rump +posterior +recoil +omelet +lymph +thaw +corduroy +wholes +hire +fraud +default +bucket +draftsman +cornmeal +gibberish +charlatan +scanner +pickax +sheath +mover +antenna +housework +valid +colt +you +houseboat +poor +polar +poop +peat +pear +peas +overreact +podium +peak +pool +fiscal +assert +moonlight +repay +forswear +mourner +skeleton +breadth +groundwork +angler +month +unrest +bequeath +carpet +corps +gymnast +foster +spearhead +fervor +fountain +washroom +horror +verb +minded +heaven +ceaseless +saboteur +tendril +blackout +smock +homonym +resound +exodus +casual +bomber +protractor +lurch +milk +turmoil +vet +excess +strong +arena +divisor +noticeboard +outgrowth +vegetarian +whine +soldier +amount +base +trainer +put +haemoglobin +seventeenth +taker +helper +pup +titbit +assessor +chestnut +yuck +suction +grill +nine +parasol +transcend +pusher +boycott +archipelago +tract +phrase +magenta +frostbit +sheikh +warhead +spreadsheet +lyre +snippet +reject +gash +circuit +rude +sneak +denial +gasp +reelect +undergo +figment +overnight +ego +dread +egg +lynch +earthworm +help +reservoir +slouch +auburn +reclaim +preempt +soot +helm +hell +clarinet +limelight +prowler +lateral +heron +astonish +forbear +dim +food +musket +terrain +vomit +foot +stopper +holiday +payer +twelfth +bless +radial +trailer +pamper +thirteenth +talisman +event +magnet +vertigo +wedlock +teak +publish +eardrum +sustain +shrivel +outrun +ass +pun +drink +bass +dirt +dung +dune +cornstarch +reason +heroin +ask +ash +turnout +bask +bash +pus +launch +curtsey +round +bridegroom +caption +liaison +heartbeat +blush +assign +arsenal +demagog +elder +effortless +mist +miss +blossom +minion +station +expand +fingernail +scheme +banana +merciless +slosh +behind +bowel +trapper +boredom +sign +leotard +tunic +lament +ouster +hyaena +bride +bobsled +currant +toward +weapon +phobia +chipmunk +yowl +null +sensual +lid +lie +koala +cave +lip +useless +honeycomb +popular +quota +plunder +mace +watt +smoulder +clear +cleat +succor +clean +skein +blend +humid +phenomenon +cowgirl +flicker +sheik +crayon +copyright +paranoid +scooter +less +ikon +ampersand +waterproof +custodian +outlast +strut +strum +basement +chimney +monogram +fluff +chasten +geld +courteous +cramp +backtrack +grey +close +despatch +bandwagon +haddock +aqueduct +wow +grocer +won +woodchuck +wok +woe +stalk +bettor +wreath +philanthropist +spray +distinguish +zipper +garrison +delimit +eggplant +buzz +vault +protector +mausoleum +onward +oversleep +liken +proton +header +badminton +vessel +catwalk +stamp +damp +nape +damn +threaten +dame +alto +liven +exempt +deter +liver +hobnob +furrow +pact +loom +utmost +look +socialist +governor +rope +pace +while +smart +fleet +loot +loop +pack +petal +hoax +grant +belong +makeshift +discredit +grand +conflict +sham +hallway +optic +dime +bonus +banter +overweight +user +boa +grind +auditor +five +ambassador +chore +abstain +bearer +morsel +tick +botch +pier +carat +march +albino +game +jibe +banjo +optician +signal +manifest +eel +sleigh +sketch +creation +undress +yolk +urgent +impoverish +mustang +clothespin +fundamentalist +gild +simmer +slash +slapstick +run +rum +rub +booklet +benefactor +rug +stem +step +stew +taboo +subtract +rut +discus +shine +faith +pigpen +letup +portico +reappear +aye +congressman +block +foreswear +misbehavior +dude +within +gilt +pentagon +connector +syllabus +palomino +harem +frost +reed +womankind +reef +reek +reel +dull +skulk +swagger +chiropractor +ringlet +foresight +similar +psychopath +kidney +straitjacket +nab +sullen +nag +objection +obelisk +nap +department +nay +draw +resign +drag +tundra +drab +formal +horseplay +outing +orbit +depth +bribe +pinion +underbrush +cheerful +go +emboss +dunk +ammonia +compact +aquarium +baron +aria +stave +ameer +shack +geranium +warpath +epitaph +velour +schooner +virtuous +fond +wave +trough +cellular +tenet +nausea +stiff +asparagus +gender +button +hive +verdict +cloister +pilfer +picket +blitz +jump +booth +ardent +languor +cartel +click +poke +wallet +colonel +valet +cell +rotten +experiment +stanch +brooch +fifteenth +quell +weirdo +convert +chant +gent +repel +behead +ricksha +wig +daybreak +danger +win +clout +wit +ligament +infest +gimmick +wiz +cloud +metaphor +snoop +copperhead +crag +crab +cram +expressway +compatriot +mismatch +starter +salad +consort +ride +donut +archer +meet +control +wharf +halloween +glade +skirt +bandana +filament +circular +fare +farm +thunderstorm +canker +foment +corral +scoop +encyclopedia +scoot +agenda +american +cadet +sperm +gunman +hock +brood +broom +brook +walnut +youngster +skater +frond +auto +dike +relentless +snorkel +placid +stout +hands +front +refuel +muff +perfectionist +mode +upward +commonwealth +unwind +chunk +mollusc +seesaw +apartheid +mollusk +special +gallant +armor +confess +fathom +remiss +activist +playground +wick +obsess +umbrella +hopscotch +watermark +jilt +undo +advisor +sneer +princess +shrew +timer +keep +counterpart +elector +keel +mad +blogger +seventeen +bitch +drinker +equinox +dump +wrapper +chintz +attach +attack +jellyfish +final +beard +introvert +punish +feint +noun +plough +piecework +waist +photograph +spurn +cartoonist +beg +bed +bee +discolor +swindler +firework +spurt +bet +are +exhibit +fume +tabu +torment +sundown +portrait +need +border +rotor +bastard +sprinkler +gunner +jaunt +tactic +truck +detector +visor +brand +african +camper +rigor +awe +plumber +eject +spleen +urn +upset +snapshot +businessman +constrain +skunk +affair +indoor +crate +molest +cavort +sheriff +fiberglass +winner +wreak +rash +earner +rasp +gradual +fuel +sulfur +joint +fallout +buyer +endless +gray +tobacco +gust +ordain +topaz +nutmeg +she +contain +recoup +grab +conduct +widow +hardwood +shake +orphan +portend +computer +driveway +equip +portent +unearth +southward +tend +state +lug +tens +antler +tent +bleed +castoff +blinker +keg +bemoan +key +overhaul +thank +sniff +career +admit +spatter +plankton +jersey +christen +tuesday +poem +sari +tread +shaikh +yap +cent +quiz +yam +treat +yak +whisper +poet +fibber +spaniel +nuptial +brunch +debtor +novel +ripen +pandemonium +harden +neuron +hearten +steer +generic +balloon +speaker +northwest +blight +fireman +flesh +absorb +powwow +inbreed +spree +magician +rift +weld +surfboard +lunch +glower +well +drone +welt +underdog +discord +mistaken +dose +distant +laurel +skill +cinder +jackal +dais +ovum +snicker +stratum +possess +warrant +homework +canter +rafter +fate +burden +loss +clown +tablespoons +lose +divest +satirist +rote +page +backlog +shed +glare +twitter +hush +redress +home +peter +competitor +pinpoint +overlay +broad +overlap +hinder +individualist +journal +offset +instinct +smidgen +refuge +freedom +cleans +nightclub +rodeo +dominion +wallop +buzzard +cocoa +pointless +gerbil +snowplow +mastermind +museum +poinsettia +drumstick +mohair +jinx +backhand +cricket +north +gait +admonish +neutral +ho +technician +overflow +ear +eat +he +leper +limit +cello +display +wringer +twist +entreat +contest +meteor +finch +chemist +fodder +star +stay +stag +foil +stab +phosphorus +appoint +sphinx +shunt +broncho +atheist +atheism +portion +pardon +mackerel +demand +unfurl +protest +asian +captain +gamut +swab +swan +sinew +swat +swap +anticlimax +sway +loon +appal +void +vase +smack +govern +affect +hitch +vast +pilgrim +naturalist +vector +washout +whirlwind +quilt +crave +yack +cactus +quill +pander +wreck +orchestra +bikini +spokesman +haze +new +net +maverick +seventh +mew +onus +cardboard +interpret +taper +credit +harass +jamb +permit +prolog +menial +hunch +campaign +bayonet +moral +handlebar +overhear +ore +overheat +calk +overhead +calm +intrust +recommend +type +tell +calf +demon +wart +warp +warn +dogma +warm +pecan +adult +qualm +flotilla +ward +blindfold +confound +rook +room +candlestick +worth +bungalow +headway +root +defer +vodka +give +climax +assent +honey +surveyor +quail +freshen +polio +rib +stockyard +answer +abdomen +plank +coup +fracas +passageway +waterfront +lesbian +guerilla +attempt +third +maintain +capitalist +fetus +deck +keyboard +windshield +furlong +harmonica +crew +better +persist +pass +workout +microfilm +caterpillar +grammar +meat +mistrust +roast +side +bone +luck +caustic +aids +dawn +extract +jell +contend +velvet +gradient +open +crucial +content +reader +linear +whiff +bestow +mistress +needlework +steward +fleck +loud +skinflint +playwright +grade +hoop +hoot +buttock +hook +ditch +hoof +hood +hydrant +acquaint +spinach +historian +enthral +woodwind +brainwash +dwell +inferno +twister +gym +somewhat +gambler +symptom +preacher +affront +keyword +matter +loiter +mink +seep +quench +modern +mind +mine +ginger +seed +seem +churn +mint +unfasten +alibi +desist +chess +sleeper +quarterback +phonograph +chatterbox +regular +condominium +blacklist +don +alarm +impostor +dog +doe +solemn +digress +constrict +consumer +dot +hunger +visitor +probe +syntax +chord +sundial +northeast +explain +jailer +sugar +folder +inventor +edict +patter +smut +stop +coast +pincushion +watermelon +smug +earl +earn +peacock +bay +reload +bad +troop +cower +ban +stinger +linguist +enchant +attest +subject +snuff +scrap +sail +causeway +scram +baboon +warrior +triplet +vitamin +lotion +orthodontist +beautician +cousin +motto +sprinter +pate +typist +height +gusher +aftermath +arsenic +ether +accustom +tint +recur +three +erect +ting +chrysanthemum +trigger +interest +basic +basil +basin +idol +chug +mushroom +suppress +dismiss +deepen +encyclopaedia +unpopular +tank +affirm +tang +near +moratorium +neat +motorist +anchor +spawn +seven +cane +diaphragm +it +shame +jest +in +id +disappear +if +abscess +growl +sap +parish +make +kit +delight +squat +garlic +warden +unicorn +jealous +overt +bequest +kid +butter +romp +smoker +inherit +bedspread +diphthong +left +protocol +just +yen +unfair +psych +human +yes +cretin +yew +legion +character +wretch +save +opt +discreet +background +shoulder +nude +manual +pillar +dean +squander +deal +deaf +maxim +dead +revel +intern +dear +strife +sprawl +pail +collect +normal +councilor +flounder +bullfight +tartar +bold +statistician +burn +blackmail +cottontail +sift +protagonist +burp +burr +tartan +super +innuendo +crucifix +craftsman +commit +marshal +unsay +paunch +chimp +down +lieu +chime +initial +lampoon +editor +fraction +unseat +fork +undergrowth +form +fore +ford +diaper +overburden +analyst +fort +pavilion +whiskey +boomerang +cosmos +propaganda +shin +disciplinarian +classic +covert +sidestep +drive +gland +scrawl +fatherhood +ship +graft +vista +marxist +smidgin +excel +handed +venison +congeal +marxism +sling +faction +handicap +slink +felt +diet +parenthood +journey +reign +stoke +weekend +derail +billion +potato +jacket +gorilla +almanac +teeth +meander +befriend +proletariat +woodwork +skip +skit +invent +adjourn +mild +mile +skim +skin +mill +primer +proletarian +skid +surplus +seasick +misread +depend +swoon +father +countdown +deject +swoop +regatta +unburden +string +yeast +pathologist +merit +join +jettison +stiffen +hoorah +din +stapler +nectar +die +dig +democrat +noiseless +item +dip +blur +shave +thresher +villa +worm +slake +sunup +talker +fillet +suspect +drunkard +shoo +dwarf +dweller +wail +guardian +clerk +makeup +stallion +waif +detest +tangent +deceit +rue +wait +box +boy +cuckoo +shift +bow +dither +boo +raccoon +cyst +bob +nylon +bog +elect +plumb +kayak +surmount +transplant +saki +wealth +perk +visit +vineyard +somersault +sharpen +yoghurt +aspirin +labyrinth +curriculum +downtown +tandem +rigid +savior +effort +gnome +demolish +pageant +moccasin +melodrama +flu +soul +impel +soup +sour +claim +plaza +reflector +predict +agent +drawer +council +craze +pink +purr +arbor +tilt +clever +parch +pine +till +sunday +sword +tile +pathway +pint +map +mar +mat +may +gelatin +membership +mankind +tablecloth +grow +man +relinquish +aimless +hemlock +omen +tale +switch +jail +deposit +talc +unleash +basket +longhand +talk +shield +rabbi +moralist +lyric +pitch +solder +pointer +group +monitor +bedbug +maid +drummer +maim +mail +main +tonic +killer +shatter +minuet +safari +teller +rattler +outweigh +feverish +peasant +careless +rock +hijack +eyelid +latin +bookshop +poker +gavel +unlock +manifesto +girl +stitch +monolog +priest +dutch +blubber +sensor +correct +monster +zombi +vinyl +jaywalk +cough +underwear +waiter +buzzer +thing +registrar +blacksmith +think +frequent +first +lone +crib +long +extrovert +thoughtless +lap +autocrat +escort +daunt +mermaid +anus +yoghourt +memo +broadcast +butt +proofread +tractor +coconut +lick +piccolo +marijuana +dash +comedian +sulk +nazi +sherbert +stopgap +daredevil +acumen +squad +interior +channel +pain +trace +roster +track +acrid +zigzag +whizz +assault +billow +pair +synonym +napkin +typeset +scowl +voodoo +toucan +amir +shop +lexicon +shot +show +cornea +veranda +therapist +shoe +threshold +corner +label +cornet +enthusiast +fend +objector +flapjack +dice +plume +enough +syphon +black +consent +enthusiasm +fiendish +plump +get +straggler +stomp +midriff +slyness +gee +gibber +neckerchief +gem +disinherit +beseech +skull +businesswoman +yield +stupid +nostril +tallow +kernel +sear +eighteenth +seat +seam +seal +stigma +calendar +wonder +puma +parent +limber +ornament +forecast +gage +pump +august +foreword +slingshot +tacit +wednesday +gauntlet +childbirth +tug +tuck +trader +tour +tout +delirium +stretcher +cancer +spank +cancel +tub +mare +underworld +imp +undershirt +mark +mart +workshop +rancher +fiftieth +chalet +graveyard +squash +wake +sound +litterbug +epidermis +slumber +cock +strait +strain +sudden +protein +par +pat +harsh +paw +pay +woodland +same +heartbreak +pad +cotton +pal +pan +exhaust +oil +chloroform +munch +companion +foghorn +polygon +drain +vertebra +soundproof +outdoor +suitor +money +imprint +leeway +aspect +flavor +asthma +godchild +comradeship +forgo +pile +pill +grip +grit +mop +mow +moo +mom +mob +railroad +grim +grin +oxygen +server +chamber +nose +hallelujah +fulfil +sneaker +afflict +witchcraft +ascend +dole +ascent +spasm +gross +confirm +pioneer +inject +gladden +highbrow +linoleum +intravenous +knife +raincoat +broker +squall +bravado +racoon +opium +contagion +roar +island +insect +mixer +thrive +partizan +road +checkup +dagger +coupon +splint +empress +whiten +strip +uptown +skillet +paraphernalia +jigsaw +totalitarian +madden +tycoon +tripod +striker +shroud +hiccup +gore +spice +ember +magnolia +grouch +conqueror +embed +deadlock +affection +deer +deep +fellow +planetarium +deem +file +girlfriend +deed +hound +film +fill +tedious +selfish +personnel +hybrid +repent +drouth +field +prism +astronaut +fruitless +lapel +shelter +gander +unload +burrow +god +gangway +oral +motel +represent +forger +pheasant +forget +founder +suburban +dollar +rebind +zinc +implement +crimson +hideous +premium +parcel +straightforward +scout +scour +fall +bottleneck +pueblo +hinterland +dampen +dictatorship +flyover +neighborhood +clinch +gnarl +burger +zero +cottonwood +lawyer +further +misrepresent +ribbon +dial +skeptic +stool +trinket +stoop +plush +movement +girlhood +malaria +intrench +twang +mule +ranger +beacon +bigamist +capacitor +search +stupor +margin +airport +chipper +chieftain +narrow +fatten +quotient +wizard +caravan +transit +sadist +sadism +establish +dachshund +hobgoblin +eye +score +distinct +two +splash +libel +furor +wiper +diamond +brisk +opportunist +particular +disfavor +nineteen +town +hour +cluster +fast +dew +remain +paragraph +den +abandon +stubborn +shark +buttress +onslaught +share +sphere +minimum +rainstorm +attain +junket +sharp +botanist +siren +awkward +comfort +rapport +stir +bleat +whopper +blacken +blood +bloom +chute +coax +orchard +coat +doctor +spiteful +electron +blunder +mislead +coal +sect +infant +setback +radar +dough +lava +suffer +hundredth +sodium +bosom +late +speech +clamor +lath +lookout +goof +good +goon +detour +frigid +compound +detach +complain +bombard +headroom +countersign +token +monsoon +clamp +harm +hark +mental +hare +hard +beret +banquet +connect +fist +callus +hart +orient +harp +flower +creditor +trooper +pigeon +seaport +granola +print +foreground +assist +cockroach +pleasant +gig +faucet +prophet +omit +wither +pure +corkscrew +copper +perturb +barbarian +shoal +cups +jabber +razor +construct +paint +leash +statement +mama +hummingbird +catapult +pare +park +selector +glycerin +dentist +part +pars +youth +totter +plead +hangout +cistern +blanch +mountain +cardigan +couch +onset +build +zucchini +flute +salmon +chart +most +charm +moss +eskimo +organist +humanitarian +mammoth +pennant +squelch +weigh +standoff +sector +sparrow +fine +find +giant +merger +nervous +ruin +fiend +boulder +prowess +paperweight +cholera +express +ferret +cheapen +batter +breast +theft +silk +pellet +restart +silo +huff +common +archaeologist +printout +vine +lion +overeat +tender +expert +burner +myriad +stowaway +subscript +hypochondria +premiss +egotist +complement +figurehead +mailbox +pagoda +aircraft +sultan +archway +annual +foreign +point +smother +newborn +pamphlet +dancer +esophagus +platinum +pocketbook +secret +amnesia +reformat +finalist +ram +gay +gas +gap +holler +gal +understand +gag +chatter +gab +bile +politician +metro +solid +bill +holocaust +crutch +fun +lingo +manner +mystic +astound +rancor +eczema +ingrain +anaesthesia +sociologist +dishonor +ewe +seminar +corridor +neutron +itch +leopard +yesterday +moment +stripe +unveil +timid +task +werewolf +withdraw +landmark +grid +recant +spend +howl +darn +shape +snot +timber +rundown +impetus +cut +cur +pollster +snag +forbid +cue +punter +cub +snap +bridal +easter +brainstorm +bin +squawk +rebound +bib +judgement +redeem +bit +knock +disrepair +blemish +flue +fagot +flux +bamboo +foolish +walrus +sequin +transgress +often +back +impeach +extremist +mirror +lightning +scald +scale +culprit +pet +pelt +pew +pep +pen +scalp +lard +lark +peg +pea +patient +fed +megaton +constraint +oatmeal +drama +catnip +pediatrician +offshoot +obstetrician +gambit +maelstrom +tiff +clack +lesson +jockey +few +doll +errand +camera +handbook +forward +nougat +sideshow +showman +switchboard +calico +lifeguard +planet +jumbo +azalea +constant +flow +possessor +lye +curd +cure +curb +curl +prevail +stagecoach +leaflet +crypt +underweight +cellar +lend +tablespoon +papa +lens +cater +desert +statesman +mantel +notion +uterus +anguish +caribou +stroller +seaman +golfer +strew +parrakeet +peanut +welter +mower +rudder +compost +blaze +atlas +gravel +queen +dessert +rhyme +claustrophobia +surgeon +molar +verandah +knight +shock +crow +queer +crop +append +power +junior +anthem +access +clipboard +bachelor +intercept +sink +sing +roof +bode +implicit +remark +talent +conceit +resurrect +weekday +climb +honor +blizzard +liqueur +talon +oval +scandal +gateway +sermon +lime +patron +asteroid +butler +charcoal +trait +kiosk +thatch +trail +train +armadillo +harvest +fan +account +tunnel +carrot +obvious +smear +parkway +unread +fetch +employe +truism +sanitarium +teamster +boney +spruce +serial +contempt +hangar +lamb +lame +lamp +forest +goner +stock +roam +leukemia +bluff +terrier +fray +drape +bind +guru +liner +linen +chief +poacher +furious +furnish +disarm +meter +bunch +marshmallow +decorum +labor +kindergarten +heroism +willing +marsh +dad +junction +dab +dam +spell +swordfish +mention +courtroom +sonata +day +strive +flail +snowdrift +thrill +slacken +cider +memoir +sawdust +disregard +flair +thwart +jailor +jugular +pivot +cupboard +lentil +salesman +hippopotamus +matt +defend +rev +repress +stub +mate +barley +stud +smog +stun +red +franc +frank +hanker +fourteen +salami +likelihood +afterward +squadron +indent +mortar +skateboard +yarn +mortal +workbook +retain +retail +waitress +suffix +overshoot +ethnic +sack +brute +whoop +puppet +guidebook +vandal +pauper +ancient +monkey +bologna +laps +vulgar +vagina +hexagon +scant +liquor +cabin +sixteenth +gear +eavesdrop +bulldog +smolder +forethought +springboard +nun +bodyguard +prune +shrapnel +shampoo +linchpin +lover +anthropologist +tide +cavern +pedlar +countryman +waken +optimum +mix +parka +spinster +meek +dryness +hazel +eight +clamber +handbag +hoodwink +transcript +payment +gullet +gulley +gather +request +absurd +rendezvous +occasion +thicken +recess +kite +text +hamlet +traitor +industrialist +sidetrack +portfolio +floodlight +thicket +staff +communism +scorpion +madman +prolong +resubmit +satan +oppressor +communist +inferior +equilibrium +gumdrop +starch +beat +rush +bear +beam +bean +october +beak +bead +organ +ashtray +nutriment +eyebrow +motherhood +mascara +conform +showdown +infidel +racket +interview +reform +pattern +nebula +brunt +hammock +progress +tailspin +sorrow +stratagem +deliver +blackbird +boloney +exclaim +instant +joke +equal +kosher +swim +swallow +highland +guerrilla +glorious +wear +comment +vent +denim +overcoat +commend +vend +harpoon +manhood +citizenship +copier +newscast +gaze +teamwork +gulch +curtain +curtail +hyacinth +juggler +censor +goddess +bulk +bull +bulb +skew +carburetor +cypher +plain +homey +bray +kinfolk +bicker +dissent +squid +blimp +creak +prose +partner +inspector +lynchpin +portray +whirl +grinder +matchbook +defiant +anorak +tumbler +infer +whirr +tighten +pockmark +sauna +ion +grandstand +sunburn +judgment +retard +center +builder +pickpocket +thought +starboard +usual +coaster +humdrum +fingerprint +storey +clinic +interim +surpass +tough +earshot +flashlight +tong +flee +lupin +lake +bench +add +citizen +ado +crossroad +ravel +match +raven +cantaloup +punctual +newsstand +dryer +insert +flamingo +like +success +sofa +journalist +heed +arraign +chick +soft +heel +outfield +propel +fuze +hail +hair +convey +proper +paddock +novelist +shrug +shrub +slide +tureen +regain +pepper +hose +slight +host +panel +beaker +actual +socket +flake +preen +toadstool +pickaback +discard +tomb +tome +snitch +chronic +guard +esteem +custard +underpass +glimmer +gene +maze +globe +buy +bus +coke +sequel +but +bun +bum +bug +bud +embargo +woodsman +wise +ecosystem +debrief +flip +wisp +wist +trapezoid +condiment +plutonium +pin +garter +domino +circus +pie +pig +pit +campus +gush +oaf +cashew +oak +detail +virtual +detain +sewer +oar +redden +dresser +wallow +nutrient +godsend +yelp +baker +jab +hiker +pupil +yell +cookbook +vermin +sleek +sleet +sleep +liar +hate +trolley +sallow +tweet +glider +under +tweed +pride +merchant +lure +risk +rise +lurk +jack +confetti +anemia +school +parrot +enjoy +overdo +cracker +almond +direct +nail +street +monorail +ransack +blue +hide +worsen +poison +beater +supplier +dashboard +wink +even +pontoon +studio +path +crossbow +enrol +connoisseur +forum +ravish +auction +settler +mentor +midway +blowtorch +stray +straw +strap +cassino +would +phlegm +bayou +asset +spike +preview +musk +mush +saber +muse +grief +phone +muss +pouch +must +shoot +hutch +ma +ms +mr +machinist +fortress +quarrel +loosen +joyous +hemoglobin +dolphin +mayhem +attract +end +trill +keen +bunk +vagrant +rhinoceros +shred +toxin +gate +ancestor +dialect +moisten +kilowatt +mess +lump +mesh +sparkler +parallel +stronghold +splendid +spout +patent +enter +vapor +hedgehog +fetter +deform +clapper +sprout +over +bleach +mallard +oven +caster +digest +forehead +theologian +womanhood +comprehend +tramp +drawback +fade +croquet +tourist +plaster +roost +knighthood +monarch +rental +gloom +chuck +choir +prohibit +hanger +unscrew +gymnasium +poncho +truant +saturday +depress +goo +lair +dinosaur +nitwit +psychic +tonsil +gob +emphysema +nite +washer +resistor +carcass +rail +free +rain +acrobat +fret +harpist +ritual +filter +hopeless +soda +rang +accent +puck +rank +restrict +rant +sober +toy +their +sarcasm +top +tow +tot +fiction +ton +duress +toe +urban +murder +overdraw +tool +hearth +embellish +solicitor +toot +incur +western +nonpartisan +lather +prong +flame +mirth +countess +rag +donkey +fashion +handkerchief +ruckus +taint +raw +rat +rap +protract +spade +ray +snow +thorough +contact +hatch +snob +cleft +extravert +quicken +rider +evangelist +shallow +milkman +coil +coin +glow +interject +flop +metal +freeway +policewoman +flog +yank +chariot +bait +endear +saga +alight +random +sage +dupe +radio +rector +earth +bail +shellfish +spite +stanza +disgust +axiom +waltz +gees +watch +fluid +ultimatum +report +reconstruct +noon +spokesperson +egoism +public +erupt +pacifist +pummel +habit +wrest +nut +resist +corrupt +hourglass +mull +mud +mug +finger +mum +approach +wean +weak +contort +boss +toothbrush +southeast +larynx +devour +devout +censorship +newt +protect +irregular +fault +papyrus +facet +elf +smuggler +trust +bingo +bathroom +beef +legend +beer +spread +communion +loft +bladder +uncommon +craft +spearmint +catch +snipe +teapot +misfit +lessen +thousandth +referendum +pyramid +handrail +broth +lollypop +exterior +suggest +wound +overstep +utilitarian +complex +papaya +screw +pick +deflect +suburb +portal +postmark +tassel +ocean +mother +bough +bugger +rodent +shorthand +enlighten +elk +elm +moonbeam +flutist +kelp +misprint +teetotal +upturn +ramrod +dismount +quicksand +spanner +authorship +roach +befit +rumor +apart +ditto +gift +zeal +contradict +hunt +dishonest +zoom +mongrel +hunk +mosquito +hunchback +sanction +excerpt +curio +accost +usher +indirect +intellect +doorstep +nobleman +cooper +combat +letterhead +ice +rhino +newsprint +skylight +convict +christmas +splendor +cord +core +khaki +brawl +corn +brawn +cork +discount +shuck +plug +census +cowboy +plum +choke +surround +caulk +dinner +plus +alga +duke +abet +civic +civil +bath +engulf +cafeteria +art +scamper +transform +sunlight +forbad +virgin +gin +head +medium +amateur +heal +stereo +heat +hear +heap +raft +counsel +muster +bargain +bide +latch +adorn +trim +trio +forearm +cobbler +trip +no +tit +when +junta +tin +whet +tie +implant +depot +pseudonym +evergreen +cleric +toad +geneticist +bullet +navel +yacht +withhold +fasten +backward +coach +impression +rob +rod +focus +livelihood +snip +yokel +rot +discern +environment +aplomb +melon +prop +coop +impend +plantain +cook +cool +looney +level +brother +encroach +quick +lever +pork +drier +trend +bullfrog +pore +inland +voucher +takeoff +bake +port +colic +hymn +choral +postman +spire +theist +thresh +tormentor +humorist +water +fluke +entertain +witch +twentieth +tire +boast +catnap +blotch +cinnamon +prude +weird +tweak +brighten +touchdown +post +panacea +concoct +scan +handler +prey +today +chapter +conductor +altar +cashier +drown +dismal +inhabit +judo +conceal +flagship +hullabaloo +fauna +laughter +streak +overpass +sandbag +trump +stream +despot +stroke +cube +hydrogen +bigot +dress +vital +fourth +dope +ballroom +fascist +clone +scoff +fascism +birthday +apprenticeship +eighth +repeat +classroom +twitch +liquid +inform +reaper +lagoon +superscript +refund +rye +midnight +blare +worship +thermostat +apex +platform +farmer +meridian +cutter +underneath +conquer +fern +rescind +wagon +term +name +realist +opera +bunion +bullion +realism +ailment +torch +zebra +distributor +hysteria +hacker +concur +profit +middleman +gram +clover +hull +hulk +flyer +tuner +flare +highjack +motion +turn +place +swine +swing +turf +preach +childhood +origin +pelican +feign +suspend +insist +scollop +bobcat +array +peddler +given +afterthought +district +opus +trillion +plastic +assort +white +hue +hug +hub +cope +season +hut +enigma +naught +grunt +holder +wide +bewitch +spokeswoman +oath +powder +rend +froth +pro +ani +ant +rent +dragon +stolid +marathon +ideal +blunt +surf +sure +aspen +equestrian +tornado +freshman +librarian +bluebird +icon +latex +tendon +annul +seafood +later +koran +readjust +senior +slope +perch +convoy +cheat +cheap +trespass +hack +broach +hustler +trot +woolen +gulf +genius +gull +shimmer +crime +gulp +woof +wood +deign +wool +entrant +viewpoint +lighten +jazz +festoon +tailor +lighter +dye +homestead +reveal +aluminum +workman +joker +dumfound +bison +picnic +pane +vizor +prowl +optometrist +detect +crooked +review +spoons +hiss +smokestack +caucus +fearless +guitar +coma +comb +come +zodiac +isthmus +reaction +superstar +region +quiet +contract +railway +penal +adjoin +color +armchair +pot +period +pop +pole +colon +polo +pod +poll +runaway +turkey +hobo +schoolboy +tiger +padlock +hernia +careful +spirit +robber +pilot +case +shaft +amend +mount +cash +cask +cast +mound +ventriloquist +vest +exult +clutter +helmet +projector +author +alphabet +fender +bowl +check +macaroni +catfish +bellboy +hermit +week +sang +applaud +nest +driver +weed +director +petticoat +lute +puke +vowel +muffler +weep +cartoon +ranch +relief +model +reward +sinner +clod +clog +tip +kilt +ninth +clot +lavish +violent +kill +kiln +kilo +polish +satin +blow +blot +hint +rose +regent +except +blog +bloc +blob +hind +scrapbook +disrupt +impound +kingdom +blowout +sandman +mugger +towel +bracelet +snort +friar +tower +node +deduct +wombat +interlock +canteen +slice +mood +slick +legal +moon +moos +teardrop +moot +heir +porter +metropolis +quit +unmask +slaughter +quip +ok +oh +of +jeer +shrimp +pistachio +karat +stand +ox +doze +accredit +amber +tribe +vicar +polka +garb +spinal +forewarn +feudal +whaler +there +racism +strict +racist +valley +fish +gibe +relic +jug +regard +cabinet +castaway +strenuous +jut +terminus +feeler +grasp +grass +toilet +ruffian +cinema +frighten +lichen +encompass +bishop +incorrect +abyss +fiddler +heather +idiot +diarrhea +rubber +idiom +heathen +trash +stalwart +championship +symbol +cove +nucleus +serious +brass +wife +invest +derrick +treason +apparel +platter +all +lace +duplex +lack +spacecraft +disc +dish +follow +settlement +titter +wanton +thursday +program +neglig +woman +song +fat +roe +psychologist +retract +ultraviolet +awful +dapper +fad +induct +stimulus +list +trench +align +flick +ten +tea +tee +rate +design +chalk +what +sub +sun +sum +whimper +rascal +brief +overload +crush +version +pulpit +intersect +row +womb +lacquer +pumpernickel +backer +goodby +thrift +misinterpret +heifer +jogger +cataract +haemophilia +murmur +snug +snub +herring +proceed +tarpaulin +wield +hurray +rustic +quash +inlay +garnish +hurrah +minor +ladybug +wretched +flap +mire +protestor +stutter +flit +flag +stick +mellow +chaplain +berth +wrestler +plagiarist +searchlight +sunscreen +pond +court +goal +goad +goat +sandwich +okay +algebra +headrest +embalm +reflect +catalog +numb +short +ricochet +tsar +shade +waylay +mission +scientist +flaunt +reconnect +pretext +stride +islam +thirtieth +style +glide +pray +inward +wilder +abbey +mattress +resort +airstrip +bout +soccer +might +alter +return +hunter +underground +abacus +mathematician +liniment +policeman +refresh +tactless +friendship +weight +needless +duchess +falsetto +expect +inflict +wager +alcohol +disquiet +hilt +dugout +loll +health +hill +shipment +fiber +solvent +friday +differ +effect +disinfect +octagon +physic +teach +sidewalk +jew +blister +thread +threat +bushel +feed +dine +feel +sailor +revolution +least +blank +cigaret +idea +moan +script +gourmet +interact +grime +stork +swarm +storm +moat +syrup +store +mainstay +imperfect +option +hotel +fidget +king +kind +vial +kink +stall +cuff +foreleg +stale +restful +amass +cleaner +exert +strengthen +shrewd +bookworm +gale +gala +gall +remodel +smallpox +toughen +bacteria +chairman +donor +pianist +buff +gill +foreman +rapist +reckon +english +reach +react +nothing +quorum +hyena +amphibian +saloon +notch +scaffold +asphalt +memorandum +felon +font +anvil +firewood +betray +hip +shepherd +hit +deaden +reprint +him +adulthood +snowstorm +forego +stump +martyrdom +arc +bare +bard +bark +ark +arm +barn +blurt +parchment +various +plywood +nincompoop +solo +muslim +sole +outfit +succeed +inertia +orangutang +blazer +bandit +context +bond +cynic +sloth +flier +distress +chaperon +sweet +wastebasket +sweep +weasel +rave +shaykh +bolster +dub +overlook +dud +due +buttermilk +pa +watchword +brick +pi +flight +quintet +dropout +marihuana +cinch +temperament +instructor +heighten +toga +shove +batch +pitchfork +kick +behavior +incognito +lodger +bluegrass +sirup +rip +shamrock +rim +frantic +rig +rid +reprogram +chauffeur +shirt +kimono +viola +shirk +sliver +straightjacket +restraint +painless +throwback +cement +birch +robust +knack +lower +earmark +cheek +cheep +cheer +pollen +facial +vigilant +cabaret +continent +tablet +contractor +plateau +tuxedo +complaint +vendor +foreshadow +awaken +confront +uproar +distrust +breeder +hallmark +play +global +litter +wonderland +butterscotch +saucer +prow +seller +prom +prod +sag +perpendicular +tinker +raider +vivid +cautious +undertow +yawn +ordeal +militia +dialog +tomboy +conquest +momma +piteous +holster +vagabond +stench +canal +pundit +question +swill +parsnip +etch +filet +potassium +glamour +cloth +crank +usurp +delta +upright +crane +outpost +penguin +patriot +consist +apricot +caller +peppermint +husk +cartwheel +highlight +dill +freak +dismay +sublet +sagebrush +rainbow +lemon +riot +peach +grouper +saffron +nick +parlor +ferment +bandstand +mock +nice +mustard +chirp +meaning +vigil +vice +ocular +remit +pyre +buffalo +scroll +pervert +lean +alien +dispel +gang +theorist +gold +uphold +floss +breach +sniper +materialist +toenail +spool +spoon +spook +spoof +harlot +outdo +pleas +pleat +trawl +procession +fold +reunion +acid +folk +sandstorm +outsmart +acronym +relent +kangaroo +gloat +miser +cyclist +barb +survivor +guarantor +orangutan +armpit +shovel +duct +ensign +apt +volt +motor +duck +thick +redo +ape +use +fee +fen +frog +germ +modicum +fez +sort +parliament +porch +musician +impress +sore +rabbit +recount +penis +sculptor +annoy +topic +augment +critic +lumber +executor +proof +bittersweet +tap +tar +tax +villain +tag +condescend +tab +spa +silt +tan +rape +counterfeit +sir +sip +scuff +sit +tamper +six +outclass +occur +sic +carrier +goldsmith +toddler +panic +sin +defeat +tension +lesion +attend +tact +hazard +discomfort +tack +wrist +taco +footpath +aftereffect +light +arduous +schoolchild +sailboat +stamped +minnow +damsel +accompanist +hemp +tyrant +badger +glen +superior +inlet +sill +glee +nostalgia +flank +restrain +glisten +turban +redhead +bye +flex +crash +citrus +flour +flout +emerald +flea +republican +investor +successor +easel +footstool +profound +edit +feast +fuzz +trap +blacktop +cocoon +tray +lilac +mincemeat +interplay +our +proclaim +out +semen +tabloid +cocktail +sentiment +frontier +vehement +disarray +clatter +impart +plural +proviso +planner +utensil +tenement +pendant +gospel +tenant +greenhorn +tanker +zoologist +rivet +uproot +embryo +sew +bouquet +echo +bonnet +eleventh +synagog +salient +droop +unknown +galley +snore +anaemia +drool +boil +tidbit +shell +shelf +transistor +woo +diminish +persecutor +goblin +institution +kickback +frugal +brazen +yodel +laughingstock +clip +fowl +splatter +flunkey +blip +footwork +outstrip +disjoint +pallor +catholic +clove +rout +outward +bagel +lope +divert +trivia +pharmacist +divers +clash +petrol +siphon +filch +fortieth +class +clasp +fang +dens +dent +pipe +vernacular +gain +son +stove +sonnet +utter +chicken +feat +winch +dandruff +rioter +herald +piano +local +counsellor +vigor +sued +skimp +plaintiff +spud +watercolor +barter +bronco +spur +rite +ghetto +bisect +compliment +ascertain +sediment +view +unison +workbench +ebb +expel +hymnal +distract +violet +still +closet +superb +favor +viper +crude +torpedo +avow +jot +exam +amen +joy +foetus +job +spoil +jog +swift +memento +lifeboat +april +grain +commando +wall +hyphen +walk +respect +unclean +decent +trademark +tutor +reindeer +mike +nickel +cypress +penmanship +dearth +overturn +present +kerchief +corset +wilt +vanilla +priesthood +will +fingertip +wild +whirlpool +layer +mutant +motif +apprehend +rooster +lightweight +thug +thud +whore +headlight +cross +member +pediatrist +inch +grandeur +slave +diploma +outcast +beast +student +pedal +whale +collar +gutter +masochist +overwork +scissor +twirl +flint +outgrow +bandanna +rocker +cameo +rocket +camel +boot +wren +obtain +replenish +biologist +daub +distend +smite +now +panther +drunk +smith +hall +book +ski +enact +knob +sick +myth +know +knot +press +redesign +doughnut +loser +cutlet +vortex +clutch +exceed +setter +flagrant +birthmark +demeanor +growth +export +leaf +lead +leak +miner +leap +belt +leader +trout +obey +slur +mitt +slut +slum +pasta +mite +slug +throne +pike +throng +rare +linger +column +biscuit +fear +swear +sweat +udder +emperor +owl +outset +own +owe +weather +champ +brush +billfold +gape +rowboat +van +platoon +transfer +spiral +grotto +cliff +vat +nourish +catsup +unwrap +saunter +mutter +brassier +assail +tomcat +daffodil +nightgown +record +cake +faggot +maroon +boardwalk +abbot +counteract +limb +squirrel +mutual +glint +boor +percent +other +boom +branch +cutthroat +junk +mulch +june +squeak +squeal +extort +jewel +gynecologist +vane +sash diff --git a/libs/pyutil/fileutil.py~ b/libs/pyutil/fileutil.py~ deleted file mode 100644 index e37eb792..00000000 --- a/libs/pyutil/fileutil.py~ +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -""" -Futz with files like a pro. -""" - -import errno, exceptions, os, stat, tempfile - -try: - import bsddb -except ImportError: - DBNoSuchFileError = None -else: - DBNoSuchFileError = bsddb.db.DBNoSuchFileError - -# read_file() and write_file() copied from Mark Seaborn's blog post. Please -# read it for complete rationale: -# http://lackingrhoticity.blogspot.com/2009/12/readfile-and-writefile-in-python.html - -def read_file(filename, mode='rb'): - """ Read the contents of the file named filename and return it in - a string. This function closes the file handle before it returns - (even if the underlying Python implementation's garbage collector - doesn't). """ - fh = open(filename, mode) - try: - return fh.read() - finally: - fh.close() - -def write_file(filename, data, mode='wb'): - """ Write the string data into a file named filename. This - function closes the file handle (ensuring that the written data is - flushed from the perspective of the Python implementation) before - it returns (even if the underlying Python implementation's garbage - collector doesn't).""" - fh = open(filename, mode) - try: - fh.write(data) - finally: - fh.close() - -# For backwards-compatibility in case someone is using these names. We used to -# have a superkludge in fileutil.py under these names. -def rename(src, dst, tries=4, basedelay=0.1): - return os.rename(src, dst) - -def remove(f, tries=4, basedelay=0.1): - return os.remove(f) - -def rmdir(f, tries=4, basedelay=0.1): - return os.rmdir(f) - -class _Dir(object): - """ - Hold a set of files and subdirs and clean them all up when asked to. - """ - def __init__(self, name, cleanup=True): - self.name = name - self.cleanup = cleanup - self.files = [] - self.subdirs = set() - - def file(self, fname, mode=None): - """ - Create a file in the tempdir and remember it so as to close() it - before attempting to cleanup the temp dir. - - @rtype: file - """ - ffn = os.path.join(self.name, fname) - if mode is not None: - fo = open(ffn, mode) - else: - fo = open(ffn) - self.register_file(fo) - return fo - - def subdir(self, dirname): - """ - Create a subdirectory in the tempdir and remember it so as to call - shutdown() on it before attempting to clean up. - - @rtype: _Dir instance - """ - ffn = os.path.join(self.name, dirname) - sd = _Dir(ffn, self.cleanup) - self.register_subdir(sd) - make_dirs(sd.name) - return sd - - def register_file(self, fileobj): - """ - Remember the file object and call close() on it before attempting to - clean up. - """ - self.files.append(fileobj) - - def register_subdir(self, dirobj): - """ - Remember the _Dir object and call shutdown() on it before attempting - to clean up. - """ - self.subdirs.add(dirobj) - - def shutdown(self): - if self.cleanup: - for subdir in hasattr(self, 'subdirs') and self.subdirs or []: - subdir.shutdown() - for fileobj in hasattr(self, 'files') and self.files or []: - if DBNoSuchFileError is None: - fileobj.close() # "close()" is idempotent so we don't need to catch exceptions here - else: - try: - fileobj.close() - except DBNoSuchFileError: - # Ah, except that the bsddb module's file-like object (a DB object) has a non-idempotent close... - pass - - if hasattr(self, 'name'): - rm_dir(self.name) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - try: - self.shutdown() - except: - import traceback - traceback.print_exc() - -class NamedTemporaryDirectory(_Dir): - """ - Call tempfile.mkdtemp(), store the name of the dir in self.name, and - rm_dir() when it gets garbage collected or "shutdown()". - - Also keep track of file objects for files within the tempdir and call - close() on them before rm_dir(). This is a convenient way to open temp - files within the directory, and it is very helpful on Windows because you - can't delete a directory which contains a file which is currently open. - """ - - def __init__(self, cleanup=True, *args, **kwargs): - """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """ - name = tempfile.mkdtemp(*args, **kwargs) - _Dir.__init__(self, name, cleanup) - -class ReopenableNamedTemporaryFile: - """ - This uses tempfile.mkstemp() to generate a secure temp file. It then closes - the file, leaving a zero-length file as a placeholder. You can get the - filename with ReopenableNamedTemporaryFile.name. When the - ReopenableNamedTemporaryFile instance is garbage collected or its shutdown() - method is called, it deletes the file. - """ - def __init__(self, *args, **kwargs): - fd, self.name = tempfile.mkstemp(*args, **kwargs) - os.close(fd) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - self.shutdown() - - def shutdown(self): - remove(self.name) - -def make_dirs(dirname, mode=0777): - """ - An idempotent version of os.makedirs(). If the dir already exists, do - nothing and return without raising an exception. If this call creates the - dir, return without raising an exception. If there is an error that - prevents creation or if the directory gets deleted after make_dirs() creates - it and before make_dirs() checks that it exists, raise an exception. - """ - tx = None - try: - os.makedirs(dirname, mode) - except OSError, x: - tx = x - - if not os.path.isdir(dirname): - if tx: - raise tx - raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning... - -def rmtree(dirname): - """ - A threadsafe and idempotent version of shutil.rmtree(). If the dir is - already gone, do nothing and return without raising an exception. If this - call removes the dir, return without raising an exception. If there is an - error that prevents deletion or if the directory gets created again after - rm_dir() deletes it and before rm_dir() checks that it is gone, raise an - exception. - """ - excs = [] - try: - os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) - for f in os.listdir(dirname): - fullname = os.path.join(dirname, f) - if os.path.isdir(fullname): - rm_dir(fullname) - else: - remove(fullname) - os.rmdir(dirname) - except EnvironmentError, le: - # Ignore "No such file or directory", collect any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - excs.append(le) - except Exception, le: - excs.append(le) - - # Okay, now we've recursively removed everything, ignoring any "No - # such file or directory" errors, and collecting any other errors. - - if os.path.exists(dirname): - if len(excs) == 1: - raise excs[0] - if len(excs) == 0: - raise OSError, "Failed to remove dir for unknown reason." - raise OSError, excs - -def rm_dir(dirname): - # Renamed to be like shutil.rmtree and unlike rmdir. - return rmtree(dirname) - -def remove_if_possible(f): - try: - remove(f) - except EnvironmentError: - pass - -def remove_if_present(f): - try: - remove(f) - except EnvironmentError, le: - # Ignore "No such file or directory", re-raise any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - raise - -def rmdir_if_possible(f): - try: - rmdir(f) - except EnvironmentError: - pass - -def open_or_create(fname, binarymode=True): - try: - f = open(fname, binarymode and "r+b" or "r+") - except EnvironmentError: - f = open(fname, binarymode and "w+b" or "w+") - return f - -def du(basedir): - size = 0 - - for root, dirs, files in os.walk(basedir): - for f in files: - fn = os.path.join(root, f) - size += os.path.getsize(fn) - - return size diff --git a/libs/pyutil/iputil.py b/libs/pyutil/iputil.py index b8b96362..cb3e7c02 100644 --- a/libs/pyutil/iputil.py +++ b/libs/pyutil/iputil.py @@ -1,22 +1,12 @@ -# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) -# portions adapted from nattraverso.ipdiscover -# portions authored by Brian Warner, working for Allmydata -# most recent version authored by Zooko O'Whielacronx, working for Allmydata - # from the Python Standard Library -import os, re, socket, sys +import os, re, socket, sys, subprocess # from Twisted -from twisted.internet import defer, reactor -from twisted.python import failure +from twisted.internet import defer, threads, reactor from twisted.internet.protocol import DatagramProtocol -from twisted.internet.utils import getProcessOutput from twisted.python.procutils import which from twisted.python import log -# from pyutil -import observer - try: import resource def increase_rlimits(): @@ -77,6 +67,7 @@ except ImportError: # since one might be shadowing the other. This hack appeases pyflakes. increase_rlimits = _increase_rlimits + def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET """ Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad @@ -121,14 +112,16 @@ def get_local_ip_for(target): except socket.gaierror: # DNS isn't running, or somehow we encountered an error - # note: if an interface is configured and up, but nothing is connected to it, - # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror - # . This is synchronous and occurs for each node being started, so users of certain unit - # tests will see something like 120s of delay, which may be enough to hit the default - # trial timeouts. For that reason, get_local_addresses_async() was changed to default to - # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also - # makes node startup a tad faster. - + # note: if an interface is configured and up, but nothing is + # connected to it, gethostbyname("A.ROOT-SERVERS.NET") will take 20 + # seconds to raise socket.gaierror . This is synchronous and occurs + # for each node being started, so users of + # test.common.SystemTestMixin (like test_system) will see something + # like 120s of delay, which may be enough to hit the default trial + # timeouts. For that reason, get_local_addresses_async() was changed + # to default to the numerical ip address for A.ROOT-SERVERS.NET, to + # avoid this DNS lookup. This also makes node startup fractionally + # faster. return None udpprot = DatagramProtocol() port = reactor.listenUDP(0, udpprot) @@ -146,16 +139,29 @@ _platform_map = { "linux-i386": "linux", # redhat "linux-ppc": "linux", # redhat "linux2": "linux", # debian + "linux3": "linux", # debian "win32": "win32", "irix6-n32": "irix", "irix6-n64": "irix", "irix6": "irix", "openbsd2": "bsd", + "openbsd3": "bsd", + "openbsd4": "bsd", + "openbsd5": "bsd", "darwin": "bsd", # Mac OS X "freebsd4": "bsd", "freebsd5": "bsd", "freebsd6": "bsd", + "freebsd7": "bsd", + "freebsd8": "bsd", + "freebsd9": "bsd", "netbsd1": "bsd", + "netbsd2": "bsd", + "netbsd3": "bsd", + "netbsd4": "bsd", + "netbsd5": "bsd", + "netbsd6": "bsd", + "dragonfly2": "bsd", "sunos5": "sunos", "cygwin": "cygwin", } @@ -173,12 +179,12 @@ _win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\ # These work in Redhat 6.x and Debian 2.2 potato _linux_path = '/sbin/ifconfig' -_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +_linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) -# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD -_bsd_path = '/sbin/ifconfig' -_bsd_args = ('-a',) -_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +# NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X +_netbsd_path = '/sbin/ifconfig' +_netbsd_args = ('-a',) +_netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) # Irix 6.5 _irix_path = '/usr/etc/ifconfig' @@ -186,39 +192,6 @@ _irix_path = '/usr/etc/ifconfig' # Solaris 2.x _sunos_path = '/usr/sbin/ifconfig' -class SequentialTrier(object): - """ I hold a list of executables to try and try each one in turn - until one gives me a list of IP addresses.""" - - def __init__(self, exebasename, args, regex): - assert not os.path.isabs(exebasename) - self.exes_left_to_try = which(exebasename) - self.exes_left_to_try.reverse() - self.args = args - self.regex = regex - self.o = observer.OneShotObserverList() - self._try_next() - - def _try_next(self): - if not self.exes_left_to_try: - self.o.fire(None) - else: - exe = self.exes_left_to_try.pop() - d2 = _query(exe, self.args, self.regex) - - def cb(res): - if res: - self.o.fire(res) - else: - self._try_next() - - def eb(why): - self._try_next() - - d2.addCallbacks(cb, eb) - - def when_tried(self): - return self.o.when_fired() # k: platform string as provided in the value of _platform_map # v: tuple of (path_to_tool, args, regex,) @@ -226,19 +199,22 @@ _tool_map = { "linux": (_linux_path, (), _linux_re,), "win32": (_win32_path, _win32_args, _win32_re,), "cygwin": (_win32_path, _win32_args, _win32_re,), - "bsd": (_bsd_path, _bsd_args, _bsd_re,), - "irix": (_irix_path, _bsd_args, _bsd_re,), - "sunos": (_sunos_path, _bsd_args, _bsd_re,), + "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,), + "irix": (_irix_path, _netbsd_args, _netbsd_re,), + "sunos": (_sunos_path, _netbsd_args, _netbsd_re,), } + def _find_addresses_via_config(): - # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + return threads.deferToThread(_synchronously_find_addresses_via_config) + +def _synchronously_find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian's API platform = _platform_map.get(sys.platform) - (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + if not platform: + raise UnsupportedPlatformError(sys.platform) - # If the platform isn't known then we attempt BSD-style ifconfig. If it - # turns out that we don't get anything resembling a dotted quad IPv4 address - # out of it, then we'll raise UnsupportedPlatformError. + (pathtotool, args, regex,) = _tool_map[platform] # If pathtotool is a fully qualified path then we just try that. # If it is merely an executable name then we use Twisted's @@ -246,34 +222,33 @@ def _find_addresses_via_config(): # gives us something that resembles a dotted-quad IPv4 address. if os.path.isabs(pathtotool): - d = _query(pathtotool, args, regex) + return _query(pathtotool, args, regex) else: - d = SequentialTrier(pathtotool, args, regex).when_tried() - - d.addCallback(_check_result) - return d - -def _check_result(result): - if not result and not _platform_map.has_key(sys.platform): - return failure.Failure(UnsupportedPlatformError(sys.platform)) - else: - return result + exes_to_try = which(pathtotool) + for exe in exes_to_try: + try: + addresses = _query(exe, args, regex) + except Exception: + addresses = [] + if addresses: + return addresses + return [] def _query(path, args, regex): - d = getProcessOutput(path, args) - def _parse(output): - addresses = [] - outputsplit = output.split('\n') - for outline in outputsplit: - m = regex.match(outline) - if m: - addr = m.groupdict()['address'] - if addr not in addresses: - addresses.append(addr) + env = {'LANG': 'en_US.UTF-8'} + p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + (output, err) = p.communicate() - return addresses - d.addCallback(_parse) - return d + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses def _cygwin_hack_find_addresses(target): addresses = [] diff --git a/libs/pyutil/iputil.py~ b/libs/pyutil/iputil.py~ new file mode 100644 index 00000000..b8b96362 --- /dev/null +++ b/libs/pyutil/iputil.py~ @@ -0,0 +1,288 @@ +# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) +# portions adapted from nattraverso.ipdiscover +# portions authored by Brian Warner, working for Allmydata +# most recent version authored by Zooko O'Whielacronx, working for Allmydata + +# from the Python Standard Library +import os, re, socket, sys + +# from Twisted +from twisted.internet import defer, reactor +from twisted.python import failure +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.utils import getProcessOutput +from twisted.python.procutils import which +from twisted.python import log + +# from pyutil +import observer + +try: + import resource + def increase_rlimits(): + # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain + # systems (OS-X, probably solaris) start with a relatively low limit + # (256), and some unit tests want to open up more sockets than this. + # Most linux systems start with both hard and soft limits at 1024, + # which is plenty. + + # unfortunately the values to pass to setrlimit() vary widely from + # one system to another. OS-X reports (256, HUGE), but the real hard + # limit is 10240, and accepts (-1,-1) to mean raise it to the + # maximum. Cygwin reports (256, -1), then ignores a request of + # (-1,-1): instead you have to guess at the hard limit (it appears to + # be 3200), so using (3200,-1) seems to work. Linux reports a + # sensible (1024,1024), then rejects (-1,-1) as trying to raise the + # maximum limit, so you could set it to (1024,1024) but you might as + # well leave it alone. + + try: + current = resource.getrlimit(resource.RLIMIT_NOFILE) + except AttributeError: + # we're probably missing RLIMIT_NOFILE + return + + if current[0] >= 1024: + # good enough, leave it alone + return + + try: + if current[1] > 0 and current[1] < 1000000: + # solaris reports (256, 65536) + resource.setrlimit(resource.RLIMIT_NOFILE, + (current[1], current[1])) + else: + # this one works on OS-X (bsd), and gives us 10240, but + # it doesn't work on linux (on which both the hard and + # soft limits are set to 1024 by default). + resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1)) + new = resource.getrlimit(resource.RLIMIT_NOFILE) + if new[0] == current[0]: + # probably cygwin, which ignores -1. Use a real value. + resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1)) + + except ValueError: + log.msg("unable to set RLIMIT_NOFILE: current value %s" + % (resource.getrlimit(resource.RLIMIT_NOFILE),)) + except: + # who knows what. It isn't very important, so log it and continue + log.err() +except ImportError: + def _increase_rlimits(): + # TODO: implement this for Windows. Although I suspect the + # solution might be "be running under the iocp reactor and + # make this function be a no-op". + pass + # pyflakes complains about two 'def FOO' statements in the same time, + # since one might be shadowing the other. This hack appeases pyflakes. + increase_rlimits = _increase_rlimits + +def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET + """ + Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad + strings) that are currently configured on this host, sorted in descending + order of how likely we think they are to work. + + @param target: we want to learn an IP address they could try using to + connect to us; The default value is fine, but it might help if you + pass the address of a host that you are actually trying to be + reachable to. + """ + addresses = [] + local_ip = get_local_ip_for(target) + if local_ip: + addresses.append(local_ip) + + if sys.platform == "cygwin": + d = _cygwin_hack_find_addresses(target) + else: + d = _find_addresses_via_config() + + def _collect(res): + for addr in res: + if addr != "0.0.0.0" and not addr in addresses: + addresses.append(addr) + return addresses + d.addCallback(_collect) + + return d + +def get_local_ip_for(target): + """Find out what our IP address is for use by a given target. + + @return: the IP address as a dotted-quad string which could be used by + to connect to us. It might work for them, it might not. If + there is no suitable address (perhaps we don't currently have an + externally-visible interface), this will return None. + """ + + try: + target_ipaddr = socket.gethostbyname(target) + except socket.gaierror: + # DNS isn't running, or somehow we encountered an error + + # note: if an interface is configured and up, but nothing is connected to it, + # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror + # . This is synchronous and occurs for each node being started, so users of certain unit + # tests will see something like 120s of delay, which may be enough to hit the default + # trial timeouts. For that reason, get_local_addresses_async() was changed to default to + # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also + # makes node startup a tad faster. + + return None + udpprot = DatagramProtocol() + port = reactor.listenUDP(0, udpprot) + try: + udpprot.transport.connect(target_ipaddr, 7) + localip = udpprot.transport.getHost().host + except socket.error: + # no route to that host + localip = None + port.stopListening() # note, this returns a Deferred + return localip + +# k: result of sys.platform, v: which kind of IP configuration reader we use +_platform_map = { + "linux-i386": "linux", # redhat + "linux-ppc": "linux", # redhat + "linux2": "linux", # debian + "win32": "win32", + "irix6-n32": "irix", + "irix6-n64": "irix", + "irix6": "irix", + "openbsd2": "bsd", + "darwin": "bsd", # Mac OS X + "freebsd4": "bsd", + "freebsd5": "bsd", + "freebsd6": "bsd", + "netbsd1": "bsd", + "sunos5": "sunos", + "cygwin": "cygwin", + } + +class UnsupportedPlatformError(Exception): + pass + +# Wow, I'm really amazed at home much mileage we've gotten out of calling +# the external route.exe program on windows... It appears to work on all +# versions so far. Still, the real system calls would much be preferred... +# ... thus wrote Greg Smith in time immemorial... +_win32_path = 'route.exe' +_win32_args = ('print',) +_win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\d+)\s+(?P\d+)\s*$', flags=re.M|re.I|re.S) + +# These work in Redhat 6.x and Debian 2.2 potato +_linux_path = '/sbin/ifconfig' +_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD +_bsd_path = '/sbin/ifconfig' +_bsd_args = ('-a',) +_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# Irix 6.5 +_irix_path = '/usr/etc/ifconfig' + +# Solaris 2.x +_sunos_path = '/usr/sbin/ifconfig' + +class SequentialTrier(object): + """ I hold a list of executables to try and try each one in turn + until one gives me a list of IP addresses.""" + + def __init__(self, exebasename, args, regex): + assert not os.path.isabs(exebasename) + self.exes_left_to_try = which(exebasename) + self.exes_left_to_try.reverse() + self.args = args + self.regex = regex + self.o = observer.OneShotObserverList() + self._try_next() + + def _try_next(self): + if not self.exes_left_to_try: + self.o.fire(None) + else: + exe = self.exes_left_to_try.pop() + d2 = _query(exe, self.args, self.regex) + + def cb(res): + if res: + self.o.fire(res) + else: + self._try_next() + + def eb(why): + self._try_next() + + d2.addCallbacks(cb, eb) + + def when_tried(self): + return self.o.when_fired() + +# k: platform string as provided in the value of _platform_map +# v: tuple of (path_to_tool, args, regex,) +_tool_map = { + "linux": (_linux_path, (), _linux_re,), + "win32": (_win32_path, _win32_args, _win32_re,), + "cygwin": (_win32_path, _win32_args, _win32_re,), + "bsd": (_bsd_path, _bsd_args, _bsd_re,), + "irix": (_irix_path, _bsd_args, _bsd_re,), + "sunos": (_sunos_path, _bsd_args, _bsd_re,), + } +def _find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + + platform = _platform_map.get(sys.platform) + (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + + # If the platform isn't known then we attempt BSD-style ifconfig. If it + # turns out that we don't get anything resembling a dotted quad IPv4 address + # out of it, then we'll raise UnsupportedPlatformError. + + # If pathtotool is a fully qualified path then we just try that. + # If it is merely an executable name then we use Twisted's + # "which()" utility and try each executable in turn until one + # gives us something that resembles a dotted-quad IPv4 address. + + if os.path.isabs(pathtotool): + d = _query(pathtotool, args, regex) + else: + d = SequentialTrier(pathtotool, args, regex).when_tried() + + d.addCallback(_check_result) + return d + +def _check_result(result): + if not result and not _platform_map.has_key(sys.platform): + return failure.Failure(UnsupportedPlatformError(sys.platform)) + else: + return result + +def _query(path, args, regex): + d = getProcessOutput(path, args) + def _parse(output): + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses + d.addCallback(_parse) + return d + +def _cygwin_hack_find_addresses(target): + addresses = [] + for h in [target, "localhost", "127.0.0.1",]: + try: + addr = get_local_ip_for(h) + if addr not in addresses: + addresses.append(addr) + except socket.gaierror: + pass + + return defer.succeed(addresses) diff --git a/libs/pyutil/mathutil.py b/libs/pyutil/mathutil.py index 46781b0c..9c169801 100644 --- a/libs/pyutil/mathutil.py +++ b/libs/pyutil/mathutil.py @@ -11,7 +11,7 @@ def div_ceil(n, d): """ The smallest integer k such that k*d >= n. """ - return (n/d) + (n%d != 0) + return int((n//d) + (n%d != 0)) def next_multiple(n, k): """ diff --git a/libs/pyutil/mathutil.py~ b/libs/pyutil/mathutil.py~ new file mode 100644 index 00000000..46781b0c --- /dev/null +++ b/libs/pyutil/mathutil.py~ @@ -0,0 +1,106 @@ +# Copyright (c) 2005-2010 Zooko Wilcox-O'Hearn +# This file is part of pyutil; see README.rst for licensing terms. + +""" +A few commonly needed functions. +""" + +import math + +def div_ceil(n, d): + """ + The smallest integer k such that k*d >= n. + """ + return (n/d) + (n%d != 0) + +def next_multiple(n, k): + """ + The smallest multiple of k which is >= n. Note that if n is 0 then the + answer is 0. + """ + return div_ceil(n, k) * k + +def pad_size(n, k): + """ + The smallest number that has to be added to n to equal a multiple of k. + """ + if n%k: + return k - n%k + else: + return 0 + +def is_power_of_k(n, k): + return k**int(math.log(n, k) + 0.5) == n + +def next_power_of_k(n, k): + p = 1 + while p < n: + p *= k + return p + +def ave(l): + return sum(l) / len(l) + +def log_ceil(n, b): + """ + The smallest integer k such that b^k >= n. + + log_ceil(n, 2) is the number of bits needed to store any of n values, e.g. + the number of bits needed to store any of 128 possible values is 7. + """ + p = 1 + k = 0 + while p < n: + p *= b + k += 1 + return k + +def log_floor(n, b): + """ + The largest integer k such that b^k <= n. + """ + p = 1 + k = 0 + while p <= n: + p *= b + k += 1 + return k - 1 + +def linear_fit_slope(ps): + """ + Single-independent-variable linear regression -- least squares method. + + At least, I *think* this function computes that answer. I no longer + remember where I learned this trick and at the moment I can't prove to + myself that this is correct. + + @param ps a sequence of tuples of (x, y) + """ + avex = ave([x for (x, y) in ps]) + avey = ave([y for (x, y) in ps]) + sxy = sum([ (x - avex) * (y - avey) for (x, y) in ps ]) + sxx = sum([ (x - avex) ** 2 for (x, y) in ps ]) + if sxx == 0: + return None + return sxy / sxx + +def permute(l): + """ + Return all possible permutations of l. + + @type l: sequence + @rtype a set of sequences + """ + if len(l) == 1: + return [l,] + + res = [] + for i in range(len(l)): + l2 = list(l[:]) + x = l2.pop(i) + for l3 in permute(l2): + l3.append(x) + res.append(l3) + + return res + diff --git a/libs/pyutil/odict.py~ b/libs/pyutil/odict.py~ deleted file mode 100644 index 0ed5ce7b..00000000 --- a/libs/pyutil/odict.py~ +++ /dev/null @@ -1,552 +0,0 @@ -# Copyright (c) 2002-2009 Zooko "Zooko" Wilcox-O'Hearn - -""" -This module offers a Ordered Dict, which is a dict that preserves -insertion order. See PEP 372 for description of the problem. This -implementation uses a linked-list to get good O(1) asymptotic -performance. (Actually it is O(hashtable-update-cost), but whatever.) - -Warning: if -O optimizations are not turned on then OrderedDict performs -extensive self-analysis in every function call, which can take minutes -and minutes for a large cache. Turn on -O, or comment out assert -self._assert_invariants() -""" - -import operator - -from assertutil import _assert, precondition -from humanreadable import hr - -class OrderedDict: - """ - An efficient ordered dict. - - Adding an item that is already in the dict *does not* make it the - most- recently-added item although it may change the state of the - dict itself (if the value is different than the previous value). - - See also SmallOrderedDict (below), which is faster in some cases. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return (k, v,) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return k - - class ValIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - precondition(self.c.d.has_key(self.i), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - (v, p, n,) = self.c.d[self.i] - self.i = p - return v - - class Sentinel: - def __init__(self, msg): - self.msg = msg - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.msg,) - - def __init__(self, initialdata={}): - self.d = {} # k: k, v: [v, prev, next,] # the dict - self.hs = OrderedDict.Sentinel("hs") - self.ts = OrderedDict.Sentinel("ts") - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.update(initialdata) - - assert self._assert_invariants() - - def __repr_n__(self, n=None): - s = ["{",] - try: - iter = self.iteritems() - x = iter.next() - s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - i = 1 - while (n is None) or (i < n): - x = iter.next() - s.append(", "); s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - except StopIteration: - pass - s.append("}") - return ''.join(s) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(),) - - def __str__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(16),) - - def _assert_invariants(self): - _assert((len(self.d) > 2) == (self.d[self.hs][2] is not self.ts) == (self.d[self.ts][1] is not self.hs), "Head and tail point to something other than each other if and only if there is at least one element in the dictionary.", self.hs, self.ts, len(self.d)) - foundprevsentinel = 0 - foundnextsentinel = 0 - for (k, (v, p, n,)) in self.d.iteritems(): - _assert(v not in (self.hs, self.ts,)) - _assert(p is not self.ts, "A reference to the tail sentinel may not appear in prev.", k, v, p, n) - _assert(n is not self.hs, "A reference to the head sentinel may not appear in next.", k, v, p, n) - _assert(p in self.d, "Each prev is required to appear as a key in the dict.", k, v, p, n) - _assert(n in self.d, "Each next is required to appear as a key in the dict.", k, v, p, n) - if p is self.hs: - foundprevsentinel += 1 - _assert(foundprevsentinel <= 2, "No more than two references to the head sentinel may appear as a prev.", k, v, p, n) - if n is self.ts: - foundnextsentinel += 1 - _assert(foundnextsentinel <= 2, "No more than one reference to the tail sentinel may appear as a next.", k, v, p, n) - _assert(foundprevsentinel == 2, "A reference to the head sentinel is required appear as a prev (plus a self-referential reference).") - _assert(foundnextsentinel == 2, "A reference to the tail sentinel is required appear as a next (plus a self-referential reference).") - - count = 0 - for (k, v,) in self.iteritems(): - _assert(k not in (self.hs, self.ts,), k, self.hs, self.ts) - count += 1 - _assert(count == len(self.d)-2, count, len(self.d)) # -2 for the sentinels - - return True - - def move_to_most_recent(self, k, strictkey=False): - assert self._assert_invariants() - - if not self.d.has_key(k): - if strictkey: - raise KeyError, k - return - - node = self.d[k] - - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - - # move to front - hnode = self.d[self.hs] - - node[1] = self.hs - node[2] = hnode[2] - hnode[2] = k - self.d[node[2]][1] = k - - assert self._assert_invariants() - - def iteritems(self): - return OrderedDict.ItemIterator(self) - - def itervalues(self): - return OrderedDict.ValIterator(self) - - def iterkeys(self): - return self.__iter__() - - def __iter__(self): - return OrderedDict.KeyIterator(self) - - def __getitem__(self, key, default=None, strictkey=True): - node = self.d.get(key) - if not node: - if strictkey: - raise KeyError, key - return default - return node[0] - - def __setitem__(self, k, v=None): - assert self._assert_invariants() - - node = self.d.get(k) - if node: - node[0] = v - return - - hnode = self.d[self.hs] - n = hnode[2] - self.d[k] = [v, self.hs, n,] - hnode[2] = k - self.d[n][1] = k - - assert self._assert_invariants() - return v - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the value removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if self.d.has_key(key): - node = self.d[key] - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - del self.d[key] - assert self._assert_invariants() - return node[0] - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def has_key(self, key): - assert self._assert_invariants() - if self.d.has_key(key): - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def clear(self): - assert self._assert_invariants() - self.d.clear() - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - - for (k, v,) in otherdict.iteritems(): - assert self._assert_invariants() - self[k] = v - assert self._assert_invariants() - - def pop(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - self.remove(k) - assert self._assert_invariants() - return k - - def popitem(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - val = self.remove(k) - assert self._assert_invariants() - return (k, val,) - - def keys_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return t.keys() - - def keys(self): - res = [None] * len(self) - i = 0 - for k in self.iterkeys(): - res[i] = k - i += 1 - return res - - def values_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return map(operator.__getitem__, t.values(), [0]*len(t)) - - def values(self): - res = [None] * len(self) - i = 0 - for v in self.itervalues(): - res[i] = v - i += 1 - return res - - def items(self): - res = [None] * len(self) - i = 0 - for it in self.iteritems(): - res[i] = it - i += 1 - return res - - def __len__(self): - return len(self.d) - 2 - - def insert(self, key, val=None): - assert self._assert_invariants() - result = self.__setitem__(key, val) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def get(self, key, default=None): - return self.__getitem__(key, default, strictkey=False) - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - -class SmallOrderedDict(dict): - """ - SmallOrderedDict is faster than OrderedDict for small sets. How small? That - depends on your machine and which operations you use most often. Use - performance profiling to determine whether the cache class that you are - using makes any difference to the performance of your program, and if it - does, then run "quick_bench()" in test/test_cache.py to see which cache - implementation is faster for the size of your datasets. - - A simple least-recently-used cache. It keeps an LRU queue, and - when the number of items in the cache reaches maxsize, it removes - the least recently used item. - - "Looking" at an item or a key such as with "has_key()" makes that - item become the most recently used item. - - You can also use "refresh()" to explicitly make an item become the most - recently used item. - - Adding an item that is already in the dict *does* make it the - most- recently-used item although it does not change the state of - the dict itself. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return (k, dict.__getitem__(self.c, k),) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return k - - class ValueIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return dict.__getitem__(self.c, k) - - def __init__(self, initialdata={}, maxsize=128): - dict.__init__(self, initialdata) - self._lru = initialdata.keys() # contains keys - self._maxsize = maxsize - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - assert self._assert_invariants() - - def _assert_invariants(self): - _assert(len(self._lru) <= self._maxsize, "Size is required to be <= maxsize.") - _assert(len(filter(lambda x: dict.has_key(self, x), self._lru)) == len(self._lru), "Each key in self._lru is required to be in dict.", filter(lambda x: not dict.has_key(self, x), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(filter(lambda x: x in self._lru, self.keys())) == len(self), "Each key in dict is required to be in self._lru.", filter(lambda x: x not in self._lru, self.keys()), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) == len(self), "internal consistency", filter(lambda x: x not in self.keys(), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) <= self._maxsize, "internal consistency", len(self._lru), self._lru, self._maxsize) - return True - - def insert(self, key, item=None): - assert self._assert_invariants() - result = self.__setitem__(key, item) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def __setitem__(self, key, item=None): - assert self._assert_invariants() - if dict.has_key(self, key): - self._lru.remove(key) - else: - if len(self._lru) == self._maxsize: - # If this insert is going to increase the size of the cache to bigger than maxsize: - killkey = self._lru.pop(0) - dict.__delitem__(self, killkey) - dict.__setitem__(self, key, item) - self._lru.append(key) - assert self._assert_invariants() - return item - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the object removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if dict.has_key(self, key): - val = dict.__getitem__(self, key) - dict.__delitem__(self, key) - self._lru.remove(key) - assert self._assert_invariants() - return val - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def clear(self): - assert self._assert_invariants() - dict.clear(self) - self._lru = [] - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - if len(otherdict) > self._maxsize: - # Handling this special case here makes it possible to implement the - # other more common cases faster below. - dict.clear(self) - self._lru = [] - if self._maxsize > (len(otherdict) - self._maxsize): - dict.update(self, otherdict) - while len(self) > self._maxsize: - dict.popitem(self) - else: - for k, v, in otherdict.iteritems(): - if len(self) == self._maxsize: - break - dict.__setitem__(self, k, v) - self._lru = dict.keys(self) - assert self._assert_invariants() - return self - - for k in otherdict.iterkeys(): - if dict.has_key(self, k): - self._lru.remove(k) - self._lru.extend(otherdict.keys()) - dict.update(self, otherdict) - - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - - assert self._assert_invariants() - return self - - def has_key(self, key): - assert self._assert_invariants() - if dict.has_key(self, key): - assert key in self._lru, "key: %s, self._lru: %s" % tuple(map(hr, (key, self._lru,))) - self._lru.remove(key) - self._lru.append(key) - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def refresh(self, key, strictkey=True): - """ - @param strictkey: raise a KeyError exception if key isn't present - """ - assert self._assert_invariants() - if not dict.has_key(self, key): - if strictkey: - raise KeyError, key - return - self._lru.remove(key) - self._lru.append(key) - - def popitem(self): - if not self._lru: - raise KeyError, 'popitem(): dictionary is empty' - k = self._lru[-1] - obj = self.remove(k) - return (k, obj,) diff --git a/libs/pyutil/randutil.py b/libs/pyutil/randutil.py index a3efb74f..82eb3e18 100644 --- a/libs/pyutil/randutil.py +++ b/libs/pyutil/randutil.py @@ -80,6 +80,5 @@ seed = randobj.seed def randstr(n): return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) -import random as insecurerandom def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) + return os.urandom(n) diff --git a/libs/pyutil/randutil.py~ b/libs/pyutil/randutil.py~ deleted file mode 100644 index b0f1c4f9..00000000 --- a/libs/pyutil/randutil.py~ +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -import warnings -import os, random - -try: - import hashexpand - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=None, deterministic=True): - warnings.warn("deprecated", DeprecationWarning) - if not deterministic: - raise NotImplementedError, "SHA256Expander is always deterministic. For non-deterministic, try urandomRandom." - - hashexpand.SHA256Expander.__init__(self) - random.Random.__init__(self, seed) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - import increasing_timer - seed = repr(increasing_timer.time()) - hashexpand.SHA256Expander.seed(self, seed) - - - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=""): - warnings.warn("deprecated", DeprecationWarning) - hashexpand.SHA256Expander.__init__(self) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - seed = os.urandom(32) - hashexpand.SHA256Expander.seed(self, seed) -except ImportError, le: - class InsecureSHA256Random: - def __init__(self, seed=None): - raise ImportError, le - class SHA256Random: - def __init__(self, seed=""): - raise ImportError, le - -class devrandomRandom(random.Random): - """ The problem with using this one, of course, is that it blocks. This - is, of course, a security flaw. (On Linux and probably on other - systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def __init__(self): - warnings.warn("deprecated", DeprecationWarning) - self.dr = open("/dev/random", "r") - - def get(self, bytes): - return self.dr.read(bytes) - - -class devurandomRandom(random.Random): - """ The problem with using this one is that it gives answers even when it - has never been properly seeded, e.g. when you are booting from CD and have - just started up and haven't yet gathered enough entropy to actually be - unguessable. (On Linux and probably on other systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def get(self, bytes): - warnings.warn("deprecated", DeprecationWarning) - return os.urandom(bytes) - - -randobj = devurandomRandom() -get = randobj.get -random = randobj.random -randrange = randobj.randrange -shuffle = randobj.shuffle -choice = randobj.choice -seed = randobj.seed - -def randstr(n): - return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) - -import random as insecurerandom -def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) diff --git a/libs/pyutil/scripts/passphrase.py b/libs/pyutil/scripts/passphrase.py new file mode 100644 index 00000000..bed79c13 --- /dev/null +++ b/libs/pyutil/scripts/passphrase.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse, math, random + +from pyutil.mathutil import div_ceil + +from pkg_resources import resource_stream + +def recursive_subset_sum(entropy_needed, wordlists): + # Pick a minimalish set of numbers which sum to at least + # entropy_needed. + + # Okay now what's the smallest number of words which will give us + # at least this much entropy? + entropy_of_biggest_wordlist = wordlists[-1][0] + assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1] + needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist) + # How much entropy do we need from each word? + needed_entropy_per_word = entropy_needed / needed_words + # What's the smallest wordlist that offers at least this much + # entropy per word? + for (wlentropy, wl) in wordlists: + if wlentropy >= needed_entropy_per_word: + break + assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word) + + result = [(wlentropy, wl)] + # If we need more, recurse... + if wlentropy < entropy_needed: + rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists) + result.extend(rest) + return result + +def gen_passphrase(entropy, allwords): + maxlenwords = [] + i = 2 # The smallest set is words of length 1 or 2. + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + while len(maxlenwords[-1][1]) < len(allwords): + i += 1 + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + + sr = random.SystemRandom() + passphrase = [] + + wordlists_to_use = recursive_subset_sum(entropy, maxlenwords) + + passphraseentropy = 0.0 + for (wle, wl) in wordlists_to_use: + passphrase.append(sr.choice(wl)) + passphraseentropy += wle + + return (u".".join(passphrase), passphraseentropy) + +def main(): + parser = argparse.ArgumentParser(prog="chbs", description="Create a random passphrase by picking a few random words.") + + parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use chbs's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT") + parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS") + args = parser.parse_args() + + dicti = args.dictionary + if not dicti: + dicti = resource_stream('pyutil', 'data/wordlist.txt') + allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()]) + + passphrase, bits = gen_passphrase(args.bits, allwords) + + print u"Your new password is: '%s'. It is worth about %s bits." % (passphrase, bits) diff --git a/libs/pyutil/scripts/time_comparisons.py b/libs/pyutil/scripts/time_comparisons.py new file mode 100644 index 00000000..15a38852 --- /dev/null +++ b/libs/pyutil/scripts/time_comparisons.py @@ -0,0 +1,209 @@ +# If you run this file, it will make up a random secret and then crack it +# using timing information from a string comparison function. Maybe--if it +# gets lucky. It takes a long, long time to work. + +# So, the thing I need help with is statistics. The way this thing works is +# extremely stupid. Suppose you want to know which function invocation takes +# longer: comparison(secret, guess1) or comparison(secret, guess2)? + +# If you can correctly determine that one of them takes longer than the +# other, then (a) you can use that to crack the secret, and (b) this is a +# unit test demonstrating that comparison() is not timing-safe. + +# So how does this script do it? Extremely stupidly. First of all, you can't +# reliably measure tiny times, so to measure the time that a function takes, +# we run that function 10,000 times in a row, measure how long that took, and +# divide by 10,000 to estimate how long any one run would have taken. + +# Then, we do that 100 times in a row, and take the fastest of 100 runs. (I +# also experimented with taking the mean of 100 runs instead of the fastest.) + +# Then, we just say whichever comparison took longer (for its fastest run of +# 100 runs of 10,000 executions per run) is the one we think is a closer +# guess to the secret. + +# Now I would *like* to think that there is some kind of statistical analysis +# more sophisticated than "take the slowest of the fastest of 100 runs of +# 10,000 executions". Such improved statistical analysis would hopefully be +# able to answer these two questions: + +# 1. Are these two function calls -- comparison(secret, guess1) and +# comparison(secret, guess2) -- drawing from the same distribution or +# different? If you can answer that question, then you've answered the +# question of whether "comparison" is timing-safe or not. + +# And, this would also allow the cracker to recover from a false step. If it +# incorrectly decides the the prefix of the secret is ABCX, when the real +# secret is ABCD, then after that every next step it takes will be the +# "drawing from the same distribution" kind -- any difference between ABCXQ +# and ABCXR will be just due to noise, since both are equally far from the +# correct answer, which startsw with ABCD. If it could realize that there is +# no real difference between the distributions, then it could back-track and +# recover. + +# 2. Giving the ability to measure, noisily, the time taken by comparison(), +# how can you most efficiently figure out which guess takes the longest? If +# you can do that more efficiently, you can crack secrets more efficiently. + +# The script takes two arguments. The first is how many symbols in the +# secret, and the second is how big the alphabet from which the symbols are +# drawn. To prove that this script can *ever* work, try passing length 5 and +# alphabet size 2. Also try editing the code to let is use sillycomp. That'll +# definitely make it work. If you can improve this script (as per the thing +# above about "needing better statistics") to the degree that it can crack a +# secret with length 32 and alphabet size 256, then that would be awesome. + +# See the result of this commandline: + +# $ python -c 'import time_comparisons ; time_comparisons.print_measurements()' + + +from pyutil import benchutil + +import hashlib, random, os + +from decimal import Decimal +D=Decimal + +p1 = 'a'*32 +p1a = 'a'*32 +p2 = 'a'*31+'b' # close, but no cigar +p3 = 'b'*32 # different in the first byte + +def randstr(n, alphabetsize): + alphabet = [ chr(x) for x in range(alphabetsize) ] + return ''.join([random.choice(alphabet) for i in range(n)]) + +def compare(n, f, a, b): + for i in xrange(n): + f(a, b) + +def eqeqcomp(a, b): + return a == b + +def sillycomp(a, b): + # This exposes a lot of information in its timing about how many leading bytes match. + for i in range(len(a)): + if a[i] != b[i]: + return False + for i in xrange(2**9): + pass + if len(a) == len(b): + return True + else: + return False + +def hashcomp(a, b): + # Brian Warner invented this for Tahoe-LAFS. It seems like it should be very safe agaist timing leakage of any kind, because of the inclusion of a new random randkey every time. Note that exposing the value of the hash (i.e. the output of md5(randkey+secret)) is *not* a security problem. You can post that on your web site and let all attackers have it, no problem. (Provided that the value of "randkey" remains secret.) + + randkey = os.urandom(32) + return hashlib.md5(randkey+ a).digest() == hashlib.md5(randkey+b).digest() + +def xorcomp(a, b): + # This appears to be the most popular timing-insensitive string comparison function. I'm not completely sure it is fully timing-insensitive. (There are all sorts of funny things inside Python, such as caching of integer objects < 100...) + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + +def print_measurements(): + N=10**4 + REPS=10**2 + + print "all times are in nanoseconds per comparison (in scientific notation)" + print + + for comparator in [eqeqcomp, hashcomp, xorcomp, sillycomp]: + print "using comparator ", comparator + + # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: + trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] + random.shuffle(trials) + for (a, b, desc) in trials: + print "comparing two strings that are %s to each other" % (desc,) + + def f(n): + compare(n, comparator, a, b) + + benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) + + print + +def try_to_crack_secret(cracker, comparator, secretlen, alphabetsize): + secret = randstr(secretlen, alphabetsize) + + def test_guess(x): + return comparator(secret, x) + + print "Giving cracker %s a chance to figure out the secret. Don't tell him, but the secret is %s. Whenever he makes a guess, we'll use comparator %s to decide if his guess is right ..." % (cracker, secret.encode('hex'), comparator,) + + guess = cracker(test_guess, secretlen, alphabetsize) + + print "Cracker %s guessed %r" % (cracker, guess,) + if guess == secret: + print "HE FIGURED IT OUT!? HOW DID HE DO THAT." + else: + print "HAHA. Our secret is safe." + +def byte_at_a_time_cracker(test_guess, secretlen, alphabetsize): + # If we were cleverer, we'd add some backtracking behaviour where, if we can't find any x such that ABCx stands out from the crowd as taking longer than all the other ABCy's, then we start to think that we've taken a wrong step and we go back to trying ABy's. Make sense? But we're not that clever. Once we take a step, we don't backtrack. + + print + + guess=[] + + while len(guess) < secretlen: + best_next_byte = None + best_next_byte_time = None + + # For each possible byte... + for next_byte in range(alphabetsize): + c = chr(next_byte) + + # Construct a guess with our best candidate so far... + candidate_guess = guess[:] + + # Plus that byte... + candidate_guess.append(c) + s = ''.join(candidate_guess) + + # Plus random bytes... + s += os.urandom(32 - len(s)) + + # And see how long it takes the test_guess to consider it... + def f(n): + for i in xrange(n): + test_guess(s) + + times = benchutil.rep_bench(f, 10**7, MAXREPS=10**3, quiet=True) + + fastesttime = times['mean'] + + print "%s..."%(c.encode('hex'),), + if best_next_byte is None or fastesttime > best_next_byte_time: + print "new candidate for slowest next-char: %s, took: %s" % (c.encode('hex'), fastesttime,), + + best_next_byte_time = fastesttime + best_next_byte = c + + # Okay we've tried all possible next bytes. Our guess is this one (the one that took longest to be tested by test_guess): + guess.append(best_next_byte) + print "SLOWEST next-char %s! Current guess at secret: %s" % (best_next_byte.encode('hex'), ''.join(guess).encode('hex'),) + + guess = ''.join(guess) + print "Our guess for the secret: %r" % (guess,) + return guess + +if __name__ == '__main__': + import sys + secretlen = int(sys.argv[1]) + alphabetsize = int(sys.argv[2]) + if alphabetsize > 256: + raise Exception("We assume we can fit one element of the alphabet into a byte.") + + print "secretlen: %d, alphabetsize: %d" % (secretlen, alphabetsize,) + + # try_to_crack_secret(byte_at_a_time_cracker, sillycomp, secretlen, alphabetsize) + try_to_crack_secret(byte_at_a_time_cracker, eqeqcomp, secretlen, alphabetsize) diff --git a/libs/pyutil/test/current/test_mathutil.py b/libs/pyutil/test/current/test_mathutil.py index 7c189dfb..da788758 100644 --- a/libs/pyutil/test/current/test_mathutil.py +++ b/libs/pyutil/test/current/test_mathutil.py @@ -42,6 +42,13 @@ class MathUtilTestCase(unittest.TestCase): self.failUnlessEqual(f(5, 3), 2) self.failUnlessEqual(f(6, 3), 2) self.failUnlessEqual(f(7, 3), 3) + self.failUnless(isinstance(f(0.0, 1), int)) + self.failUnlessEqual(f(7.0, 3.0), 3) + self.failUnlessEqual(f(7, 3.0), 3) + self.failUnlessEqual(f(7.0, 3), 3) + self.failUnlessEqual(f(6.0, 3.0), 2) + self.failUnlessEqual(f(6.0, 3), 2) + self.failUnlessEqual(f(6, 3.0), 2) def test_next_multiple(self): f = mathutil.next_multiple diff --git a/libs/pyutil/test/current/test_mathutil.py~ b/libs/pyutil/test/current/test_mathutil.py~ new file mode 100644 index 00000000..7c189dfb --- /dev/null +++ b/libs/pyutil/test/current/test_mathutil.py~ @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import unittest + +from pyutil import mathutil +from pyutil.assertutil import _assert + +class MathUtilTestCase(unittest.TestCase): + def _help_test_is_power_of_k(self, k): + for i in range(2, 40): + _assert(mathutil.is_power_of_k(k**i, k), k, i) + + def test_is_power_of_k(self): + for i in range(2, 5): + self._help_test_is_power_of_k(i) + + def test_log_ceil(self): + f = mathutil.log_ceil + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 2), 2) + + def test_log_floor(self): + f = mathutil.log_floor + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 0) + self.failUnlessEqual(f(3, 2), 1) + + def test_div_ceil(self): + f = mathutil.div_ceil + self.failUnlessEqual(f(0, 1), 0) + self.failUnlessEqual(f(0, 2), 0) + self.failUnlessEqual(f(0, 3), 0) + self.failUnlessEqual(f(1, 3), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 3), 1) + self.failUnlessEqual(f(4, 3), 2) + self.failUnlessEqual(f(5, 3), 2) + self.failUnlessEqual(f(6, 3), 2) + self.failUnlessEqual(f(7, 3), 3) + + def test_next_multiple(self): + f = mathutil.next_multiple + self.failUnlessEqual(f(5, 1), 5) + self.failUnlessEqual(f(5, 2), 6) + self.failUnlessEqual(f(5, 3), 6) + self.failUnlessEqual(f(5, 4), 8) + self.failUnlessEqual(f(5, 5), 5) + self.failUnlessEqual(f(5, 6), 6) + self.failUnlessEqual(f(32, 1), 32) + self.failUnlessEqual(f(32, 2), 32) + self.failUnlessEqual(f(32, 3), 33) + self.failUnlessEqual(f(32, 4), 32) + self.failUnlessEqual(f(32, 5), 35) + self.failUnlessEqual(f(32, 6), 36) + self.failUnlessEqual(f(32, 7), 35) + self.failUnlessEqual(f(32, 8), 32) + self.failUnlessEqual(f(32, 9), 36) + self.failUnlessEqual(f(32, 10), 40) + self.failUnlessEqual(f(32, 11), 33) + self.failUnlessEqual(f(32, 12), 36) + self.failUnlessEqual(f(32, 13), 39) + self.failUnlessEqual(f(32, 14), 42) + self.failUnlessEqual(f(32, 15), 45) + self.failUnlessEqual(f(32, 16), 32) + self.failUnlessEqual(f(32, 17), 34) + self.failUnlessEqual(f(32, 18), 36) + self.failUnlessEqual(f(32, 589), 589) + + def test_pad_size(self): + f = mathutil.pad_size + self.failUnlessEqual(f(0, 4), 0) + self.failUnlessEqual(f(1, 4), 3) + self.failUnlessEqual(f(2, 4), 2) + self.failUnlessEqual(f(3, 4), 1) + self.failUnlessEqual(f(4, 4), 0) + self.failUnlessEqual(f(5, 4), 3) + + def test_is_power_of_k_part_2(self): + f = mathutil.is_power_of_k + for i in range(1, 100): + if i in (1, 2, 4, 8, 16, 32, 64): + self.failUnless(f(i, 2), "but %d *is* a power of 2" % i) + else: + self.failIf(f(i, 2), "but %d is *not* a power of 2" % i) + for i in range(1, 100): + if i in (1, 3, 9, 27, 81): + self.failUnless(f(i, 3), "but %d *is* a power of 3" % i) + else: + self.failIf(f(i, 3), "but %d is *not* a power of 3" % i) + + def test_next_power_of_k(self): + f = mathutil.next_power_of_k + self.failUnlessEqual(f(0,2), 1) + self.failUnlessEqual(f(1,2), 1) + self.failUnlessEqual(f(2,2), 2) + self.failUnlessEqual(f(3,2), 4) + self.failUnlessEqual(f(4,2), 4) + for i in range(5, 8): self.failUnlessEqual(f(i,2), 8, "%d" % i) + for i in range(9, 16): self.failUnlessEqual(f(i,2), 16, "%d" % i) + for i in range(17, 32): self.failUnlessEqual(f(i,2), 32, "%d" % i) + for i in range(33, 64): self.failUnlessEqual(f(i,2), 64, "%d" % i) + for i in range(65, 100): self.failUnlessEqual(f(i,2), 128, "%d" % i) + + self.failUnlessEqual(f(0,3), 1) + self.failUnlessEqual(f(1,3), 1) + self.failUnlessEqual(f(2,3), 3) + self.failUnlessEqual(f(3,3), 3) + for i in range(4, 9): self.failUnlessEqual(f(i,3), 9, "%d" % i) + for i in range(10, 27): self.failUnlessEqual(f(i,3), 27, "%d" % i) + for i in range(28, 81): self.failUnlessEqual(f(i,3), 81, "%d" % i) + for i in range(82, 200): self.failUnlessEqual(f(i,3), 243, "%d" % i) + + def test_ave(self): + f = mathutil.ave + self.failUnlessEqual(f([1,2,3]), 2) + self.failUnlessEqual(f([0,0,0,4]), 1) + self.failUnlessAlmostEqual(f([0.0, 1.0, 1.0]), .666666666666) + + def failUnlessEqualContents(self, a, b): + self.failUnlessEqual(sorted(a), sorted(b)) + + def test_permute(self): + f = mathutil.permute + self.failUnlessEqualContents(f([]), []) + self.failUnlessEqualContents(f([1]), [[1]]) + self.failUnlessEqualContents(f([1,2]), [[1,2], [2,1]]) + self.failUnlessEqualContents(f([1,2,3]), + [[1,2,3], [1,3,2], + [2,1,3], [2,3,1], + [3,1,2], [3,2,1]]) diff --git a/libs/pyutil/time_comparisons.py b/libs/pyutil/time_comparisons.py deleted file mode 100644 index ee1bcfa0..00000000 --- a/libs/pyutil/time_comparisons.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyutil import benchutil - -import hashlib, random, os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeqcomp(a, b): - return a == b - -def hashcomp(a, b): - salt = os.urandom(32) - return hashlib.md5(salt+ a).digest() == hashlib.md5(salt+b).digest() - -N=10**4 -REPS=10**2 - -print "all times are in nanoseconds per comparison (scientific notation)" -print - -for comparator in [eqeqcomp, hashcomp]: - print "using comparator ", comparator - - # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: - trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] - random.shuffle(trials) - for (a, b, desc) in trials: - print "comparing two strings that are %s to each other" % (desc,) - - def f(n): - compare(n, comparator, a, b) - - benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - - print diff --git a/libs/pyutil/time_comparisons.py~ b/libs/pyutil/time_comparisons.py~ deleted file mode 100644 index abf151ad..00000000 --- a/libs/pyutil/time_comparisons.py~ +++ /dev/null @@ -1,72 +0,0 @@ -from pyutil import benchutil - -import hashlib -import os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeq(a, b): - return a == b - -def equalsequals_s(n): - # return compare(n, eqeq, - for i in xrange(n): - p1 == p1a - -def equalsequals_c(n): - for i in xrange(n): - p1 == p2 - -def equalsequals_f(n): - for i in xrange(n): - p1 == p3 - -def hash_s(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p1a).digest() - -def hash_c(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p2).digest() - -def hash_f(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p3).digest() - -N=10**4 -REPS=10**2 - -print "using '=='" - -print "same" -benchutil.rep_bench(equalsequals_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(equalsequals_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(equalsequals_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "using hash" - -print "same" -benchutil.rep_bench(hash_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(hash_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(hash_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 0cbfa7c0..a2cef356 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -549,7 +549,7 @@ class OAuth2Mixin(object): @return_future def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None, - callback=None): + callback=None, scope=None, response_type="code"): """Redirects the user to obtain OAuth authorization for this service. Some providers require that you register a redirect URL with @@ -566,10 +566,13 @@ class OAuth2Mixin(object): """ args = { "redirect_uri": redirect_uri, - "client_id": client_id + "client_id": client_id, + "response_type": response_type } if extra_params: args.update(extra_params) + if scope: + args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) callback() @@ -945,6 +948,67 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): return OpenIdMixin.get_authenticated_user(self) +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2.""" + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = 'google_oauth' + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Google user, returning a user object. + + Example usage:: + + class GoogleOAuth2LoginHandler(LoginHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.asynchronous + @tornado.gen.coroutine + def get(self): + if self.get_argument("code", False): + user = yield self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + yield self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings["google_consumer_key"], + scope=['openid', 'email'], + response_type='code', + extra_params={"approval_prompt": "auto"}) + """ + http = self.get_auth_http_client() + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, + "code": code, + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], + "grant_type": "authorization_code", + }) + + http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + self.async_callback(self._on_access_token, callback), + method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) + + def _on_access_token(self, future, response): + """Callback function for the exchange to the access token.""" + if response.error: + future.set_exception(AuthError('Google auth error: %s' % str(response))) + return + + args = escape.json_decode(response.body) + future.set_result(args) + + def get_auth_http_client(self): + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + class FacebookMixin(object): """Facebook Connect authentication. diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 05754299..79cccb49 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -16,11 +16,15 @@ """xAutomatically restart the server when a source file is modified. -Most applications should not access this module directly. Instead, pass the -keyword argument ``debug=True`` to the `tornado.web.Application` constructor. -This will enable autoreload mode as well as checking for changes to templates -and static resources. Note that restarting is a destructive operation -and any requests in progress will be aborted when the process restarts. +Most applications should not access this module directly. Instead, +pass the keyword argument ``autoreload=True`` to the +`tornado.web.Application` constructor (or ``debug=True``, which +enables this setting and several others). This will enable autoreload +mode as well as checking for changes to templates and static +resources. Note that restarting is a destructive operation and any +requests in progress will be aborted when the process restarts. (If +you want to disable autoreload while using other debug-mode features, +pass both ``debug=True`` and ``autoreload=False``). This module can also be used as a command-line wrapper around scripts such as unit test runners. See the `main` method for details. @@ -38,6 +42,7 @@ Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) because it re-executes Python using ``sys.executable`` and ``sys.argv``. Additionally, modifying these variables will cause reloading to behave incorrectly. + """ from __future__ import absolute_import, division, print_function, with_statement diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index e0900569..cb97710a 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -360,6 +360,7 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.PROXYUSERPWD, credentials) else: curl.setopt(pycurl.PROXY, '') + curl.unsetopt(pycurl.PROXYUSERPWD) if request.validate_cert: curl.setopt(pycurl.SSL_VERIFYPEER, 1) curl.setopt(pycurl.SSL_VERIFYHOST, 2) @@ -382,6 +383,8 @@ def _curl_setup_request(curl, request, buffer, headers): # that we can't reach, so allow ipv6 unless the user asks to disable. # (but see version check in _process_queue above) curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + else: + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER) # Set the request method through curl's irritating interface which makes # up names for almost every single method @@ -404,6 +407,11 @@ def _curl_setup_request(curl, request, buffer, headers): # Handle curl's cryptic options for every individual HTTP method if request.method in ("POST", "PUT"): + if request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % request.method) + request_buffer = BytesIO(utf8(request.body)) curl.setopt(pycurl.READFUNCTION, request_buffer.read) if request.method == "POST": @@ -414,6 +422,9 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.POSTFIELDSIZE, len(request.body)) else: curl.setopt(pycurl.INFILESIZE, len(request.body)) + elif request.method == "GET": + if request.body is not None: + raise AssertionError('Body must be empty for GET request') if request.auth_username is not None: userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index 92b7458e..217ebdf5 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -38,8 +38,8 @@ since it is both shorter and provides better exception handling):: def get(self): yield gen.Task(AsyncHTTPClient().fetch, "http://example.com") -You can also yield a list of ``Futures`` and/or ``Tasks``, which will be -started at the same time and run in parallel; a list of results will +You can also yield a list or dict of ``Futures`` and/or ``Tasks``, which will be +started at the same time and run in parallel; a list or dict of results will be returned when they are all finished:: @gen.coroutine @@ -47,6 +47,13 @@ be returned when they are all finished:: http_client = AsyncHTTPClient() response1, response2 = yield [http_client.fetch(url1), http_client.fetch(url2)] + response_dict = yield dict(response3=http_client.fetch(url3), + response4=http_client.fetch(url4)) + response3 = response_dict['response3'] + response4 = response_dict['response4'] + +.. versionchanged:: 3.2 + Dict support added. For more complicated interfaces, `Task` can be split into two parts: `Callback` and `Wait`:: @@ -404,6 +411,10 @@ class Multi(YieldPoint): a list of ``YieldPoints``. """ def __init__(self, children): + self.keys = None + if isinstance(children, dict): + self.keys = list(children.keys()) + children = children.values() self.children = [] for i in children: if isinstance(i, Future): @@ -423,7 +434,11 @@ class Multi(YieldPoint): return not self.unfinished_children def get_result(self): - return [i.get_result() for i in self.children] + result = (i.get_result() for i in self.children) + if self.keys is not None: + return dict(zip(self.keys, result)) + else: + return list(result) class _NullYieldPoint(YieldPoint): @@ -523,7 +538,7 @@ class Runner(object): self.finished = True self.yield_point = _null_yield_point raise - if isinstance(yielded, list): + if isinstance(yielded, (list, dict)): yielded = Multi(yielded) elif isinstance(yielded, Future): yielded = YieldFuture(yielded) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 67675894..b58a8348 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -282,7 +282,8 @@ class HTTPRequest(object): :arg int max_redirects: Limit for ``follow_redirects`` :arg string user_agent: String to send as ``User-Agent`` header :arg bool use_gzip: Request gzip encoding from the server - :arg string network_interface: Network interface to use for request + :arg string network_interface: Network interface to use for request. + ``curl_httpclient`` only; see note below. :arg callable streaming_callback: If set, ``streaming_callback`` will be run with each chunk of data as it is received, and ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in @@ -310,14 +311,26 @@ class HTTPRequest(object): :arg bool validate_cert: For HTTPS requests, validate the server's certificate? :arg string ca_certs: filename of CA certificates in PEM format, - or None to use defaults. Note that in ``curl_httpclient``, if - any request uses a custom ``ca_certs`` file, they all must (they - don't have to all use the same ``ca_certs``, but it's not possible - to mix requests with ``ca_certs`` and requests that use the defaults. + or None to use defaults. See note below when used with + ``curl_httpclient``. :arg bool allow_ipv6: Use IPv6 when available? Default is false in ``simple_httpclient`` and true in ``curl_httpclient`` - :arg string client_key: Filename for client SSL key, if any - :arg string client_cert: Filename for client SSL certificate, if any + :arg string client_key: Filename for client SSL key, if any. See + note below when used with ``curl_httpclient``. + :arg string client_cert: Filename for client SSL certificate, if any. + See note below when used with ``curl_httpclient``. + + .. note:: + + When using ``curl_httpclient`` certain options may be + inherited by subsequent fetches because ``pycurl`` does + not allow them to be cleanly reset. This applies to the + ``ca_certs``, ``client_key``, ``client_cert``, and + ``network_interface`` arguments. If you use these + options, you should pass them on every request (you don't + have to always use the same values, but it's not possible + to mix requests that specify these options with ones that + use the defaults). .. versionadded:: 3.1 The ``auth_mode`` argument. @@ -372,6 +385,9 @@ class HTTPResponse(object): * headers: `tornado.httputil.HTTPHeaders` object + * effective_url: final location of the resource after following any + redirects + * buffer: ``cStringIO`` object for response body * body: response body as string (created on demand from ``self.buffer``) diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index d005545e..34e7b768 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -29,6 +29,7 @@ from __future__ import absolute_import, division, print_function, with_statement import socket import ssl import time +import copy from tornado.escape import native_str, parse_qs_bytes from tornado import httputil @@ -326,8 +327,8 @@ class HTTPConnection(object): self.request_callback(self._request) except _BadRequestException as e: - gen_log.info("Malformed HTTP request from %s: %s", - self.address[0], e) + gen_log.info("Malformed HTTP request from %r: %s", + self.address, e) self.close() return @@ -336,7 +337,10 @@ class HTTPConnection(object): if self._request.method in ("POST", "PATCH", "PUT"): httputil.parse_body_arguments( self._request.headers.get("Content-Type", ""), data, - self._request.arguments, self._request.files) + self._request.body_arguments, self._request.files) + + for k, v in self._request.body_arguments.items(): + self._request.arguments.setdefault(k, []).extend(v) self.request_callback(self._request) @@ -403,6 +407,20 @@ class HTTPRequest(object): `.RequestHandler.get_argument`, which returns argument values as unicode strings. + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + .. attribute:: files File uploads are available in the files property, which maps file @@ -457,6 +475,8 @@ class HTTPRequest(object): self.path, sep, self.query = uri.partition('?') self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 3e7337d9..2575bc56 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -320,7 +320,11 @@ def parse_body_arguments(content_type, body, arguments, files): with the parsed contents. """ if content_type.startswith("application/x-www-form-urlencoded"): - uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + try: + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + except Exception as e: + gen_log.warning('Invalid x-www-form-urlencoded body: %s', e) + uri_arguments = {} for name, values in uri_arguments.items(): if values: arguments.setdefault(name, []).extend(values) diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 91ee2c5b..a36ab7a5 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -676,8 +676,7 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - if self._handlers.has_key(fd): - self._handlers[fd](fd, events) + self._handlers[fd](fd, events) except (OSError, IOError) as e: if e.args[0] == errno.EPIPE: # Happens when the client closes the connection diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 6bdc6397..08430cea 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -774,7 +774,7 @@ class IOStream(BaseIOStream): # Sometimes setsockopt will fail if the socket is closed # at the wrong time. This can happen with HTTPServer # resetting the value to false between requests. - if e.errno != errno.EINVAL: + if e.errno not in (errno.EINVAL, errno.ECONNRESET): raise diff --git a/libs/tornado/log.py b/libs/tornado/log.py index fa11f379..648db5c6 100755 --- a/libs/tornado/log.py +++ b/libs/tornado/log.py @@ -51,7 +51,7 @@ gen_log = logging.getLogger("tornado.general") def _stderr_supports_color(): color = False - if curses and sys.stderr.isatty(): + if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 9dc8506e..21db4755 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -20,7 +20,6 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os -import re import socket import ssl import stat @@ -30,6 +29,13 @@ from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec from tornado.util import Configurable +if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ + ssl_match_hostname = ssl.match_hostname + SSLCertificateError = ssl.CertificateError +else: + import backports.ssl_match_hostname + ssl_match_hostname = backports.ssl_match_hostname.match_hostname + SSLCertificateError = backports.ssl_match_hostname.CertificateError def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. @@ -391,73 +397,3 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): return context.wrap_socket(socket, **kwargs) else: return ssl.wrap_socket(socket, **dict(context, **kwargs)) - -if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ - ssl_match_hostname = ssl.match_hostname - SSLCertificateError = ssl.CertificateError -else: - # match_hostname was added to the standard library ssl module in python 3.2. - # The following code was backported for older releases and copied from - # https://bitbucket.org/brandon/backports.ssl_match_hostname - class SSLCertificateError(ValueError): - pass - - def _dnsname_to_pat(dn, max_wildcards=1): - pats = [] - for frag in dn.split(r'.'): - if frag.count('*') > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survery of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise SSLCertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - if frag == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - else: - # Otherwise, '*' matches any dotless fragment. - frag = re.escape(frag) - pats.append(frag.replace(r'\*', '[^.]*')) - return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - - def ssl_match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules - are mostly followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate") - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise SSLCertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise SSLCertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise SSLCertificateError("no appropriate commonName or " - "subjectAltName fields were found") diff --git a/libs/tornado/platform/asyncio.py b/libs/tornado/platform/asyncio.py new file mode 100644 index 00000000..a8f5bad4 --- /dev/null +++ b/libs/tornado/platform/asyncio.py @@ -0,0 +1,134 @@ +"""Bridges between the `asyncio` module and Tornado IOLoop. + +This is a work in progress and interfaces are subject to change. + +To test: +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOLoop +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOMainLoop +(the tests log a few warnings with AsyncIOMainLoop because they leave some +unfinished callbacks on the event loop that fail when it resumes) +""" +import asyncio +import datetime +import functools +import os + +from tornado.ioloop import IOLoop +from tornado import stack_context + +class BaseAsyncIOLoop(IOLoop): + def initialize(self, asyncio_loop, close_loop=False): + self.asyncio_loop = asyncio_loop + self.close_loop = close_loop + self.asyncio_loop.call_soon(self.make_current) + # Maps fd to handler function (as in IOLoop.add_handler) + self.handlers = {} + # Set of fds listening for reads/writes + self.readers = set() + self.writers = set() + self.closing = False + + def close(self, all_fds=False): + self.closing = True + for fd in list(self.handlers): + self.remove_handler(fd) + if all_fds: + os.close(fd) + if self.close_loop: + self.asyncio_loop.close() + + def add_handler(self, fd, handler, events): + if fd in self.handlers: + raise ValueError("fd %d added twice" % fd) + self.handlers[fd] = stack_context.wrap(handler) + if events & IOLoop.READ: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + if events & IOLoop.WRITE: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + + def update_handler(self, fd, events): + if events & IOLoop.READ: + if fd not in self.readers: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + else: + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if events & IOLoop.WRITE: + if fd not in self.writers: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + else: + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + + def remove_handler(self, fd): + if fd not in self.handlers: + return + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + del self.handlers[fd] + + def _handle_events(self, fd, events): + self.handlers[fd](fd, events) + + def start(self): + self.asyncio_loop.run_forever() + + def stop(self): + self.asyncio_loop.stop() + + def _run_callback(self, callback, *args, **kwargs): + try: + callback(*args, **kwargs) + except Exception: + self.handle_callback_exception(callback) + + def add_timeout(self, deadline, callback): + if isinstance(deadline, (int, float)): + delay = max(deadline - self.time(), 0) + elif isinstance(deadline, datetime.timedelta): + delay = deadline.total_seconds() + else: + raise TypeError("Unsupported deadline %r", deadline) + return self.asyncio_loop.call_later(delay, self._run_callback, + stack_context.wrap(callback)) + + def remove_timeout(self, timeout): + timeout.cancel() + + def add_callback(self, callback, *args, **kwargs): + if self.closing: + raise RuntimeError("IOLoop is closing") + if kwargs: + self.asyncio_loop.call_soon_threadsafe(functools.partial( + self._run_callback, stack_context.wrap(callback), + *args, **kwargs)) + else: + self.asyncio_loop.call_soon_threadsafe( + self._run_callback, stack_context.wrap(callback), *args) + + add_callback_from_signal = add_callback + + +class AsyncIOMainLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(), + close_loop=False) + +class AsyncIOLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOLoop, self).initialize(asyncio.new_event_loop(), + close_loop=True) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index ffd2d29d..942c5c3f 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -92,7 +92,8 @@ def fork_processes(num_processes, max_restarts=100): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the debug=True option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``fork_processes``. diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index d8dbb271..2558ada8 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -72,6 +72,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.max_clients = max_clients self.queue = collections.deque() self.active = {} + self.waiting = {} self.max_buffer_size = max_buffer_size if resolver: self.resolver = resolver @@ -89,7 +90,16 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.resolver.close() def fetch_impl(self, request, callback): - self.queue.append((request, callback)) + key = object() + self.queue.append((key, request, callback)) + if not len(self.active) < self.max_clients: + timeout_handle = self.io_loop.add_timeout( + self.io_loop.time() + min(request.connect_timeout, + request.request_timeout), + functools.partial(self._on_timeout, key)) + else: + timeout_handle = None + self.waiting[key] = (request, callback, timeout_handle) self._process_queue() if self.queue: gen_log.debug("max_clients limit reached, request queued. " @@ -99,8 +109,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _process_queue(self): with stack_context.NullContext(): while self.queue and len(self.active) < self.max_clients: - request, callback = self.queue.popleft() - key = object() + key, request, callback = self.queue.popleft() + if key not in self.waiting: + continue + self._remove_timeout(key) self.active[key] = (request, callback) release_callback = functools.partial(self._release_fetch, key) self._handle_request(request, release_callback, callback) @@ -113,6 +125,22 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): del self.active[key] self._process_queue() + def _remove_timeout(self, key): + if key in self.waiting: + request, callback, timeout_handle = self.waiting[key] + if timeout_handle is not None: + self.io_loop.remove_timeout(timeout_handle) + del self.waiting[key] + + def _on_timeout(self, key): + request, callback, timeout_handle = self.waiting[key] + self.queue.remove((key, request, callback)) + timeout_response = HTTPResponse( + request, 599, error=HTTPError(599, "Timeout"), + request_time=self.io_loop.time() - request.start_time) + self.io_loop.add_callback(callback, timeout_response) + del self.waiting[key] + class _HTTPConnection(object): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) @@ -162,15 +190,18 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) self.resolver.resolve(host, port, af, callback=self._on_resolve) def _on_resolve(self, addrinfo): + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout + return self.stream = self._create_stream(addrinfo) - timeout = min(self.request.connect_timeout, self.request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) self.stream.set_close_callback(self._on_close) # ipv6 addresses are broken (in self.parsed.hostname) until # 2.7, here is correctly parsed value calculated in __init__ @@ -199,10 +230,10 @@ class _HTTPConnection(object): # the SSL_OP_NO_SSLv2, but that wasn't exposed to python # until 3.2. Python 2.7 adds the ciphers argument, which # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to SSLv3. This is + # on python 2.6, we set ssl_version to TLSv1. This is # more narrow than we'd like since it also breaks - # compatibility with servers configured for TLSv1 only, - # but nearly all servers support SSLv3: + # compatibility with servers configured for SSLv3 only, + # but nearly all servers support both SSLv3 and TLSv1: # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html if sys.version_info >= (2, 7): ssl_options["ciphers"] = "DEFAULT:!SSLv2" @@ -210,7 +241,7 @@ class _HTTPConnection(object): # This is really only necessary for pre-1.0 versions # of openssl, but python 2.6 doesn't expose version # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 return SSLIOStream(socket.socket(af), io_loop=self.io_loop, @@ -233,6 +264,8 @@ class _HTTPConnection(object): def _on_connect(self): self._remove_timeout() + if self.final_callback is None: + return if self.request.request_timeout: self._timeout = self.io_loop.add_timeout( self.start_time + self.request.request_timeout, @@ -269,9 +302,15 @@ class _HTTPConnection(object): self.request.headers["User-Agent"] = self.request.user_agent if not self.request.allow_nonstandard_methods: if self.request.method in ("POST", "PATCH", "PUT"): - assert self.request.body is not None + if self.request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % self.request.method) else: - assert self.request.body is None + if self.request.body is not None: + raise AssertionError( + 'Body must be empty for "%s" request' + % self.request.method) if self.request.body is not None: self.request.headers["Content-Length"] = str(len( self.request.body)) diff --git a/libs/tornado/speedups.c b/libs/tornado/speedups.c new file mode 100644 index 00000000..8a316c58 --- /dev/null +++ b/libs/tornado/speedups.c @@ -0,0 +1,49 @@ +#include + +static PyObject* websocket_mask(PyObject* self, PyObject* args) { + const char* mask; + int mask_len; + const char* data; + int data_len; + int i; + + if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { + return NULL; + } + + PyObject* result = PyBytes_FromStringAndSize(NULL, data_len); + if (!result) { + return NULL; + } + char* buf = PyBytes_AsString(result); + for (i = 0; i < data_len; i++) { + buf[i] = data[i] ^ mask[i % 4]; + } + + return result; +} + +static PyMethodDef methods[] = { + {"websocket_mask", websocket_mask, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef speedupsmodule = { + PyModuleDef_HEAD_INIT, + "speedups", + NULL, + -1, + methods +}; + +PyMODINIT_FUNC +PyInit_speedups() { + return PyModule_Create(&speedupsmodule); +} +#else // Python 2.x +PyMODINIT_FUNC +initspeedups() { + Py_InitModule("tornado.speedups", methods); +} +#endif diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index 8473a21a..c0773732 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -180,7 +180,8 @@ class TCPServer(object): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the ``debug=True`` option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``TCPServer.start(n)``. """ diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 5f8d6091..b6d7e97e 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -250,7 +250,7 @@ class RequestHandler(object): not self.request.connection.no_keep_alive): conn_header = self.request.headers.get("Connection") if conn_header and (conn_header.lower() == "keep-alive"): - self.set_header("Connection", "Keep-Alive") + self._headers["Connection"] = "Keep-Alive" self._write_buffer = [] self._status_code = 200 self._reason = httputil.responses[200] @@ -348,12 +348,7 @@ class RequestHandler(object): The returned value is always unicode. """ - args = self.get_arguments(name, strip=strip) - if not args: - if default is self._ARG_DEFAULT: - raise MissingArgumentError(name) - return default - return args[-1] + return self._get_argument(name, default, self.request.arguments, strip) def get_arguments(self, name, strip=True): """Returns a list of the arguments with the given name. @@ -362,9 +357,73 @@ class RequestHandler(object): The returned values are always unicode. """ + return self._get_arguments(name, self.request.arguments, strip) + def get_body_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request body. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.body_arguments, strip) + + def get_body_arguments(self, name, strip=True): + """Returns a list of the body arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.body_arguments, strip) + + def get_query_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request query string. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.query_arguments, strip) + + def get_query_arguments(self, name, strip=True): + """Returns a list of the query arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.query_arguments, strip) + + def _get_argument(self, name, default, source, strip=True): + args = self._get_arguments(name, source, strip=strip) + if not args: + if default is self._ARG_DEFAULT: + raise MissingArgumentError(name) + return default + return args[-1] + + def _get_arguments(self, name, source, strip=True): values = [] - for v in self.request.arguments.get(name, []): + for v in source.get(name, []): v = self.decode_argument(v, name=name) if isinstance(v, unicode_type): # Get rid of any weird control chars (unless decoding gave @@ -838,7 +897,7 @@ class RequestHandler(object): else: self.finish(self.get_error_html(status_code, **kwargs)) return - if self.settings.get("debug") and "exc_info" in kwargs: + if self.settings.get("serve_traceback") and "exc_info" in kwargs: # in debug mode, try to send a traceback self.set_header('Content-Type', 'text/plain') for line in traceback.format_exception(*kwargs["exc_info"]): @@ -1318,6 +1377,12 @@ def asynchronous(method): if not self._finished: self.finish() IOLoop.current().add_future(result, future_complete) + # Once we have done this, hide the Future from our + # caller (i.e. RequestHandler._when_complete), which + # would otherwise set up its own callback and + # exception handler (resulting in exceptions being + # logged twice). + return None return result return wrapper @@ -1383,10 +1448,16 @@ class Application(object): or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. + The request class can be specified as either a class object or a + (fully-qualified) name. - Each tuple can contain an optional third element, which should be - a dictionary if it is present. That dictionary is passed as - keyword arguments to the contructor of the handler. This pattern + Each tuple can contain additional elements, which correspond to the + arguments to the `URLSpec` constructor. (Prior to Tornado 3.2, this + only tuples of two or three elements were allowed). + + A dictionary may be passed as the third element of the tuple, + which will be used as keyword arguments to the handler's + constructor and `~RequestHandler.initialize` method. This pattern is used for the `StaticFileHandler` in this example (note that a `StaticFileHandler` can be installed automatically with the static_path setting described below):: @@ -1409,6 +1480,7 @@ class Application(object): and ``/robots.txt`` from the same directory. A custom subclass of `StaticFileHandler` can be specified with the ``static_handler_class`` setting. + """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1447,8 +1519,14 @@ class Application(object): if handlers: self.add_handlers(".*$", handlers) + if self.settings.get('debug'): + self.settings.setdefault('autoreload', True) + self.settings.setdefault('compiled_template_cache', False) + self.settings.setdefault('static_hash_cache', False) + self.settings.setdefault('serve_traceback', True) + # Automatically reload modified modules - if self.settings.get("debug") and not wsgi: + if self.settings.get('autoreload') and not wsgi: from tornado import autoreload autoreload.start() @@ -1493,20 +1571,8 @@ class Application(object): for spec in host_handlers: if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3) - pattern = spec[0] - handler = spec[1] - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - if len(spec) == 3: - kwargs = spec[2] - else: - kwargs = {} - spec = URLSpec(pattern, handler, kwargs) + assert len(spec) in (2, 3, 4) + spec = URLSpec(*spec) handlers.append(spec) if spec.name: if spec.name in self.named_handlers: @@ -1597,14 +1663,23 @@ class Application(object): args = [unquote(s) for s in match.groups()] break if not handler: - handler = ErrorHandler(self, request, status_code=404) + if self.settings.get('default_handler_class'): + handler_class = self.settings['default_handler_class'] + handler_args = self.settings.get( + 'default_handler_args', {}) + else: + handler_class = ErrorHandler + handler_args = dict(status_code=404) + handler = handler_class(self, request, **handler_args) - # In debug mode, re-compile templates and reload static files on every + # If template cache is disabled (usually in the debug mode), + # re-compile templates and reload static files on every # request so you don't need to restart to see changes - if self.settings.get("debug"): + if not self.settings.get("compiled_template_cache", True): with RequestHandler._template_loader_lock: for loader in RequestHandler._template_loaders.values(): loader.reset() + if not self.settings.get('static_hash_cache', True): StaticFileHandler.reset() handler._execute(transforms, *args, **kwargs) @@ -2454,7 +2529,7 @@ class _UIModuleNamespace(object): class URLSpec(object): """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler_class, kwargs=None, name=None): + def __init__(self, pattern, handler, kwargs=None, name=None): """Parameters: * ``pattern``: Regular expression to be matched. Any groups @@ -2475,7 +2550,13 @@ class URLSpec(object): assert len(self.regex.groupindex) in (0, self.regex.groups), \ ("groups in url regexes must either be all named or all " "positional: %r" % self.regex.pattern) - self.handler_class = handler_class + + if isinstance(handler, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + handler = import_object(handler) + + self.handler_class = handler self.kwargs = kwargs or {} self.name = name self._path, self._group_count = self._find_groups() diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 676d21bf..8c2f5a64 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -33,7 +33,7 @@ import tornado.web from tornado.concurrent import TracebackFuture from tornado.escape import utf8, native_str -from tornado import httpclient +from tornado import httpclient, httputil from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log @@ -52,6 +52,10 @@ class WebSocketError(Exception): class WebSocketClosedError(WebSocketError): + """Raised by operations on a closed connection. + + .. versionadded:: 3.2 + """ pass @@ -163,6 +167,12 @@ class WebSocketHandler(tornado.web.RequestHandler): encoded as json). If the ``binary`` argument is false, the message will be sent as utf8; in binary mode any byte string is allowed. + + If the connection is already closed, raises `WebSocketClosedError`. + + .. versionchanged:: 3.2 + `WebSocketClosedError` was added (previously a closed connection + would raise an `AttributeError`) """ if self.ws_connection is None: raise WebSocketClosedError() @@ -586,7 +596,7 @@ class WebSocketProtocol13(WebSocketProtocol): frame += struct.pack("!BQ", 127 | mask_bit, l) if self.mask_outgoing: mask = os.urandom(4) - data = mask + self._apply_mask(mask, data) + data = mask + _websocket_mask(mask, data) frame += data self.stream.write(frame) @@ -671,21 +681,8 @@ class WebSocketProtocol13(WebSocketProtocol): except StreamClosedError: self._abort() - def _apply_mask(self, mask, data): - mask = array.array("B", mask) - unmasked = array.array("B", data) - for i in xrange(len(data)): - unmasked[i] = unmasked[i] ^ mask[i % 4] - if hasattr(unmasked, 'tobytes'): - # tostring was deprecated in py32. It hasn't been removed, - # but since we turn on deprecation warnings in our tests - # we need to use the right one. - return unmasked.tobytes() - else: - return unmasked.tostring() - def _on_masked_frame_data(self, data): - self._on_frame_data(self._apply_mask(self._frame_mask, data)) + self._on_frame_data(_websocket_mask(self._frame_mask, data)) def _on_frame_data(self, data): if self._frame_opcode_is_control: @@ -771,7 +768,11 @@ class WebSocketProtocol13(WebSocketProtocol): class WebSocketClientConnection(simple_httpclient._HTTPConnection): - """WebSocket client connection.""" + """WebSocket client connection. + + This class should not be instantiated directly; use the + `websocket_connect` function instead. + """ def __init__(self, io_loop, request): self.connect_future = TracebackFuture() self.read_future = None @@ -793,9 +794,19 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): io_loop, None, request, lambda: None, self._on_http_response, 104857600, self.resolver) + def close(self): + """Closes the websocket connection. + + .. versionadded:: 3.2 + """ + if self.protocol is not None: + self.protocol.close() + self.protocol = None + def _on_close(self): self.on_message(None) self.resolver.close() + super(WebSocketClientConnection, self)._on_close() def _on_http_response(self, response): if not self.connect_future.done(): @@ -859,13 +870,54 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): Takes a url and returns a Future whose result is a `WebSocketClientConnection`. + + .. versionchanged:: 3.2 + Also accepts ``HTTPRequest`` objects in place of urls. """ if io_loop is None: io_loop = IOLoop.current() - request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) + if isinstance(url, httpclient.HTTPRequest): + assert connect_timeout is None + request = url + # Copy and convert the headers dict/object (see comments in + # AsyncHTTPClient.fetch) + request.headers = httputil.HTTPHeaders(request.headers) + else: + request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) request = httpclient._RequestProxy( request, httpclient.HTTPRequest._DEFAULTS) conn = WebSocketClientConnection(io_loop, request) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future + +def _websocket_mask_python(mask, data): + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. + Returns a `bytes` object of the same length as `data` with the mask applied + as specified in section 5.3 of RFC 6455. + + This pure-python implementation may be replaced by an optimized version when available. + """ + mask = array.array("B", mask) + unmasked = array.array("B", data) + for i in xrange(len(data)): + unmasked[i] = unmasked[i] ^ mask[i % 4] + if hasattr(unmasked, 'tobytes'): + # tostring was deprecated in py32. It hasn't been removed, + # but since we turn on deprecation warnings in our tests + # we need to use the right one. + return unmasked.tobytes() + else: + return unmasked.tostring() + +if os.environ.get('TORNADO_NO_EXTENSION'): + # This environment variable exists to make it easier to do performance comparisons; + # it's not guaranteed to remain supported in the future. + _websocket_mask = _websocket_mask_python +else: + try: + from tornado.speedups import websocket_mask as _websocket_mask + except ImportError: + _websocket_mask = _websocket_mask_python diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 5e25a564..8e5ddedb 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -33,6 +33,7 @@ from __future__ import absolute_import, division, print_function, with_statement import sys import time +import copy import tornado from tornado import escape @@ -142,11 +143,14 @@ class HTTPRequest(object): self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) self.uri = self.path self.arguments = {} + self.query_arguments = {} + self.body_arguments = {} self.query = environ.get("QUERY_STRING", "") if self.query: self.uri += "?" + self.query self.arguments = parse_qs_bytes(native_str(self.query), keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) self.version = "HTTP/1.1" self.headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): @@ -171,7 +175,10 @@ class HTTPRequest(object): # Parse request body self.files = {} httputil.parse_body_arguments(self.headers.get("Content-Type", ""), - self.body, self.arguments, self.files) + self.body, self.body_arguments, self.files) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) self._start_time = time.time() self._finish_time = None
, , and blocks) we @@ -1026,19 +977,19 @@ def getPhases(debug): self.processSpaceCharacters = self.processSpaceCharactersNonPre if (data.startswith("\n") and self.tree.openElements[-1].name in ("pre", "listing", "textarea") - and not self.tree.openElements[-1].hasContent()): + and not self.tree.openElements[-1].hasContent()): data = data[1:] if data: self.tree.reconstructActiveFormattingElements() self.tree.insertText(data) def processCharacters(self, token): - if token["data"] == u"\u0000": - #The tokenizer should always emit null on its own + if token["data"] == "\u0000": + # The tokenizer should always emit null on its own return self.tree.reconstructActiveFormattingElements() self.tree.insertText(token["data"]) - #This must be bad for performance + # This must be bad for performance if (self.parser.framesetOK and any([char not in spaceCharacters for char in token["data"]])): @@ -1054,11 +1005,11 @@ def getPhases(debug): def startTagBody(self, token): self.parser.parseError("unexpected-start-tag", {"name": "body"}) if (len(self.tree.openElements) == 1 - or self.tree.openElements[1].name != "body"): + or self.tree.openElements[1].name != "body"): assert self.parser.innerHTML else: self.parser.framesetOK = False - for attr, value in token["data"].iteritems(): + for attr, value in token["data"].items(): if attr not in self.tree.openElements[1].attributes: self.tree.openElements[1].attributes[attr] = value @@ -1090,7 +1041,7 @@ def getPhases(debug): def startTagForm(self, token): if self.tree.formPointer: - self.parser.parseError(u"unexpected-start-tag", {"name": "form"}) + self.parser.parseError("unexpected-start-tag", {"name": "form"}) else: if self.tree.elementInScope("p", variant="button"): self.endTagP(impliedTagToken("p")) @@ -1100,9 +1051,9 @@ def getPhases(debug): def startTagListItem(self, token): self.parser.framesetOK = False - stopNamesMap = {"li":["li"], - "dt":["dt", "dd"], - "dd":["dt", "dd"]} + stopNamesMap = {"li": ["li"], + "dt": ["dt", "dd"], + "dd": ["dt", "dd"]} stopNames = stopNamesMap[token["name"]] for node in reversed(self.tree.openElements): if node.name in stopNames: @@ -1110,7 +1061,7 @@ def getPhases(debug): impliedTagToken(node.name, "EndTag")) break if (node.nameTuple in specialElements and - node.name not in ("address", "div", "p")): + node.name not in ("address", "div", "p")): break if self.tree.elementInScope("p", variant="button"): @@ -1137,7 +1088,7 @@ def getPhases(debug): afeAElement = self.tree.elementInActiveFormattingElements("a") if afeAElement: self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "a", "endName": "a"}) + {"startName": "a", "endName": "a"}) self.endTagFormatting(impliedTagToken("a")) if afeAElement in self.tree.openElements: self.tree.openElements.remove(afeAElement) @@ -1154,7 +1105,7 @@ def getPhases(debug): self.tree.reconstructActiveFormattingElements() if self.tree.elementInScope("nobr"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "nobr", "endName": "nobr"}) + {"startName": "nobr", "endName": "nobr"}) self.processEndTag(impliedTagToken("nobr")) # XXX Need tests that trigger the following self.tree.reconstructActiveFormattingElements() @@ -1163,7 +1114,7 @@ def getPhases(debug): def startTagButton(self, token): if self.tree.elementInScope("button"): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "button", "endName": "button"}) + {"startName": "button", "endName": "button"}) self.processEndTag(impliedTagToken("button")) return token else: @@ -1203,8 +1154,8 @@ def getPhases(debug): framesetOK = self.parser.framesetOK self.startTagVoidFormatting(token) if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): - #input type=hidden doesn't change framesetOK + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + # input type=hidden doesn't change framesetOK self.parser.framesetOK = framesetOK def startTagParamSource(self, token): @@ -1223,7 +1174,7 @@ def getPhases(debug): def startTagImage(self, token): # No really... self.parser.parseError("unexpected-start-tag-treated-as", - {"originalName": "image", "newName": "img"}) + {"originalName": "image", "newName": "img"}) self.processStartTag(impliedTagToken("img", "StartTag", attributes=token["data"], selfClosing=token["selfClosing"])) @@ -1243,18 +1194,18 @@ def getPhases(debug): if "prompt" in token["data"]: prompt = token["data"]["prompt"] else: - prompt = u"This is a searchable index. Enter search keywords: " + prompt = "This is a searchable index. Enter search keywords: " self.processCharacters( - {"type":tokenTypes["Characters"], "data":prompt}) + {"type": tokenTypes["Characters"], "data": prompt}) attributes = token["data"].copy() if "action" in attributes: del attributes["action"] if "prompt" in attributes: del attributes["prompt"] attributes["name"] = "isindex" - self.processStartTag(impliedTagToken("input", "StartTag", - attributes = attributes, - selfClosing = + self.processStartTag(impliedTagToken("input", "StartTag", + attributes=attributes, + selfClosing= token["selfClosing"])) self.processEndTag(impliedTagToken("label")) self.processStartTag(impliedTagToken("hr", "StartTag")) @@ -1287,7 +1238,7 @@ def getPhases(debug): if self.parser.phase in (self.parser.phases["inTable"], self.parser.phases["inCaption"], self.parser.phases["inColumnGroup"], - self.parser.phases["inTableBody"], + self.parser.phases["inTableBody"], self.parser.phases["inRow"], self.parser.phases["inCell"]): self.parser.phase = self.parser.phases["inSelectInTable"] @@ -1307,8 +1258,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["mathml"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1319,8 +1270,8 @@ def getPhases(debug): self.parser.adjustForeignAttributes(token) token["namespace"] = namespaces["svg"] self.tree.insertElement(token) - #Need to get the parse error right for the case where the token - #has a namespace not equal to the xmlns attribute + # Need to get the parse error right for the case where the token + # has a namespace not equal to the xmlns attribute if token["selfClosing"]: self.tree.openElements.pop() token["selfClosingAcknowledged"] = True @@ -1362,7 +1313,7 @@ def getPhases(debug): "tbody", "td", "tfoot", "th", "thead", "tr", "body", "html")): - #Not sure this is the correct name for the parse error + # Not sure this is the correct name for the parse error self.parser.parseError( "expected-one-end-tag-but-got-another", {"expectedName": "body", "gotName": node.name}) @@ -1370,20 +1321,20 @@ def getPhases(debug): self.parser.phase = self.parser.phases["afterBody"] def endTagHtml(self, token): - #We repeat the test for the body end tag token being ignored here + # We repeat the test for the body end tag token being ignored here if self.tree.elementInScope("body"): self.endTagBody(impliedTagToken("body")) return token def endTagBlock(self, token): - #Put us back in the right whitespace handling mode + # Put us back in the right whitespace handling mode if token["name"] == "pre": self.processSpaceCharacters = self.processSpaceCharactersNonPre inScope = self.tree.elementInScope(token["name"]) if inScope: self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != token["name"]: - self.parser.parseError("end-tag-too-early", {"name": token["name"]}) + self.parser.parseError("end-tag-too-early", {"name": token["name"]}) if inScope: node = self.tree.openElements.pop() while node.name != token["name"]: @@ -1394,7 +1345,7 @@ def getPhases(debug): self.tree.formPointer = None if node is None or not self.tree.elementInScope(node): self.parser.parseError("unexpected-end-tag", - {"name":"form"}) + {"name": "form"}) else: self.tree.generateImpliedEndTags() if self.tree.openElements[-1] != node: @@ -1410,7 +1361,7 @@ def getPhases(debug): if not self.tree.elementInScope(token["name"], variant=variant): self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) else: - self.tree.generateImpliedEndTags(exclude = token["name"]) + self.tree.generateImpliedEndTags(exclude=token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError( "end-tag-too-early", @@ -1436,65 +1387,105 @@ def getPhases(debug): def endTagFormatting(self, token): """The much-feared adoption agency algorithm""" - # http://www.whatwg.org/specs/web-apps/current-work/#adoptionAgency + # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867 # XXX Better parseError messages appreciated. - name = token["name"] + # Step 1 outerLoopCounter = 0 + + # Step 2 while outerLoopCounter < 8: + + # Step 3 outerLoopCounter += 1 - # Step 1 paragraph 1 + # Step 4: + + # Let the formatting element be the last element in + # the list of active formatting elements that: + # - is between the end of the list and the last scope + # marker in the list, if any, or the start of the list + # otherwise, and + # - has the same tag name as the token. formattingElement = self.tree.elementInActiveFormattingElements( token["name"]) - if (not formattingElement or + if (not formattingElement or (formattingElement in self.tree.openElements and not self.tree.elementInScope(formattingElement.name))): - self.parser.parseError("adoption-agency-1.1", {"name": token["name"]}) + # If there is no such node, then abort these steps + # and instead act as described in the "any other + # end tag" entry below. + self.endTagOther(token) return - # Step 1 paragraph 2 + # Otherwise, if there is such a node, but that node is + # not in the stack of open elements, then this is a + # parse error; remove the element from the list, and + # abort these steps. elif formattingElement not in self.tree.openElements: self.parser.parseError("adoption-agency-1.2", {"name": token["name"]}) self.tree.activeFormattingElements.remove(formattingElement) return - # Step 1 paragraph 3 - if formattingElement != self.tree.openElements[-1]: - self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + # Otherwise, if there is such a node, and that node is + # also in the stack of open elements, but the element + # is not in scope, then this is a parse error; ignore + # the token, and abort these steps. + elif not self.tree.elementInScope(formattingElement.name): + self.parser.parseError("adoption-agency-4.4", {"name": token["name"]}) + return - # Step 2 - # Start of the adoption agency algorithm proper + # Otherwise, there is a formatting element and that + # element is in the stack and is in scope. If the + # element is not the current node, this is a parse + # error. In any case, proceed with the algorithm as + # written in the following steps. + else: + if formattingElement != self.tree.openElements[-1]: + self.parser.parseError("adoption-agency-1.3", {"name": token["name"]}) + + # Step 5: + + # Let the furthest block be the topmost node in the + # stack of open elements that is lower in the stack + # than the formatting element, and is an element in + # the special category. There might not be one. afeIndex = self.tree.openElements.index(formattingElement) furthestBlock = None for element in self.tree.openElements[afeIndex:]: if element.nameTuple in specialElements: furthestBlock = element break - # Step 3 + + # Step 6: + + # If there is no furthest block, then the UA must + # first pop all the nodes from the bottom of the stack + # of open elements, from the current node up to and + # including the formatting element, then remove the + # formatting element from the list of active + # formatting elements, and finally abort these steps. if furthestBlock is None: element = self.tree.openElements.pop() while element != formattingElement: element = self.tree.openElements.pop() self.tree.activeFormattingElements.remove(element) return - commonAncestor = self.tree.openElements[afeIndex-1] - # Step 5 - #if furthestBlock.parent: - # furthestBlock.parent.removeChild(furthestBlock) + # Step 7 + commonAncestor = self.tree.openElements[afeIndex - 1] - # Step 5 + # Step 8: # The bookmark is supposed to help us identify where to reinsert - # nodes in step 12. We have to ensure that we reinsert nodes after + # nodes in step 15. We have to ensure that we reinsert nodes after # the node before the active formatting element. Note the bookmark - # can move in step 7.4 + # can move in step 9.7 bookmark = self.tree.activeFormattingElements.index(formattingElement) - # Step 6 + # Step 9 lastNode = node = furthestBlock innerLoopCounter = 0 - + index = self.tree.openElements.index(node) while innerLoopCounter < 3: innerLoopCounter += 1 @@ -1504,15 +1495,13 @@ def getPhases(debug): if node not in self.tree.activeFormattingElements: self.tree.openElements.remove(node) continue - # Step 6.3 + # Step 9.6 if node == formattingElement: break - # Step 6.4 + # Step 9.7 if lastNode == furthestBlock: - bookmark = (self.tree.activeFormattingElements.index(node) - + 1) - # Step 6.5 - #cite = node.parent + bookmark = self.tree.activeFormattingElements.index(node) + 1 + # Step 9.8 clone = node.cloneNode() # Replace node with clone self.tree.activeFormattingElements[ @@ -1520,20 +1509,18 @@ def getPhases(debug): self.tree.openElements[ self.tree.openElements.index(node)] = clone node = clone - - # Step 6.6 + # Step 9.9 # Remove lastNode from its parents, if any if lastNode.parent: lastNode.parent.removeChild(lastNode) node.appendChild(lastNode) - # Step 7.7 + # Step 9.10 lastNode = node - # End of inner loop - # Step 7 + # Step 10 # Foster parent lastNode if commonAncestor is a - # table, tbody, tfoot, thead, or tr we need to foster parent the - # lastNode + # table, tbody, tfoot, thead, or tr we need to foster + # parent the lastNode if lastNode.parent: lastNode.parent.removeChild(lastNode) @@ -1543,23 +1530,23 @@ def getPhases(debug): else: commonAncestor.appendChild(lastNode) - # Step 8 + # Step 11 clone = formattingElement.cloneNode() - # Step 9 + # Step 12 furthestBlock.reparentChildren(clone) - # Step 10 + # Step 13 furthestBlock.appendChild(clone) - # Step 11 + # Step 14 self.tree.activeFormattingElements.remove(formattingElement) self.tree.activeFormattingElements.insert(bookmark, clone) - # Step 12 + # Step 15 self.tree.openElements.remove(formattingElement) self.tree.openElements.insert( - self.tree.openElements.index(furthestBlock) + 1, clone) + self.tree.openElements.index(furthestBlock) + 1, clone) def endTagAppletMarqueeObject(self, token): if self.tree.elementInScope(token["name"]): @@ -1575,7 +1562,7 @@ def getPhases(debug): def endTagBr(self, token): self.parser.parseError("unexpected-end-tag-treated-as", - {"originalName": "br", "newName": "br element"}) + {"originalName": "br", "newName": "br element"}) self.tree.reconstructActiveFormattingElements() self.tree.insertElement(impliedTagToken("br", "StartTag")) self.tree.openElements.pop() @@ -1600,31 +1587,31 @@ def getPhases(debug): self.startTagHandler = utils.MethodDispatcher([]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([ - ("script", self.endTagScript)]) + ("script", self.endTagScript)]) self.endTagHandler.default = self.endTagOther def processCharacters(self, token): self.tree.insertText(token["data"]) def processEOF(self): - self.parser.parseError("expected-named-closing-tag-but-got-eof", - self.tree.openElements[-1].name) + self.parser.parseError("expected-named-closing-tag-but-got-eof", + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase return True def startTagOther(self, token): - assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode"%token['name'] + assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name'] def endTagScript(self, token): node = self.tree.openElements.pop() assert node.name == "script" self.parser.phase = self.parser.originalPhase - #The rest of this method is all stuff that only happens if - #document.write works + # The rest of this method is all stuff that only happens if + # document.write works def endTagOther(self, token): - node = self.tree.openElements.pop() + self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase class InTablePhase(Phase): @@ -1656,7 +1643,7 @@ def getPhases(debug): def clearStackToTableContext(self): # "clear the stack back to a table context" while self.tree.openElements[-1].name not in ("table", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() # When the current node is it's an innerHTML case @@ -1667,7 +1654,7 @@ def getPhases(debug): self.parser.parseError("eof-in-table") else: assert self.parser.innerHTML - #Stop parsing + # Stop parsing def processSpaceCharacters(self, token): originalPhase = self.parser.phase @@ -1682,7 +1669,7 @@ def getPhases(debug): self.parser.phase.processCharacters(token) def insertText(self, token): - #If we get here there must be at least one non-whitespace character + # If we get here there must be at least one non-whitespace character # Do the table magic! self.tree.insertFromTable = True self.parser.phases["inBody"].processCharacters(token) @@ -1714,7 +1701,7 @@ def getPhases(debug): def startTagTable(self, token): self.parser.parseError("unexpected-start-tag-implies-end-tag", - {"startName": "table", "endName": "table"}) + {"startName": "table", "endName": "table"}) self.parser.phase.processEndTag(impliedTagToken("table")) if not self.parser.innerHTML: return token @@ -1723,8 +1710,8 @@ def getPhases(debug): return self.parser.phases["inHead"].processStartTag(token) def startTagInput(self, token): - if ("type" in token["data"] and - token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): + if ("type" in token["data"] and + token["data"]["type"].translate(asciiUpper2Lower) == "hidden"): self.parser.parseError("unexpected-hidden-input-in-table") self.tree.insertElement(token) # XXX associate with form @@ -1751,8 +1738,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "table": self.parser.parseError("end-tag-too-early-named", - {"gotName": "table", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "table", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "table": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1781,7 +1768,7 @@ def getPhases(debug): def flushCharacters(self): data = "".join([item["data"] for item in self.characterTokens]) if any([item not in spaceCharacters for item in data]): - token = {"type":tokenTypes["Characters"], "data":data} + token = {"type": tokenTypes["Characters"], "data": data} self.parser.phases["inTable"].insertText(token) elif data: self.tree.insertText(data) @@ -1798,12 +1785,12 @@ def getPhases(debug): return True def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.characterTokens.append(token) def processSpaceCharacters(self, token): - #pretty sure we should never reach here + # pretty sure we should never reach here self.characterTokens.append(token) # assert False @@ -1817,7 +1804,6 @@ def getPhases(debug): self.parser.phase = self.originalPhase return token - class InCaptionPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-caption def __init__(self, parser, tree): @@ -1849,7 +1835,7 @@ def getPhases(debug): def startTagTableElement(self, token): self.parser.parseError() - #XXX Have to duplicate logic here to find out if the tag is ignored + # XXX Have to duplicate logic here to find out if the tag is ignored ignoreEndTag = self.ignoreEndTagCaption() self.parser.phase.processEndTag(impliedTagToken("caption")) if not ignoreEndTag: @@ -1864,8 +1850,8 @@ def getPhases(debug): self.tree.generateImpliedEndTags() if self.tree.openElements[-1].name != "caption": self.parser.parseError("expected-one-end-tag-but-got-another", - {"gotName": "caption", - "expectedName": self.tree.openElements[-1].name}) + {"gotName": "caption", + "expectedName": self.tree.openElements[-1].name}) while self.tree.openElements[-1].name != "caption": self.tree.openElements.pop() self.tree.openElements.pop() @@ -1889,7 +1875,6 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) - class InColumnGroupPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-column @@ -1955,7 +1940,6 @@ def getPhases(debug): if not ignoreEndTag: return token - class InTableBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table0 def __init__(self, parser, tree): @@ -1980,8 +1964,8 @@ def getPhases(debug): # helper methods def clearStackToTableBodyContext(self): while self.tree.openElements[-1].name not in ("tbody", "tfoot", - "thead", "html"): - #self.parser.parseError("unexpected-implied-end-tag-in-table", + "thead", "html"): + # self.parser.parseError("unexpected-implied-end-tag-in-table", # {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() if self.tree.openElements[-1].name == "html": @@ -2003,7 +1987,7 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inRow"] def startTagTableCell(self, token): - self.parser.parseError("unexpected-cell-in-table-body", + self.parser.parseError("unexpected-cell-in-table-body", {"name": token["name"]}) self.startTagTr(impliedTagToken("tr", "StartTag")) return token @@ -2012,7 +1996,7 @@ def getPhases(debug): # XXX AT Any ideas on how to share this with endTagTable? if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2032,12 +2016,12 @@ def getPhases(debug): self.parser.phase = self.parser.phases["inTable"] else: self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagTable(self, token): if (self.tree.elementInScope("tbody", variant="table") or self.tree.elementInScope("thead", variant="table") or - self.tree.elementInScope("tfoot", variant="table")): + self.tree.elementInScope("tfoot", variant="table")): self.clearStackToTableBodyContext() self.endTagTableRowGroup( impliedTagToken(self.tree.openElements[-1].name)) @@ -2049,12 +2033,11 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-body", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) - class InRowPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-row def __init__(self, parser, tree): @@ -2072,7 +2055,7 @@ def getPhases(debug): ("table", self.endTagTable), (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), (("body", "caption", "col", "colgroup", "html", "td", "th"), - self.endTagIgnore) + self.endTagIgnore) ]) self.endTagHandler.default = self.endTagOther @@ -2080,7 +2063,7 @@ def getPhases(debug): def clearStackToTableRowContext(self): while self.tree.openElements[-1].name not in ("tr", "html"): self.parser.parseError("unexpected-implied-end-tag-in-table-row", - {"name": self.tree.openElements[-1].name}) + {"name": self.tree.openElements[-1].name}) self.tree.openElements.pop() def ignoreEndTagTr(self): @@ -2091,7 +2074,7 @@ def getPhases(debug): self.parser.phases["inTable"].processEOF() def processSpaceCharacters(self, token): - return self.parser.phases["inTable"].processSpaceCharacters(token) + return self.parser.phases["inTable"].processSpaceCharacters(token) def processCharacters(self, token): return self.parser.phases["inTable"].processCharacters(token) @@ -2139,7 +2122,7 @@ def getPhases(debug): def endTagIgnore(self, token): self.parser.parseError("unexpected-end-tag-in-table-row", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) @@ -2178,7 +2161,7 @@ def getPhases(debug): def startTagTableOther(self, token): if (self.tree.elementInScope("td", variant="table") or - self.tree.elementInScope("th", variant="table")): + self.tree.elementInScope("th", variant="table")): self.closeCell() return token else: @@ -2194,7 +2177,7 @@ def getPhases(debug): self.tree.generateImpliedEndTags(token["name"]) if self.tree.openElements[-1].name != token["name"]: self.parser.parseError("unexpected-cell-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) while True: node = self.tree.openElements.pop() if node.name == token["name"]: @@ -2249,7 +2232,7 @@ def getPhases(debug): assert self.parser.innerHTML def processCharacters(self, token): - if token["data"] == u"\u0000": + if token["data"] == "\u0000": return self.tree.insertText(token["data"]) @@ -2283,19 +2266,19 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-select", - {"name": token["name"]}) + {"name": token["name"]}) def endTagOption(self, token): if self.tree.openElements[-1].name == "option": self.tree.openElements.pop() else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "option"}) + {"name": "option"}) def endTagOptgroup(self, token): # implicitly closes if (self.tree.openElements[-1].name == "option" and - self.tree.openElements[-2].name == "optgroup"): + self.tree.openElements[-2].name == "optgroup"): self.tree.openElements.pop() # It also closes if self.tree.openElements[-1].name == "optgroup": @@ -2303,7 +2286,7 @@ def getPhases(debug): # But nothing else else: self.parser.parseError("unexpected-end-tag-in-select", - {"name": "optgroup"}) + {"name": "optgroup"}) def endTagSelect(self, token): if self.tree.elementInScope("select", variant="select"): @@ -2318,8 +2301,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-select", - {"name": token["name"]}) - + {"name": token["name"]}) class InSelectInTablePhase(Phase): def __init__(self, parser, tree): @@ -2360,64 +2342,64 @@ def getPhases(debug): def endTagOther(self, token): return self.parser.phases["inSelect"].processEndTag(token) - class InForeignContentPhase(Phase): - breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", + breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", - "em", "embed", "h1", "h2", "h3", + "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", - "li", "listing", "menu", "meta", "nobr", - "ol", "p", "pre", "ruby", "s", "small", - "span", "strong", "strike", "sub", "sup", + "li", "listing", "menu", "meta", "nobr", + "ol", "p", "pre", "ruby", "s", "small", + "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var"]) + def __init__(self, parser, tree): Phase.__init__(self, parser, tree) def adjustSVGTagNames(self, token): - replacements = {u"altglyph":u"altGlyph", - u"altglyphdef":u"altGlyphDef", - u"altglyphitem":u"altGlyphItem", - u"animatecolor":u"animateColor", - u"animatemotion":u"animateMotion", - u"animatetransform":u"animateTransform", - u"clippath":u"clipPath", - u"feblend":u"feBlend", - u"fecolormatrix":u"feColorMatrix", - u"fecomponenttransfer":u"feComponentTransfer", - u"fecomposite":u"feComposite", - u"feconvolvematrix":u"feConvolveMatrix", - u"fediffuselighting":u"feDiffuseLighting", - u"fedisplacementmap":u"feDisplacementMap", - u"fedistantlight":u"feDistantLight", - u"feflood":u"feFlood", - u"fefunca":u"feFuncA", - u"fefuncb":u"feFuncB", - u"fefuncg":u"feFuncG", - u"fefuncr":u"feFuncR", - u"fegaussianblur":u"feGaussianBlur", - u"feimage":u"feImage", - u"femerge":u"feMerge", - u"femergenode":u"feMergeNode", - u"femorphology":u"feMorphology", - u"feoffset":u"feOffset", - u"fepointlight":u"fePointLight", - u"fespecularlighting":u"feSpecularLighting", - u"fespotlight":u"feSpotLight", - u"fetile":u"feTile", - u"feturbulence":u"feTurbulence", - u"foreignobject":u"foreignObject", - u"glyphref":u"glyphRef", - u"lineargradient":u"linearGradient", - u"radialgradient":u"radialGradient", - u"textpath":u"textPath"} + replacements = {"altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath"} if token["name"] in replacements: token["name"] = replacements[token["name"]] def processCharacters(self, token): - if token["data"] == u"\u0000": - token["data"] = u"\uFFFD" - elif (self.parser.framesetOK and + if token["data"] == "\u0000": + token["data"] = "\uFFFD" + elif (self.parser.framesetOK and any(char not in spaceCharacters for char in token["data"])): self.parser.framesetOK = False Phase.processCharacters(self, token) @@ -2428,9 +2410,9 @@ def getPhases(debug): (token["name"] == "font" and set(token["data"].keys()) & set(["color", "face", "size"]))): self.parser.parseError("unexpected-html-element-in-foreign-content", - token["name"]) + {"name": token["name"]}) while (self.tree.openElements[-1].namespace != - self.tree.defaultNamespace and + self.tree.defaultNamespace and not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])): self.tree.openElements.pop() @@ -2453,11 +2435,11 @@ def getPhases(debug): nodeIndex = len(self.tree.openElements) - 1 node = self.tree.openElements[-1] if node.name != token["name"]: - self.parser.parseError("unexpected-end-tag", token["name"]) + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) while True: if node.name.translate(asciiUpper2Lower) == token["name"]: - #XXX this isn't in the spec but it seems necessary + # XXX this isn't in the spec but it seems necessary if self.parser.phase == self.parser.phases["inTableText"]: self.parser.phase.flushCharacters() self.parser.phase = self.parser.phase.originalPhase @@ -2475,21 +2457,20 @@ def getPhases(debug): break return new_token - class AfterBodyPhase(Phase): def __init__(self, parser, tree): Phase.__init__(self, parser, tree) self.startTagHandler = utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) + ("html", self.startTagHtml) + ]) self.startTagHandler.default = self.startTagOther self.endTagHandler = utils.MethodDispatcher([("html", self.endTagHtml)]) self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processComment(self, token): @@ -2507,11 +2488,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token - def endTagHtml(self,name): + def endTagHtml(self, name): if self.parser.innerHTML: self.parser.parseError("unexpected-end-tag-after-body-innerhtml") else: @@ -2519,7 +2500,7 @@ def getPhases(debug): def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-body", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2562,7 +2543,7 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-in-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagFrameset(self, token): if self.tree.openElements[-1].name == "html": @@ -2571,15 +2552,14 @@ def getPhases(debug): else: self.tree.openElements.pop() if (not self.parser.innerHTML and - self.tree.openElements[-1].name != "frameset"): + self.tree.openElements[-1].name != "frameset"): # If we're not in innerHTML mode and the the current node is not a # "frameset" element (anymore) then switch. self.parser.phase = self.parser.phases["afterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterFramesetPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#after3 @@ -2598,7 +2578,7 @@ def getPhases(debug): self.endTagHandler.default = self.endTagOther def processEOF(self): - #Stop parsing + # Stop parsing pass def processCharacters(self, token): @@ -2609,15 +2589,14 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("unexpected-start-tag-after-frameset", - {"name": token["name"]}) + {"name": token["name"]}) def endTagHtml(self, token): self.parser.phase = self.parser.phases["afterAfterFrameset"] def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-frameset", - {"name": token["name"]}) - + {"name": token["name"]}) class AfterAfterBodyPhase(Phase): def __init__(self, parser, tree): @@ -2647,13 +2626,13 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) + {"name": token["name"]}) self.parser.phase = self.parser.phases["inBody"] return token @@ -2687,12 +2666,11 @@ def getPhases(debug): def startTagOther(self, token): self.parser.parseError("expected-eof-but-got-start-tag", - {"name": token["name"]}) + {"name": token["name"]}) def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", - {"name": token["name"]}) - + {"name": token["name"]}) return { "initial": InitialPhase, @@ -2719,14 +2697,16 @@ def getPhases(debug): "afterAfterBody": AfterAfterBodyPhase, "afterAfterFrameset": AfterAfterFramesetPhase, # XXX after after frameset - } + } -def impliedTagToken(name, type="EndTag", attributes = None, - selfClosing = False): + +def impliedTagToken(name, type="EndTag", attributes=None, + selfClosing=False): if attributes is None: attributes = {} - return {"type":tokenTypes[type], "name":unicode(name), "data":attributes, - "selfClosing":selfClosing} + return {"type": tokenTypes[type], "name": name, "data": attributes, + "selfClosing": selfClosing} + class ParseError(Exception): """Error in parsed document""" diff --git a/libs/html5lib/ihatexml.py b/libs/html5lib/ihatexml.py index dd785639..0fc79308 100644 --- a/libs/html5lib/ihatexml.py +++ b/libs/html5lib/ihatexml.py @@ -1,25 +1,105 @@ -import re +from __future__ import absolute_import, division, unicode_literals -baseChar = """[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | [#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | [#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | [#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | [#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | [#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | [#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | [#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | [#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | [#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | [#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | [#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | [#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | [#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | [#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | [#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | [#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | [#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | [#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | [#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | [#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | [#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | [#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | [#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | [#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | [#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | [#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | [#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | [#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | #x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | #x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | #x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | [#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | [#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | #x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | [#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | [#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | [#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | [#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | [#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | #x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | [#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | [#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | [#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | [#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" +import re +import warnings + +from .constants import DataLossWarning + +baseChar = """ +[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | +[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | +[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | +[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | +[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | +[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | +[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | +[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | +[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | +[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | +[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | +[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | +[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | +[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | +[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | +[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | +[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | +[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | +[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | +[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | +[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | +[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | +[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | +[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | +[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | +[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | +[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | +[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | +[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | +[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | +#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | +#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | +#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | +[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | +[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | +#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | +[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | +[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | +[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | +[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | +[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | +#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | +[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | +[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | +[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | +[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]""" ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]""" -combiningCharacter = """[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | [#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | [#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | [#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | #x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | [#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | [#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | #x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | [#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | [#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | #x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | [#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | [#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | [#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | [#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | [#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | #x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | [#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | #x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | [#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | [#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | #x3099 | #x309A""" +combiningCharacter = """ +[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | +[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | +[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | +[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | +#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | +[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | +[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | +#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | +[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | +[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | +#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | +[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | +[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | +[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | +[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | +[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | +#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | +[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | +#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | +[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | +[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | +#x3099 | #x309A""" -digit = """[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | [#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | [#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | [#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" +digit = """ +[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | +[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | +[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | +[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]""" -extender = """#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | [#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" +extender = """ +#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | +#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]""" letter = " | ".join([baseChar, ideographic]) -#Without the -name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, - extender]) +# Without the +name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter, + extender]) nameFirst = " | ".join([letter, "_"]) reChar = re.compile(r"#x([\d|A-F]{4,4})") reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]") + def charStringToList(chars): charRanges = [item.strip() for item in chars.split(" | ")] rv = [] @@ -30,16 +110,17 @@ def charStringToList(chars): if match is not None: rv.append([hexToInt(item) for item in match.groups()]) if len(rv[-1]) == 1: - rv[-1] = rv[-1]*2 + rv[-1] = rv[-1] * 2 foundMatch = True break if not foundMatch: assert len(item) == 1 - + rv.append([ord(item)] * 2) rv = normaliseCharList(rv) return rv + def normaliseCharList(charList): charList = sorted(charList) for item in charList: @@ -49,61 +130,69 @@ def normaliseCharList(charList): while i < len(charList): j = 1 rv.append(charList[i]) - while i + j < len(charList) and charList[i+j][0] <= rv[-1][1] + 1: - rv[-1][1] = charList[i+j][1] + while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1: + rv[-1][1] = charList[i + j][1] j += 1 i += j return rv -#We don't really support characters above the BMP :( +# We don't really support characters above the BMP :( max_unicode = int("FFFF", 16) - + + def missingRanges(charList): rv = [] if charList[0] != 0: rv.append([0, charList[0][0] - 1]) for i, item in enumerate(charList[:-1]): - rv.append([item[1]+1, charList[i+1][0] - 1]) + rv.append([item[1] + 1, charList[i + 1][0] - 1]) if charList[-1][1] != max_unicode: rv.append([charList[-1][1] + 1, max_unicode]) return rv + def listToRegexpStr(charList): rv = [] for item in charList: if item[0] == item[1]: - rv.append(escapeRegexp(unichr(item[0]))) + rv.append(escapeRegexp(chr(item[0]))) else: - rv.append(escapeRegexp(unichr(item[0])) + "-" + - escapeRegexp(unichr(item[1]))) - return "[%s]"%"".join(rv) + rv.append(escapeRegexp(chr(item[0])) + "-" + + escapeRegexp(chr(item[1]))) + return "[%s]" % "".join(rv) + def hexToInt(hex_str): return int(hex_str, 16) + def escapeRegexp(string): specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}", - "[", "]", "|", "(", ")", "-") + "[", "]", "|", "(", ")", "-") for char in specialCharacters: string = string.replace(char, "\\" + char) - if char in string: - print string return string -#output from the above -nonXmlNameBMPRegexp = re.compile(u'[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') +# output from the above +nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') + +# Simpler things +nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\-\'()+,./:=?;!*#@$_%]") -nonXmlNameFirstBMPRegexp = re.compile(u'[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') class InfosetFilter(object): replacementRegexp = re.compile(r"U[\dA-F]{5,5}") - def __init__(self, replaceChars = None, - dropXmlnsLocalName = False, - dropXmlnsAttrNs = False, - preventDoubleDashComments = False, - preventDashAtCommentEnd = False, - replaceFormFeedCharacters = True): + + def __init__(self, replaceChars=None, + dropXmlnsLocalName=False, + dropXmlnsAttrNs=False, + preventDoubleDashComments=False, + preventDashAtCommentEnd=False, + replaceFormFeedCharacters=True, + preventSingleQuotePubid=False): self.dropXmlnsLocalName = dropXmlnsLocalName self.dropXmlnsAttrNs = dropXmlnsAttrNs @@ -113,14 +202,17 @@ class InfosetFilter(object): self.replaceFormFeedCharacters = replaceFormFeedCharacters + self.preventSingleQuotePubid = preventSingleQuotePubid + self.replaceCache = {} def coerceAttribute(self, name, namespace=None): if self.dropXmlnsLocalName and name.startswith("xmlns:"): - #Need a datalosswarning here + warnings.warn("Attributes cannot begin with xmlns", DataLossWarning) return None - elif (self.dropXmlnsAttrNs and + elif (self.dropXmlnsAttrNs and namespace == "http://www.w3.org/2000/xmlns/"): + warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning) return None else: return self.toXmlName(name) @@ -131,20 +223,35 @@ class InfosetFilter(object): def coerceComment(self, data): if self.preventDoubleDashComments: while "--" in data: + warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning) data = data.replace("--", "- -") return data - + def coerceCharacters(self, data): if self.replaceFormFeedCharacters: + for i in range(data.count("\x0C")): + warnings.warn("Text cannot contain U+000C", DataLossWarning) data = data.replace("\x0C", " ") - #Other non-xml characters + # Other non-xml characters return data + def coercePubid(self, data): + dataOutput = data + for char in nonPubidCharRegexp.findall(data): + warnings.warn("Coercing non-XML pubid", DataLossWarning) + replacement = self.getReplacementCharacter(char) + dataOutput = dataOutput.replace(char, replacement) + if self.preventSingleQuotePubid and dataOutput.find("'") >= 0: + warnings.warn("Pubid cannot contain single quote", DataLossWarning) + dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'")) + return dataOutput + def toXmlName(self, name): nameFirst = name[0] nameRest = name[1:] m = nonXmlNameFirstBMPRegexp.match(nameFirst) if m: + warnings.warn("Coercing non-XML name", DataLossWarning) nameFirstOutput = self.getReplacementCharacter(nameFirst) else: nameFirstOutput = nameFirst @@ -152,10 +259,11 @@ class InfosetFilter(object): nameRestOutput = nameRest replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest)) for char in replaceChars: + warnings.warn("Coercing non-XML name", DataLossWarning) replacement = self.getReplacementCharacter(char) nameRestOutput = nameRestOutput.replace(char, replacement) return nameFirstOutput + nameRestOutput - + def getReplacementCharacter(self, char): if char in self.replaceCache: replacement = self.replaceCache[char] @@ -169,9 +277,9 @@ class InfosetFilter(object): return name def escapeChar(self, char): - replacement = "U" + hex(ord(char))[2:].upper().rjust(5, "0") + replacement = "U%05X" % ord(char) self.replaceCache[char] = replacement return replacement def unescapeChar(self, charcode): - return unichr(int(charcode[1:], 16)) + return chr(int(charcode[1:], 16)) diff --git a/libs/html5lib/inputstream.py b/libs/html5lib/inputstream.py index edec1329..004bdd4a 100644 --- a/libs/html5lib/inputstream.py +++ b/libs/html5lib/inputstream.py @@ -1,19 +1,33 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + import codecs import re -import types -import sys -from constants import EOF, spaceCharacters, asciiLetters, asciiUppercase -from constants import encodings, ReparseException -import utils +from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase +from .constants import encodings, ReparseException +from . import utils -#Non-unicode versions of constants for use in the pre-parser -spaceCharactersBytes = frozenset([str(item) for item in spaceCharacters]) -asciiLettersBytes = frozenset([str(item) for item in asciiLetters]) -asciiUppercaseBytes = frozenset([str(item) for item in asciiUppercase]) -spacesAngleBrackets = spaceCharactersBytes | frozenset([">", "<"]) +from io import StringIO -invalid_unicode_re = re.compile(u"[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") +try: + from io import BytesIO +except ImportError: + BytesIO = StringIO + +try: + from io import BufferedIOBase +except ImportError: + class BufferedIOBase(object): + pass + +# Non-unicode versions of constants for use in the pre-parser +spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters]) +asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters]) +asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase]) +spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"]) + +invalid_unicode_re = re.compile("[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]") non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, @@ -23,22 +37,23 @@ non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF]) -ascii_punctuation_re = re.compile(ur"[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") +ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]") # Cache for charsUntil() charsUntilRegEx = {} - -class BufferedStream: + + +class BufferedStream(object): """Buffering for streams that do not have buffering of their own - The buffer is implemented as a list of chunks on the assumption that + The buffer is implemented as a list of chunks on the assumption that joining many strings will be slow since it is O(n**2) """ - + def __init__(self, stream): self.stream = stream self.buffer = [] - self.position = [-1,0] #chunk number, offset + self.position = [-1, 0] # chunk number, offset def tell(self): pos = 0 @@ -48,11 +63,11 @@ class BufferedStream: return pos def seek(self, pos): - assert pos < self._bufferedBytes() + assert pos <= self._bufferedBytes() offset = pos i = 0 while len(self.buffer[i]) < offset: - offset -= pos + offset -= len(self.buffer[i]) i += 1 self.position = [i, offset] @@ -64,7 +79,7 @@ class BufferedStream: return self._readStream(bytes) else: return self._readFromBuffer(bytes) - + def _bufferedBytes(self): return sum([len(item) for item in self.buffer]) @@ -83,7 +98,7 @@ class BufferedStream: while bufferIndex < len(self.buffer) and remainingBytes != 0: assert remainingBytes > 0 bufferedData = self.buffer[bufferIndex] - + if remainingBytes <= len(bufferedData) - bufferOffset: bytesToRead = remainingBytes self.position = [bufferIndex, bufferOffset + bytesToRead] @@ -91,20 +106,33 @@ class BufferedStream: bytesToRead = len(bufferedData) - bufferOffset self.position = [bufferIndex, len(bufferedData)] bufferIndex += 1 - data = rv.append(bufferedData[bufferOffset: - bufferOffset + bytesToRead]) + rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead]) remainingBytes -= bytesToRead bufferOffset = 0 if remainingBytes: rv.append(self._readStream(remainingBytes)) - - return "".join(rv) - + + return b"".join(rv) -class HTMLInputStream: +def HTMLInputStream(source, encoding=None, parseMeta=True, chardet=True): + if hasattr(source, "read"): + isUnicode = isinstance(source.read(0), text_type) + else: + isUnicode = isinstance(source, text_type) + + if isUnicode: + if encoding is not None: + raise TypeError("Cannot explicitly set an encoding with a unicode string") + + return HTMLUnicodeInputStream(source) + else: + return HTMLBinaryInputStream(source, encoding, parseMeta, chardet) + + +class HTMLUnicodeInputStream(object): """Provides a unicode stream of characters to the HTMLTokenizer. This class takes care of character encoding and removing or replacing @@ -114,7 +142,7 @@ class HTMLInputStream: _defaultChunkSize = 10240 - def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + def __init__(self, source): """Initialises the HTMLInputStream. HTMLInputStream(source, [encoding]) -> Normalized stream from source @@ -126,49 +154,29 @@ class HTMLInputStream: the encoding. If specified, that encoding will be used, regardless of any BOM or later declaration (such as in a meta element) - + parseMeta - Look for a element containing encoding information """ - #Craziness - if len(u"\U0010FFFF") == 1: + # Craziness + if len("\U0010FFFF") == 1: self.reportCharacterErrors = self.characterErrorsUCS4 - self.replaceCharactersRegexp = re.compile(u"[\uD800-\uDFFF]") + self.replaceCharactersRegexp = re.compile("[\uD800-\uDFFF]") else: self.reportCharacterErrors = self.characterErrorsUCS2 - self.replaceCharactersRegexp = re.compile(u"([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 1: lastv = ord(data[-1]) if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF: self._bufferedCharacter = data[-1] data = data[:-1] - + self.reportCharacterErrors(data) - + # Replace invalid characters # Note U+0000 is dealt with in the tokenizer - data = self.replaceCharactersRegexp.sub(u"\ufffd", data) - - data = data.replace(u"\r\n", u"\n") - data = data.replace(u"\r", u"\n") + data = self.replaceCharactersRegexp.sub("\ufffd", data) + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") self.chunk = data self.chunkSize = len(data) @@ -378,23 +275,22 @@ class HTMLInputStream: return True def characterErrorsUCS4(self, data): - for i in xrange(len(invalid_unicode_re.findall(data))): + for i in range(len(invalid_unicode_re.findall(data))): self.errors.append("invalid-codepoint") def characterErrorsUCS2(self, data): - #Someone picked the wrong compile option - #You lose + # Someone picked the wrong compile option + # You lose skip = False - import sys for match in invalid_unicode_re.finditer(data): if skip: continue codepoint = ord(match.group()) pos = match.start() - #Pretty sure there should be endianness issues here - if utils.isSurrogatePair(data[pos:pos+2]): - #We have a surrogate pair! - char_val = utils.surrogatePairToCodepoint(data[pos:pos+2]) + # Pretty sure there should be endianness issues here + if utils.isSurrogatePair(data[pos:pos + 2]): + # We have a surrogate pair! + char_val = utils.surrogatePairToCodepoint(data[pos:pos + 2]) if char_val in non_bmp_invalid_codepoints: self.errors.append("invalid-codepoint") skip = True @@ -405,7 +301,7 @@ class HTMLInputStream: skip = False self.errors.append("invalid-codepoint") - def charsUntil(self, characters, opposite = False): + def charsUntil(self, characters, opposite=False): """ Returns a string of characters from the stream up to but not including any character in 'characters' or EOF. 'characters' must be a container that supports the 'in' method and iteration over its @@ -417,12 +313,12 @@ class HTMLInputStream: chars = charsUntilRegEx[(characters, opposite)] except KeyError: if __debug__: - for c in characters: + for c in characters: assert(ord(c) < 128) - regex = u"".join([u"\\x%02x" % ord(c) for c in characters]) + regex = "".join(["\\x%02x" % ord(c) for c in characters]) if not opposite: - regex = u"^%s" % regex - chars = charsUntilRegEx[(characters, opposite)] = re.compile(u"[%s]+" % regex) + regex = "^%s" % regex + chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex) rv = [] @@ -449,7 +345,7 @@ class HTMLInputStream: # Reached EOF break - r = u"".join(rv) + r = "".join(rv) return r def unget(self, char): @@ -468,26 +364,210 @@ class HTMLInputStream: self.chunkOffset -= 1 assert self.chunk[self.chunkOffset] == char -class EncodingBytes(str): + +class HTMLBinaryInputStream(HTMLUnicodeInputStream): + """Provides a unicode stream of characters to the HTMLTokenizer. + + This class takes care of character encoding and removing or replacing + incorrect byte-sequences and also provides column and line tracking. + + """ + + def __init__(self, source, encoding=None, parseMeta=True, chardet=True): + """Initialises the HTMLInputStream. + + HTMLInputStream(source, [encoding]) -> Normalized stream from source + for use by html5lib. + + source can be either a file-object, local filename or a string. + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + + parseMeta - Look for a element containing encoding information + + """ + # Raw Stream - for unicode objects this will encode to utf-8 and set + # self.charEncoding as appropriate + self.rawStream = self.openStream(source) + + HTMLUnicodeInputStream.__init__(self, self.rawStream) + + self.charEncoding = (codecName(encoding), "certain") + + # Encoding Information + # Number of bytes to use when looking for a meta element with + # encoding information + self.numBytesMeta = 512 + # Number of bytes to use when using detecting encoding using chardet + self.numBytesChardet = 100 + # Encoding to use if no other information can be found + self.defaultEncoding = "windows-1252" + + # Detect encoding iff no explicit "transport level" encoding is supplied + if (self.charEncoding[0] is None): + self.charEncoding = self.detectEncoding(parseMeta, chardet) + + # Call superclass + self.reset() + + def reset(self): + self.dataStream = codecs.getreader(self.charEncoding[0])(self.rawStream, + 'replace') + HTMLUnicodeInputStream.reset(self) + + def openStream(self, source): + """Produces a file object from source. + + source can be either a file object, local filename or a string. + + """ + # Already a file object + if hasattr(source, 'read'): + stream = source + else: + stream = BytesIO(source) + + try: + stream.seek(stream.tell()) + except: + stream = BufferedStream(stream) + + return stream + + def detectEncoding(self, parseMeta=True, chardet=True): + # First look for a BOM + # This will also read past the BOM if present + encoding = self.detectBOM() + confidence = "certain" + # If there is no BOM need to look for meta elements with encoding + # information + if encoding is None and parseMeta: + encoding = self.detectEncodingMeta() + confidence = "tentative" + # Guess with chardet, if avaliable + if encoding is None and chardet: + confidence = "tentative" + try: + try: + from charade.universaldetector import UniversalDetector + except ImportError: + from chardet.universaldetector import UniversalDetector + buffers = [] + detector = UniversalDetector() + while not detector.done: + buffer = self.rawStream.read(self.numBytesChardet) + assert isinstance(buffer, bytes) + if not buffer: + break + buffers.append(buffer) + detector.feed(buffer) + detector.close() + encoding = detector.result['encoding'] + self.rawStream.seek(0) + except ImportError: + pass + # If all else fails use the default encoding + if encoding is None: + confidence = "tentative" + encoding = self.defaultEncoding + + # Substitute for equivalent encodings: + encodingSub = {"iso-8859-1": "windows-1252"} + + if encoding.lower() in encodingSub: + encoding = encodingSub[encoding.lower()] + + return encoding, confidence + + def changeEncoding(self, newEncoding): + assert self.charEncoding[1] != "certain" + newEncoding = codecName(newEncoding) + if newEncoding in ("utf-16", "utf-16-be", "utf-16-le"): + newEncoding = "utf-8" + if newEncoding is None: + return + elif newEncoding == self.charEncoding[0]: + self.charEncoding = (self.charEncoding[0], "certain") + else: + self.rawStream.seek(0) + self.reset() + self.charEncoding = (newEncoding, "certain") + raise ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding)) + + def detectBOM(self): + """Attempts to detect at BOM at the start of the stream. If + an encoding can be determined from the BOM return the name of the + encoding otherwise return None""" + bomDict = { + codecs.BOM_UTF8: 'utf-8', + codecs.BOM_UTF16_LE: 'utf-16-le', codecs.BOM_UTF16_BE: 'utf-16-be', + codecs.BOM_UTF32_LE: 'utf-32-le', codecs.BOM_UTF32_BE: 'utf-32-be' + } + + # Go to beginning of file and read in 4 bytes + string = self.rawStream.read(4) + assert isinstance(string, bytes) + + # Try detecting the BOM using bytes from the string + encoding = bomDict.get(string[:3]) # UTF-8 + seek = 3 + if not encoding: + # Need to detect UTF-32 before UTF-16 + encoding = bomDict.get(string) # UTF-32 + seek = 4 + if not encoding: + encoding = bomDict.get(string[:2]) # UTF-16 + seek = 2 + + # Set the read position past the BOM if one was found, otherwise + # set it to the start of the stream + self.rawStream.seek(encoding and seek or 0) + + return encoding + + def detectEncodingMeta(self): + """Report the encoding declared by the meta element + """ + buffer = self.rawStream.read(self.numBytesMeta) + assert isinstance(buffer, bytes) + parser = EncodingParser(buffer) + self.rawStream.seek(0) + encoding = parser.getEncoding() + + if encoding in ("utf-16", "utf-16-be", "utf-16-le"): + encoding = "utf-8" + + return encoding + + +class EncodingBytes(bytes): """String-like object with an associated position and various extra methods If the position is ever greater than the string length then an exception is raised""" def __new__(self, value): - return str.__new__(self, value.lower()) + assert isinstance(value, bytes) + return bytes.__new__(self, value.lower()) def __init__(self, value): - self._position=-1 - + self._position = -1 + def __iter__(self): return self - - def next(self): + + def __next__(self): p = self._position = self._position + 1 if p >= len(self): raise StopIteration elif p < 0: raise TypeError - return self[p] + return self[p:p + 1] + + def next(self): + # Py2 compat + return self.__next__() def previous(self): p = self._position @@ -496,13 +576,13 @@ class EncodingBytes(str): elif p < 0: raise TypeError self._position = p = p - 1 - return self[p] - + return self[p:p + 1] + def setPosition(self, position): if self._position >= len(self): raise StopIteration self._position = position - + def getPosition(self): if self._position >= len(self): raise StopIteration @@ -510,19 +590,19 @@ class EncodingBytes(str): return self._position else: return None - + position = property(getPosition, setPosition) def getCurrentByte(self): - return self[self.position] - + return self[self.position:self.position + 1] + currentByte = property(getCurrentByte) def skip(self, chars=spaceCharactersBytes): """Skip past a list of characters""" p = self.position # use property for the error-checking while p < len(self): - c = self[p] + c = self[p:p + 1] if c not in chars: self._position = p return c @@ -533,7 +613,7 @@ class EncodingBytes(str): def skipUntil(self, chars): p = self.position while p < len(self): - c = self[p] + c = self[p:p + 1] if c in chars: self._position = p return c @@ -542,16 +622,16 @@ class EncodingBytes(str): return None def matchBytes(self, bytes): - """Look for a sequence of bytes at the start of a string. If the bytes - are found return True and advance the position to the byte after the + """Look for a sequence of bytes at the start of a string. If the bytes + are found return True and advance the position to the byte after the match. Otherwise return False and leave the position alone""" p = self.position - data = self[p:p+len(bytes)] + data = self[p:p + len(bytes)] rv = data.startswith(bytes) if rv: self.position += len(bytes) return rv - + def jumpTo(self, bytes): """Look for the next sequence of bytes matching a given sequence. If a match is found advance the position to the last byte of the match""" @@ -560,11 +640,12 @@ class EncodingBytes(str): # XXX: This is ugly, but I can't see a nicer way to fix this. if self._position == -1: self._position = 0 - self._position += (newPosition + len(bytes)-1) + self._position += (newPosition + len(bytes) - 1) return True else: raise StopIteration + class EncodingParser(object): """Mini parser for detecting character encoding from meta elements""" @@ -575,147 +656,158 @@ class EncodingParser(object): def getEncoding(self): methodDispatch = ( - ("") + return self.data.jumpTo(b"-->") def handleMeta(self): if self.data.currentByte not in spaceCharactersBytes: - #if we have ") + return self.data.jumpTo(b">") def getAttribute(self): - """Return a name,value pair for the next attribute in the stream, + """Return a name,value pair for the next attribute in the stream, if one is found, or None""" data = self.data # Step 1 (skip chars) - c = data.skip(spaceCharactersBytes | frozenset("/")) + c = data.skip(spaceCharactersBytes | frozenset([b"/"])) + assert c is None or len(c) == 1 # Step 2 - if c in (">", None): + if c in (b">", None): return None # Step 3 attrName = [] attrValue = [] - #Step 4 attribute name + # Step 4 attribute name while True: - if c == "=" and attrName: + if c == b"=" and attrName: break elif c in spaceCharactersBytes: - #Step 6! + # Step 6! c = data.skip() - c = data.next() break - elif c in ("/", ">"): - return "".join(attrName), "" + elif c in (b"/", b">"): + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrName.append(c.lower()) - elif c == None: + elif c is None: return None else: attrName.append(c) - #Step 5 - c = data.next() - #Step 7 - if c != "=": + # Step 5 + c = next(data) + # Step 7 + if c != b"=": data.previous() - return "".join(attrName), "" - #Step 8 - data.next() - #Step 9 + return b"".join(attrName), b"" + # Step 8 + next(data) + # Step 9 c = data.skip() - #Step 10 - if c in ("'", '"'): - #10.1 + # Step 10 + if c in (b"'", b'"'): + # 10.1 quoteChar = c while True: - #10.2 - c = data.next() - #10.3 + # 10.2 + c = next(data) + # 10.3 if c == quoteChar: - data.next() - return "".join(attrName), "".join(attrValue) - #10.4 + next(data) + return b"".join(attrName), b"".join(attrValue) + # 10.4 elif c in asciiUppercaseBytes: attrValue.append(c.lower()) - #10.5 + # 10.5 else: attrValue.append(c) - elif c == ">": - return "".join(attrName), "" + elif c == b">": + return b"".join(attrName), b"" elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -724,9 +816,9 @@ class EncodingParser(object): attrValue.append(c) # Step 11 while True: - c = data.next() + c = next(data) if c in spacesAngleBrackets: - return "".join(attrName), "".join(attrValue) + return b"".join(attrName), b"".join(attrValue) elif c in asciiUppercaseBytes: attrValue.append(c.lower()) elif c is None: @@ -737,21 +829,23 @@ class EncodingParser(object): class ContentAttrParser(object): def __init__(self, data): + assert isinstance(data, bytes) self.data = data + def parse(self): try: - #Check if the attr name is charset - #otherwise return - self.data.jumpTo("charset") + # Check if the attr name is charset + # otherwise return + self.data.jumpTo(b"charset") self.data.position += 1 self.data.skip() - if not self.data.currentByte == "=": - #If there is no = sign keep looking for attrs + if not self.data.currentByte == b"=": + # If there is no = sign keep looking for attrs return None self.data.position += 1 self.data.skip() - #Look for an encoding between matching quote marks - if self.data.currentByte in ('"', "'"): + # Look for an encoding between matching quote marks + if self.data.currentByte in (b'"', b"'"): quoteMark = self.data.currentByte self.data.position += 1 oldPosition = self.data.position @@ -760,13 +854,13 @@ class ContentAttrParser(object): else: return None else: - #Unquoted value + # Unquoted value oldPosition = self.data.position try: self.data.skipUntil(spaceCharactersBytes) return self.data[oldPosition:self.data.position] except StopIteration: - #Return the whole remaining value + # Return the whole remaining value return self.data[oldPosition:] except StopIteration: return None @@ -775,7 +869,12 @@ class ContentAttrParser(object): def codecName(encoding): """Return the python codec name corresponding to an encoding or None if the string doesn't correspond to a valid encoding.""" - if (encoding is not None and type(encoding) in types.StringTypes): + if isinstance(encoding, bytes): + try: + encoding = encoding.decode("ascii") + except UnicodeDecodeError: + return None + if encoding: canonicalName = ascii_punctuation_re.sub("", encoding).lower() return encodings.get(canonicalName, None) else: diff --git a/libs/html5lib/sanitizer.py b/libs/html5lib/sanitizer.py index ae4c7d83..71dc5212 100644 --- a/libs/html5lib/sanitizer.py +++ b/libs/html5lib/sanitizer.py @@ -1,142 +1,145 @@ +from __future__ import absolute_import, division, unicode_literals + import re from xml.sax.saxutils import escape, unescape -from tokenizer import HTMLTokenizer -from constants import tokenTypes +from .tokenizer import HTMLTokenizer +from .constants import tokenTypes + class HTMLSanitizerMixin(object): """ sanitization of XHTML+MathML+SVG and of inline style attributes.""" acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] - + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi', - 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', - 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', - 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', - 'munderover', 'none'] - + 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', + 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', + 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none'] + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', - 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', - 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', - 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', - 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', - 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] - + 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', + 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', + 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', - 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', - 'background', 'balance', 'bgcolor', 'bgproperties', 'border', - 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', - 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', - 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', - 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', - 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', - 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', - 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', - 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', - 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', - 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', - 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', - 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', - 'optimum', 'pattern', 'ping', 'point-size', 'prompt', 'pqg', - 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', - 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', - 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', - 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', - 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', - 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', - 'width', 'wrap', 'xml:lang'] + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', + 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', + 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', + 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', + 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', + 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', + 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', + 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', + 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', + 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', + 'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload', + 'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', + 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', + 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', + 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', + 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', + 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', + 'width', 'wrap', 'xml:lang'] mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', - 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', - 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', - 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', - 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', - 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', - 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', - 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', - 'xlink:type', 'xmlns', 'xmlns:xlink'] - - svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', - 'arabic-form', 'ascent', 'attributeName', 'attributeType', - 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', - 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', - 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', - 'fill-opacity', 'fill-rule', 'font-family', 'font-size', - 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', - 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', - 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', - 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', - 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', - 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', - 'opacity', 'orient', 'origin', 'overline-position', - 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', - 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', - 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', - 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', - 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', - 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', - 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', - 'transform', 'type', 'u1', 'u2', 'underline-position', - 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', - 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', - 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', - 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', - 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', - 'y1', 'y2', 'zoomAndPan'] + 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', + 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', + 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', + 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', + 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', + 'xlink:type', 'xmlns', 'xmlns:xlink'] - attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', - 'xlink:href', 'xml:base'] + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', + 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', + 'fill-opacity', 'fill-rule', 'font-family', 'font-size', + 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', + 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', + 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', + 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', + 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', + 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', + 'opacity', 'orient', 'origin', 'overline-position', + 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', + 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', + 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', + 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', + 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', + 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', + 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', + 'transform', 'type', 'u1', 'u2', 'underline-position', + 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', + 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', + 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', + 'y1', 'y2', 'zoomAndPan'] + + attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster', + 'xlink:href', 'xml:base'] svg_attr_val_allows_ref = ['clip-path', 'color-profile', 'cursor', 'fill', - 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', - 'mask', 'stroke'] + 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', + 'mask', 'stroke'] svg_allow_local_href = ['altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', - 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', - 'set', 'use'] - + 'animateMotion', 'animateTransform', 'cursor', 'feImage', 'filter', + 'linearGradient', 'pattern', 'radialGradient', 'textpath', 'tref', + 'set', 'use'] + acceptable_css_properties = ['azimuth', 'background-color', - 'border-bottom-color', 'border-collapse', 'border-color', - 'border-left-color', 'border-right-color', 'border-top-color', 'clear', - 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', - 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', - 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', - 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', - 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', - 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', - 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', - 'white-space', 'width'] - + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', - 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', - 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', - 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', - 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', - 'transparent', 'underline', 'white', 'yellow'] - - acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', - 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', - 'stroke-opacity'] - - acceptable_protocols = [ 'ed2k', 'ftp', 'http', 'https', 'irc', - 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', - 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', - 'ssh', 'sftp', 'rtsp', 'afs' ] - + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + acceptable_svg_properties = ['fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] + + acceptable_protocols = ['ed2k', 'ftp', 'http', 'https', 'irc', + 'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal', + 'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag', + 'ssh', 'sftp', 'rtsp', 'afs'] + # subclasses may define their own versions of these constants allowed_elements = acceptable_elements + mathml_elements + svg_elements allowed_attributes = acceptable_attributes + mathml_attributes + svg_attributes @@ -160,94 +163,104 @@ class HTMLSanitizerMixin(object): # accommodate filters which use token_type differently token_type = token["type"] - if token_type in tokenTypes.keys(): - token_type = tokenTypes[token_type] + if token_type in list(tokenTypes.keys()): + token_type = tokenTypes[token_type] - if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], - tokenTypes["EmptyTag"]): + if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"]): if token["name"] in self.allowed_elements: - if token.has_key("data"): - attrs = dict([(name,val) for name,val in - token["data"][::-1] - if name in self.allowed_attributes]) - for attr in self.attr_val_is_uri: - if not attrs.has_key(attr): - continue - val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', - unescape(attrs[attr])).lower() - #remove replacement characters from unescaped characters - val_unescaped = val_unescaped.replace(u"\ufffd", "") - if (re.match("^[a-z0-9][-+.a-z0-9]*:",val_unescaped) and - (val_unescaped.split(':')[0] not in - self.allowed_protocols)): - del attrs[attr] - for attr in self.svg_attr_val_allows_ref: - if attr in attrs: - attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', - ' ', - unescape(attrs[attr])) - if (token["name"] in self.svg_allow_local_href and - 'xlink:href' in attrs and re.search('^\s*[^#\s].*', - attrs['xlink:href'])): - del attrs['xlink:href'] - if attrs.has_key('style'): - attrs['style'] = self.sanitize_css(attrs['style']) - token["data"] = [[name,val] for name,val in attrs.items()] - return token + return self.allowed_token(token, token_type) else: - if token_type == tokenTypes["EndTag"]: - token["data"] = "%s>" % token["name"] - elif token["data"]: - attrs = ''.join([' %s="%s"' % (k,escape(v)) for k,v in token["data"]]) - token["data"] = "<%s%s>" % (token["name"],attrs) - else: - token["data"] = "<%s>" % token["name"] - if token.get("selfClosing"): - token["data"]=token["data"][:-1] + "/>" - - if token["type"] in tokenTypes.keys(): - token["type"] = "Characters" - else: - token["type"] = tokenTypes["Characters"] - - del token["name"] - return token + return self.disallowed_token(token, token_type) elif token_type == tokenTypes["Comment"]: pass else: return token + def allowed_token(self, token, token_type): + if "data" in token: + attrs = dict([(name, val) for name, val in + token["data"][::-1] + if name in self.allowed_attributes]) + for attr in self.attr_val_is_uri: + if attr not in attrs: + continue + val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '', + unescape(attrs[attr])).lower() + # remove replacement characters from unescaped characters + val_unescaped = val_unescaped.replace("\ufffd", "") + if (re.match("^[a-z0-9][-+.a-z0-9]*:", val_unescaped) and + (val_unescaped.split(':')[0] not in + self.allowed_protocols)): + del attrs[attr] + for attr in self.svg_attr_val_allows_ref: + if attr in attrs: + attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', + ' ', + unescape(attrs[attr])) + if (token["name"] in self.svg_allow_local_href and + 'xlink:href' in attrs and re.search('^\s*[^#\s].*', + attrs['xlink:href'])): + del attrs['xlink:href'] + if 'style' in attrs: + attrs['style'] = self.sanitize_css(attrs['style']) + token["data"] = [[name, val] for name, val in list(attrs.items())] + return token + + def disallowed_token(self, token, token_type): + if token_type == tokenTypes["EndTag"]: + token["data"] = "%s>" % token["name"] + elif token["data"]: + attrs = ''.join([' %s="%s"' % (k, escape(v)) for k, v in token["data"]]) + token["data"] = "<%s%s>" % (token["name"], attrs) + else: + token["data"] = "<%s>" % token["name"] + if token.get("selfClosing"): + token["data"] = token["data"][:-1] + "/>" + + if token["type"] in list(tokenTypes.keys()): + token["type"] = "Characters" + else: + token["type"] = tokenTypes["Characters"] + + del token["name"] + return token + def sanitize_css(self, style): # disallow urls - style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) + style = re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) # gauntlet - if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' - if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): return '' + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): + return '' clean = [] - for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): - if not value: continue - if prop.lower() in self.allowed_css_properties: - clean.append(prop + ': ' + value + ';') - elif prop.split('-')[0].lower() in ['background','border','margin', - 'padding']: - for keyword in value.split(): - if not keyword in self.acceptable_css_keywords and \ - not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$",keyword): - break - else: - clean.append(prop + ': ' + value + ';') - elif prop.lower() in self.allowed_svg_properties: - clean.append(prop + ': ' + value + ';') + for prop, value in re.findall("([-\w]+)\s*:\s*([^:;]*)", style): + if not value: + continue + if prop.lower() in self.allowed_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background', 'border', 'margin', + 'padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif prop.lower() in self.allowed_svg_properties: + clean.append(prop + ': ' + value + ';') return ' '.join(clean) + class HTMLSanitizer(HTMLTokenizer, HTMLSanitizerMixin): def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True, lowercaseElementName=False, lowercaseAttrName=False, parser=None): - #Change case matching defaults as we only output lowercase html anyway - #This solution doesn't seem ideal... + # Change case matching defaults as we only output lowercase html anyway + # This solution doesn't seem ideal... HTMLTokenizer.__init__(self, stream, encoding, parseMeta, useChardet, lowercaseElementName, lowercaseAttrName, parser=parser) diff --git a/libs/html5lib/serializer/__init__.py b/libs/html5lib/serializer/__init__.py index 1b746655..8380839a 100644 --- a/libs/html5lib/serializer/__init__.py +++ b/libs/html5lib/serializer/__init__.py @@ -1,17 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals -from html5lib import treewalkers +from .. import treewalkers -from htmlserializer import HTMLSerializer -from xhtmlserializer import XHTMLSerializer +from .htmlserializer import HTMLSerializer -def serialize(input, tree="simpletree", format="html", encoding=None, + +def serialize(input, tree="etree", format="html", encoding=None, **serializer_opts): # XXX: Should we cache this? - walker = treewalkers.getTreeWalker(tree) + walker = treewalkers.getTreeWalker(tree) if format == "html": s = HTMLSerializer(**serializer_opts) - elif format == "xhtml": - s = XHTMLSerializer(**serializer_opts) else: - raise ValueError, "type must be either html or xhtml" + raise ValueError("type must be html") return s.render(walker(input), encoding) diff --git a/libs/html5lib/serializer/htmlserializer.py b/libs/html5lib/serializer/htmlserializer.py index 8dd0a815..412a5a22 100644 --- a/libs/html5lib/serializer/htmlserializer.py +++ b/libs/html5lib/serializer/htmlserializer.py @@ -1,18 +1,20 @@ -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type import gettext _ = gettext.gettext -from html5lib.constants import voidElements, booleanAttributes, spaceCharacters -from html5lib.constants import rcdataElements, entities, xmlEntities -from html5lib import utils +try: + from functools import reduce +except ImportError: + pass + +from ..constants import voidElements, booleanAttributes, spaceCharacters +from ..constants import rcdataElements, entities, xmlEntities +from .. import utils from xml.sax.saxutils import escape -spaceCharacters = u"".join(spaceCharacters) +spaceCharacters = "".join(spaceCharacters) try: from codecs import register_error, xmlcharrefreplace_errors @@ -21,24 +23,18 @@ except ImportError: else: unicode_encode_errors = "htmlentityreplace" - from html5lib.constants import entities - encode_entity_map = {} - is_ucs4 = len(u"\U0010FFFF") == 1 - for k, v in entities.items(): - #skip multi-character entities + is_ucs4 = len("\U0010FFFF") == 1 + for k, v in list(entities.items()): + # skip multi-character entities if ((is_ucs4 and len(v) > 1) or - (not is_ucs4 and len(v) > 2)): + (not is_ucs4 and len(v) > 2)): continue if v != "&": if len(v) == 2: v = utils.surrogatePairToCodepoint(v) else: - try: - v = ord(v) - except: - print v - raise + v = ord(v) if not v in encode_entity_map or k.islower(): # prefer < over < and similarly for &, >, etc. encode_entity_map[v] = k @@ -53,8 +49,8 @@ else: skip = False continue index = i + exc.start - if utils.isSurrogatePair(exc.object[index:min([exc.end, index+2])]): - codepoint = utils.surrogatePairToCodepoint(exc.object[index:index+2]) + if utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]): + codepoint = utils.surrogatePairToCodepoint(exc.object[index:index + 2]) skip = True else: codepoint = ord(c) @@ -67,8 +63,8 @@ else: if not e.endswith(";"): res.append(";") else: - res.append("%s;"%(hex(cp)[2:])) - return (u"".join(res), exc.end) + res.append("%s;" % (hex(cp)[2:])) + return ("".join(res), exc.end) else: return xmlcharrefreplace_errors(exc) @@ -81,7 +77,7 @@ class HTMLSerializer(object): # attribute quoting options quote_attr_values = False - quote_char = u'"' + quote_char = '"' use_best_quote_char = True # tag syntax options @@ -96,15 +92,17 @@ class HTMLSerializer(object): resolve_entities = True # miscellaneous options + alphabetical_attributes = False inject_meta_charset = True strip_whitespace = False sanitize = False options = ("quote_attr_values", "quote_char", "use_best_quote_char", - "minimize_boolean_attributes", "use_trailing_solidus", - "space_before_trailing_solidus", "omit_optional_tags", - "strip_whitespace", "inject_meta_charset", "escape_lt_in_attrs", - "escape_rcdata", "resolve_entities", "sanitize") + "omit_optional_tags", "minimize_boolean_attributes", + "use_trailing_solidus", "space_before_trailing_solidus", + "escape_lt_in_attrs", "escape_rcdata", "resolve_entities", + "alphabetical_attributes", "inject_meta_charset", + "strip_whitespace", "sanitize") def __init__(self, **kwargs): """Initialize HTMLSerializer. @@ -147,10 +145,12 @@ class HTMLSerializer(object): See `html5lib user documentation`_ omit_optional_tags=True|False Omit start/end tags that are optional. + alphabetical_attributes=False|True + Reorder attributes to be in alphabetical order. .. _html5lib user documentation: http://code.google.com/p/html5lib/wiki/UserDocumentation """ - if kwargs.has_key('quote_char'): + if 'quote_char' in kwargs: self.use_best_quote_char = False for attr in self.options: setattr(self, attr, kwargs.get(attr, getattr(self, attr))) @@ -158,14 +158,14 @@ class HTMLSerializer(object): self.strict = False def encode(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, unicode_encode_errors) else: return string def encodeStrict(self, string): - assert(isinstance(string, unicode)) + assert(isinstance(string, text_type)) if self.encoding: return string.encode(self.encoding, "strict") else: @@ -175,39 +175,46 @@ class HTMLSerializer(object): self.encoding = encoding in_cdata = False self.errors = [] + if encoding and self.inject_meta_charset: - from html5lib.filters.inject_meta_charset import Filter + from ..filters.inject_meta_charset import Filter treewalker = Filter(treewalker, encoding) - # XXX: WhitespaceFilter should be used before OptionalTagFilter + # WhitespaceFilter should be used before OptionalTagFilter # for maximum efficiently of this latter filter if self.strip_whitespace: - from html5lib.filters.whitespace import Filter + from ..filters.whitespace import Filter treewalker = Filter(treewalker) if self.sanitize: - from html5lib.filters.sanitizer import Filter + from ..filters.sanitizer import Filter treewalker = Filter(treewalker) if self.omit_optional_tags: - from html5lib.filters.optionaltags import Filter + from ..filters.optionaltags import Filter treewalker = Filter(treewalker) + # Alphabetical attributes must be last, as other filters + # could add attributes and alter the order + if self.alphabetical_attributes: + from ..filters.alphabeticalattributes import Filter + treewalker = Filter(treewalker) + for token in treewalker: type = token["type"] if type == "Doctype": - doctype = u"= 0: - if token["systemId"].find(u"'") >= 0: + doctype += " SYSTEM" + if token["systemId"]: + if token["systemId"].find('"') >= 0: + if token["systemId"].find("'") >= 0: self.serializeError(_("System identifer contains both single and double quote characters")) - quote_char = u"'" + quote_char = "'" else: - quote_char = u'"' - doctype += u" %s%s%s" % (quote_char, token["systemId"], quote_char) - - doctype += u">" + quote_char = '"' + doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char) + + doctype += ">" yield self.encodeStrict(doctype) elif type in ("Characters", "SpaceCharacters"): @@ -220,41 +227,41 @@ class HTMLSerializer(object): elif type in ("StartTag", "EmptyTag"): name = token["name"] - yield self.encodeStrict(u"<%s" % name) + yield self.encodeStrict("<%s" % name) if name in rcdataElements and not self.escape_rcdata: in_cdata = True elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - attributes = [] - for (attr_namespace,attr_name),attr_value in sorted(token["data"].items()): - #TODO: Add namespace support here + for (attr_namespace, attr_name), attr_value in token["data"].items(): + # TODO: Add namespace support here k = attr_name v = attr_value - yield self.encodeStrict(u' ') + yield self.encodeStrict(' ') yield self.encodeStrict(k) if not self.minimize_boolean_attributes or \ - (k not in booleanAttributes.get(name, tuple()) \ - and k not in booleanAttributes.get("", tuple())): - yield self.encodeStrict(u"=") + (k not in booleanAttributes.get(name, tuple()) + and k not in booleanAttributes.get("", tuple())): + yield self.encodeStrict("=") if self.quote_attr_values or not v: quote_attr = True else: - quote_attr = reduce(lambda x,y: x or (y in v), - spaceCharacters + u">\"'=", False) - v = v.replace(u"&", u"&") - if self.escape_lt_in_attrs: v = v.replace(u"<", u"<") + quote_attr = reduce(lambda x, y: x or (y in v), + spaceCharacters + ">\"'=", False) + v = v.replace("&", "&") + if self.escape_lt_in_attrs: + v = v.replace("<", "<") if quote_attr: quote_char = self.quote_char if self.use_best_quote_char: - if u"'" in v and u'"' not in v: - quote_char = u'"' - elif u'"' in v and u"'" not in v: - quote_char = u"'" - if quote_char == u"'": - v = v.replace(u"'", u"'") + if "'" in v and '"' not in v: + quote_char = '"' + elif '"' in v and "'" not in v: + quote_char = "'" + if quote_char == "'": + v = v.replace("'", "'") else: - v = v.replace(u'"', u""") + v = v.replace('"', """) yield self.encodeStrict(quote_char) yield self.encode(v) yield self.encodeStrict(quote_char) @@ -262,10 +269,10 @@ class HTMLSerializer(object): yield self.encode(v) if name in voidElements and self.use_trailing_solidus: if self.space_before_trailing_solidus: - yield self.encodeStrict(u" /") + yield self.encodeStrict(" /") else: - yield self.encodeStrict(u"/") - yield self.encode(u">") + yield self.encodeStrict("/") + yield self.encode(">") elif type == "EndTag": name = token["name"] @@ -273,13 +280,13 @@ class HTMLSerializer(object): in_cdata = False elif in_cdata: self.serializeError(_("Unexpected child element of a CDATA element")) - yield self.encodeStrict(u"%s>" % name) + yield self.encodeStrict("%s>" % name) elif type == "Comment": data = token["data"] if data.find("--") >= 0: self.serializeError(_("Comment contains --")) - yield self.encodeStrict(u"" % token["data"]) + yield self.encodeStrict("" % token["data"]) elif type == "Entity": name = token["name"] @@ -289,7 +296,7 @@ class HTMLSerializer(object): if self.resolve_entities and key not in xmlEntities: data = entities[key] else: - data = u"&%s;" % name + data = "&%s;" % name yield self.encodeStrict(data) else: @@ -297,9 +304,9 @@ class HTMLSerializer(object): def render(self, treewalker, encoding=None): if encoding: - return "".join(list(self.serialize(treewalker, encoding))) + return b"".join(list(self.serialize(treewalker, encoding))) else: - return u"".join(list(self.serialize(treewalker))) + return "".join(list(self.serialize(treewalker))) def serializeError(self, data="XXX ERROR MESSAGE NEEDED"): # XXX The idea is to make data mandatory. @@ -307,6 +314,7 @@ class HTMLSerializer(object): if self.strict: raise SerializeError + def SerializeError(Exception): """Error in serialized tree""" pass diff --git a/libs/html5lib/serializer/xhtmlserializer.py b/libs/html5lib/serializer/xhtmlserializer.py deleted file mode 100644 index 7fdce47b..00000000 --- a/libs/html5lib/serializer/xhtmlserializer.py +++ /dev/null @@ -1,9 +0,0 @@ -from htmlserializer import HTMLSerializer - -class XHTMLSerializer(HTMLSerializer): - quote_attr_values = True - minimize_boolean_attributes = False - use_trailing_solidus = True - escape_lt_in_attrs = True - omit_optional_tags = False - escape_rcdata = True diff --git a/libs/html5lib/tokenizer.py b/libs/html5lib/tokenizer.py index 7e9eca88..79774578 100644 --- a/libs/html5lib/tokenizer.py +++ b/libs/html5lib/tokenizer.py @@ -1,27 +1,25 @@ +from __future__ import absolute_import, division, unicode_literals + try: - frozenset + chr = unichr # flake8: noqa except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset -try: - from collections import deque -except ImportError: - from utils import deque - -from constants import spaceCharacters -from constants import entitiesWindows1252, entities -from constants import asciiLowercase, asciiLetters, asciiUpper2Lower -from constants import digits, hexDigits, EOF -from constants import tokenTypes, tagTokenTypes -from constants import replacementCharacters + pass -from inputstream import HTMLInputStream +from collections import deque + +from .constants import spaceCharacters +from .constants import entities +from .constants import asciiLetters, asciiUpper2Lower +from .constants import digits, hexDigits, EOF +from .constants import tokenTypes, tagTokenTypes +from .constants import replacementCharacters + +from .inputstream import HTMLInputStream + +from .trie import Trie + +entitiesTrie = Trie(entities) -# Group entities by their first character, for faster lookups -entitiesByFirstChar = {} -for e in entities: - entitiesByFirstChar.setdefault(e[0], []).append(e) class HTMLTokenizer(object): """ This class takes care of tokenizing HTML. @@ -42,10 +40,10 @@ class HTMLTokenizer(object): self.stream = HTMLInputStream(stream, encoding, parseMeta, useChardet) self.parser = parser - #Perform case conversions? + # Perform case conversions? self.lowercaseElementName = lowercaseElementName self.lowercaseAttrName = lowercaseAttrName - + # Setup the initial tokenizer state self.escapeFlag = False self.lastFourChars = [] @@ -100,78 +98,79 @@ class HTMLTokenizer(object): if charAsInt in replacementCharacters: char = replacementCharacters[charAsInt] self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) - elif ((0xD800 <= charAsInt <= 0xDFFF) or + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) + elif ((0xD800 <= charAsInt <= 0xDFFF) or (charAsInt > 0x10FFFF)): - char = u"\uFFFD" + char = "\uFFFD" self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", - "datavars": {"charAsInt": charAsInt}}) + "illegal-codepoint-for-numeric-entity", + "datavars": {"charAsInt": charAsInt}}) else: - #Should speed up this check somehow (e.g. move the set to a constant) - if ((0x0001 <= charAsInt <= 0x0008) or - (0x000E <= charAsInt <= 0x001F) or - (0x007F <= charAsInt <= 0x009F) or - (0xFDD0 <= charAsInt <= 0xFDEF) or - charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, + # Should speed up this check somehow (e.g. move the set to a constant) + if ((0x0001 <= charAsInt <= 0x0008) or + (0x000E <= charAsInt <= 0x001F) or + (0x007F <= charAsInt <= 0x009F) or + (0xFDD0 <= charAsInt <= 0xFDEF) or + charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, - 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, + 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, - 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, - 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, - 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, + 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, + 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, + 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF])): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "illegal-codepoint-for-numeric-entity", + "illegal-codepoint-for-numeric-entity", "datavars": {"charAsInt": charAsInt}}) try: # Try/except needed as UCS-2 Python builds' unichar only works # within the BMP. - char = unichr(charAsInt) + char = chr(charAsInt) except ValueError: - char = eval("u'\\U%08x'" % charAsInt) + v = charAsInt - 0x10000 + char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF)) # Discard the ; if present. Otherwise, put it back on the queue and # invoke parseError on parser. - if c != u";": + if c != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "numeric-entity-without-semicolon"}) + "numeric-entity-without-semicolon"}) self.stream.unget(c) return char def consumeEntity(self, allowedChar=None, fromAttribute=False): # Initialise to the default output for when no entity is matched - output = u"&" + output = "&" charStack = [self.stream.char()] - if (charStack[0] in spaceCharacters or charStack[0] in (EOF, u"<", u"&") - or (allowedChar is not None and allowedChar == charStack[0])): + if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") + or (allowedChar is not None and allowedChar == charStack[0])): self.stream.unget(charStack[0]) - elif charStack[0] == u"#": + elif charStack[0] == "#": # Read the next character to see if it's hex or decimal hex = False charStack.append(self.stream.char()) - if charStack[-1] in (u"x", u"X"): + if charStack[-1] in ("x", "X"): hex = True charStack.append(self.stream.char()) # charStack[-1] should be the first digit if (hex and charStack[-1] in hexDigits) \ - or (not hex and charStack[-1] in digits): + or (not hex and charStack[-1] in digits): # At least one digit found, so consume the whole number self.stream.unget(charStack[-1]) output = self.consumeNumberEntity(hex) else: # No digits found self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data": "expected-numeric-entity"}) + "data": "expected-numeric-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: # At this point in the process might have named entity. Entities @@ -179,46 +178,40 @@ class HTMLTokenizer(object): # # Consume characters and compare to these to a substring of the # entity names in the list until the substring no longer matches. - filteredEntityList = entitiesByFirstChar.get(charStack[0], []) - - def entitiesStartingWith(name): - return [e for e in filteredEntityList if e.startswith(name)] - - while (charStack[-1] is not EOF and - entitiesStartingWith("".join(charStack))): + while (charStack[-1] is not EOF): + if not entitiesTrie.has_keys_with_prefix("".join(charStack)): + break charStack.append(self.stream.char()) # At this point we have a string that starts with some characters # that may match an entity - entityName = None - # Try to find the longest entity the string will match to take care # of ¬i for instance. - for entityLength in xrange(len(charStack)-1, 1, -1): - possibleEntityName = "".join(charStack[:entityLength]) - if possibleEntityName in entities: - entityName = possibleEntityName - break + try: + entityName = entitiesTrie.longest_prefix("".join(charStack[:-1])) + entityLength = len(entityName) + except KeyError: + entityName = None if entityName is not None: if entityName[-1] != ";": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "named-entity-without-semicolon"}) + "named-entity-without-semicolon"}) if (entityName[-1] != ";" and fromAttribute and (charStack[entityLength] in asciiLetters or charStack[entityLength] in digits or - charStack[entityLength] == "=")): + charStack[entityLength] == "=")): self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) else: output = entities[entityName] self.stream.unget(charStack.pop()) - output += u"".join(charStack[entityLength:]) + output += "".join(charStack[entityLength:]) else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-named-entity"}) + "expected-named-entity"}) self.stream.unget(charStack.pop()) - output = u"&" + u"".join(charStack) + output = "&" + "".join(charStack) if fromAttribute: self.currentToken["data"][-1][1] += output @@ -246,28 +239,26 @@ class HTMLTokenizer(object): token["name"] = token["name"].translate(asciiUpper2Lower) if token["type"] == tokenTypes["EndTag"]: if token["data"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"attributes-in-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "attributes-in-end-tag"}) if token["selfClosing"]: - self.tokenQueue.append({"type":tokenTypes["ParseError"], - "data":"self-closing-flag-on-end-tag"}) + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "self-closing-flag-on-end-tag"}) self.tokenQueue.append(token) self.state = self.dataState - # Below are the various tokenizer states worked out. - def dataState(self): data = self.stream.char() if data == "&": self.state = self.entityDataState elif data == "<": self.state = self.tagOpenState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], - "data":"invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\u0000"}) + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], + "data": "invalid-codepoint"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\u0000"}) elif data is EOF: # Tokenization ends. return False @@ -276,21 +267,21 @@ class HTMLTokenizer(object): # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def entityDataState(self): self.consumeEntity() self.state = self.dataState return True - + def rcdataState(self): data = self.stream.char() if data == "&": @@ -300,113 +291,113 @@ class HTMLTokenizer(object): elif data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data in spaceCharacters: # Directly after emitting a token you switch back to the "data # state". At that point spaceCharacters are important so they are # emitted separately. self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data": - data + self.stream.charsUntil(spaceCharacters, True)}) + data + self.stream.charsUntil(spaceCharacters, True)}) # No need to update lastFourChars here, since the first space will # have already been appended to lastFourChars and will have broken # any sequences else: - chars = self.stream.charsUntil((u"&", u"<")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("&", "<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True def characterReferenceInRcdata(self): self.consumeEntity() self.state = self.rcdataState return True - + def rawtextState(self): data = self.stream.char() if data == "<": self.state = self.rawtextLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataState(self): data = self.stream.char() if data == "<": self.state = self.scriptDataLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: # Tokenization ends. return False else: - chars = self.stream.charsUntil((u"<", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def plaintextState(self): data = self.stream.char() if data == EOF: # Tokenization ends. return False - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + self.stream.charsUntil(u"\u0000")}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + self.stream.charsUntil("\u0000")}) return True def tagOpenState(self): data = self.stream.char() - if data == u"!": + if data == "!": self.state = self.markupDeclarationOpenState - elif data == u"/": + elif data == "/": self.state = self.closeTagOpenState elif data in asciiLetters: - self.currentToken = {"type": tokenTypes["StartTag"], + self.currentToken = {"type": tokenTypes["StartTag"], "name": data, "data": [], "selfClosing": False, "selfClosingAcknowledged": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-right-bracket"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<>"}) + "expected-tag-name-but-got-right-bracket"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"}) self.state = self.dataState - elif data == u"?": + elif data == "?": # XXX In theory it could be something besides a tag name. But # do we really care? self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name-but-got-question-mark"}) + "expected-tag-name-but-got-question-mark"}) self.stream.unget(data) self.state = self.bogusCommentState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-tag-name"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + "expected-tag-name"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.dataState return True @@ -415,22 +406,22 @@ class HTMLTokenizer(object): data = self.stream.char() if data in asciiLetters: self.currentToken = {"type": tokenTypes["EndTag"], "name": data, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.tagNameState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-right-bracket"}) + "expected-closing-tag-but-got-right-bracket"}) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-eof"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + "expected-closing-tag-but-got-eof"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.state = self.dataState else: # XXX data can be _'_... self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-closing-tag-but-got-char", - "datavars": {"data": data}}) + "expected-closing-tag-but-got-char", + "datavars": {"data": data}}) self.stream.unget(data) self.state = self.bogusCommentState return True @@ -439,229 +430,229 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-tag-name"}) + "eof-in-tag-name"}) self.state = self.dataState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" else: self.currentToken["name"] += data # (Don't use charsUntil here, because tag names are # very short and it's faster to not do anything fancy) return True - + def rcdataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rcdataEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rcdataEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rcdataState return True - + def rcdataEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rcdataState return True - + def rawtextLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.rawtextEndTagOpenState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer += data self.state = self.rawtextEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.rawtextState return True - + def rawtextEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.rawtextState return True - + def scriptDataLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEndTagOpenState elif data == "!": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapeStartDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapeStartDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState else: self.stream.unget(data) self.state = self.scriptDataState return True - + def scriptDataEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.state = self.dataState else: - chars = self.stream.charsUntil((u"<", u"-", u"\u0000")) - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": - data + chars}) + chars = self.stream.charsUntil(("<", "-", "\u0000")) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": + data + chars}) return True - + def scriptDataEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataEscapedDashDashState elif data == "<": self.state = self.scriptDataEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -669,21 +660,21 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": self.state = self.scriptDataEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataEscapedState elif data == EOF: self.state = self.dataState @@ -691,61 +682,61 @@ class HTMLTokenizer(object): self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedLessThanSignState(self): data = self.stream.char() if data == "/": self.temporaryBuffer = "" self.state = self.scriptDataEscapedEndTagOpenState elif data in asciiLetters: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<" + data}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data}) self.temporaryBuffer = data self.state = self.scriptDataDoubleEscapeStartState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagOpenState(self): data = self.stream.char() if data in asciiLetters: self.temporaryBuffer = data self.state = self.scriptDataEscapedEndTagNameState else: - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ""}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataEscapedEndTagNameState(self): appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower() data = self.stream.char() if data in spaceCharacters and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.beforeAttributeNameState elif data == "/" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.state = self.selfClosingStartTagState elif data == ">" and appropriate: self.currentToken = {"type": tokenTypes["EndTag"], "name": self.temporaryBuffer, - "data": [], "selfClosing":False} + "data": [], "selfClosing": False} self.emitCurrentToken() self.state = self.dataState elif data in asciiLetters: self.temporaryBuffer += data else: self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"" + self.temporaryBuffer}) + "data": "" + self.temporaryBuffer}) self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapeStartState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -761,87 +752,87 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.scriptDataEscapedState return True - + def scriptDataDoubleEscapedState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) return True - + def scriptDataDoubleEscapedDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) self.state = self.scriptDataDoubleEscapedDashDashState elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - - def scriptDataDoubleEscapedDashState(self): + + def scriptDataDoubleEscapedDashDashState(self): data = self.stream.char() if data == "-": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"}) elif data == "<": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"}) self.state = self.scriptDataDoubleEscapedLessThanSignState elif data == ">": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"}) self.state = self.scriptDataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.tokenQueue.append({"type": tokenTypes["Characters"], - "data": u"\uFFFD"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], + "data": "\uFFFD"}) self.state = self.scriptDataDoubleEscapedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-script-in-script"}) + "eof-in-script-in-script"}) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapedLessThanSignState(self): data = self.stream.char() if data == "/": - self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"/"}) + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"}) self.temporaryBuffer = "" self.state = self.scriptDataDoubleEscapeEndState else: self.stream.unget(data) self.state = self.scriptDataDoubleEscapedState return True - + def scriptDataDoubleEscapeEndState(self): data = self.stream.char() if data in (spaceCharacters | frozenset(("/", ">"))): @@ -865,23 +856,23 @@ class HTMLTokenizer(object): elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data in (u"'", u'"', u"=", u"<"): + elif data in ("'", '"', "=", "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-name-but-got-eof"}) + "expected-attribute-name-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -892,34 +883,34 @@ class HTMLTokenizer(object): data = self.stream.char() leavingThisState = True emitToken = False - if data == u"=": + if data == "=": self.state = self.beforeAttributeValueState elif data in asciiLetters: self.currentToken["data"][-1][0] += data +\ - self.stream.charsUntil(asciiLetters, True) + self.stream.charsUntil(asciiLetters, True) leavingThisState = False - elif data == u">": + elif data == ">": # XXX If we emit here the attributes are converted to a dict # without being checked and when the code below runs we error # because data is a dict not a list emitToken = True elif data in spaceCharacters: self.state = self.afterAttributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][0] += u"\uFFFD" + self.currentToken["data"][-1][0] += "\uFFFD" leavingThisState = False - elif data in (u"'", u'"', u"<"): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data in ("'", '"', "<"): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-in-attribute-name"}) + "invalid-character-in-attribute-name"}) self.currentToken["data"][-1][0] += data leavingThisState = False elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-attribute-name"}) self.state = self.dataState else: @@ -936,7 +927,7 @@ class HTMLTokenizer(object): for name, value in self.currentToken["data"][:-1]: if self.currentToken["data"][-1][0] == name: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "duplicate-attribute"}) + "duplicate-attribute"}) break # XXX Fix for above XXX if emitToken: @@ -947,28 +938,28 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"=": + elif data == "=": self.state = self.beforeAttributeValueState - elif data == u">": + elif data == ">": self.emitCurrentToken() elif data in asciiLetters: self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"].append([u"\uFFFD", ""]) + self.currentToken["data"].append(["\uFFFD", ""]) self.state = self.attributeNameState - elif data in (u"'", u'"', u"<"): + elif data in ("'", '"', "<"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "invalid-character-after-attribute-name"}) + "invalid-character-after-attribute-name"}) self.currentToken["data"].append([data, ""]) self.state = self.attributeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-end-of-tag-but-got-eof"}) + "expected-end-of-tag-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"].append([data, ""]) @@ -979,30 +970,30 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: self.stream.charsUntil(spaceCharacters, True) - elif data == u"\"": + elif data == "\"": self.state = self.attributeValueDoubleQuotedState - elif data == u"&": + elif data == "&": self.state = self.attributeValueUnQuotedState - self.stream.unget(data); - elif data == u"'": + self.stream.unget(data) + elif data == "'": self.state = self.attributeValueSingleQuotedState - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-right-bracket"}) + "expected-attribute-value-but-got-right-bracket"}) self.emitCurrentToken() - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" self.state = self.attributeValueUnQuotedState - elif data in (u"=", u"<", u"`"): + elif data in ("=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "equals-in-unquoted-attribute-value"}) + "equals-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data self.state = self.attributeValueUnQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-attribute-value-but-got-eof"}) + "expected-attribute-value-but-got-eof"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data @@ -1013,81 +1004,81 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u'"') - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute('"') + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-double-quote"}) + "eof-in-attribute-value-double-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("\"", u"&")) + self.stream.charsUntil(("\"", "&", "\u0000")) return True def attributeValueSingleQuotedState(self): data = self.stream.char() if data == "'": self.state = self.afterAttributeValueState - elif data == u"&": - self.processEntityInAttribute(u"'") - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "&": + self.processEntityInAttribute("'") + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-single-quote"}) + "eof-in-attribute-value-single-quote"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data +\ - self.stream.charsUntil(("'", u"&")) + self.stream.charsUntil(("'", "&", "\u0000")) return True def attributeValueUnQuotedState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u"&": + elif data == "&": self.processEntityInAttribute(">") - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data in (u'"', u"'", u"=", u"<", u"`"): + elif data in ('"', "'", "=", "<", "`"): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-in-unquoted-attribute-value"}) + "unexpected-character-in-unquoted-attribute-value"}) self.currentToken["data"][-1][1] += data - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"][-1][1] += u"\uFFFD" + self.currentToken["data"][-1][1] += "\uFFFD" elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-attribute-value-no-quotes"}) + "eof-in-attribute-value-no-quotes"}) self.state = self.dataState else: self.currentToken["data"][-1][1] += data + self.stream.charsUntil( - frozenset((u"&", u">", u'"', u"'", u"=", u"<", u"`")) | spaceCharacters) + frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters) return True def afterAttributeValueState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeAttributeNameState - elif data == u">": + elif data == ">": self.emitCurrentToken() - elif data == u"/": + elif data == "/": self.state = self.selfClosingStartTagState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-attribute-value"}) + "unexpected-EOF-after-attribute-value"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-attribute-value"}) + "unexpected-character-after-attribute-value"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1098,14 +1089,14 @@ class HTMLTokenizer(object): self.currentToken["selfClosing"] = True self.emitCurrentToken() elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-EOF-after-solidus-in-tag"}) + "unexpected-EOF-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-character-after-soldius-in-tag"}) + "unexpected-character-after-solidus-in-tag"}) self.stream.unget(data) self.state = self.beforeAttributeNameState return True @@ -1114,10 +1105,10 @@ class HTMLTokenizer(object): # Make a new comment token and give it as value all the characters # until the first > or EOF (charsUntil checks for EOF automatically) # and emit it. - data = self.stream.charsUntil(u">") - data = data.replace(u"\u0000", u"\uFFFD") + data = self.stream.charsUntil(">") + data = data.replace("\u0000", "\uFFFD") self.tokenQueue.append( - {"type": tokenTypes["Comment"], "data": data}) + {"type": tokenTypes["Comment"], "data": data}) # Eat the character directly after the bogus comment which is either a # ">" or an EOF. @@ -1127,28 +1118,28 @@ class HTMLTokenizer(object): def markupDeclarationOpenState(self): charStack = [self.stream.char()] - if charStack[-1] == u"-": + if charStack[-1] == "-": charStack.append(self.stream.char()) - if charStack[-1] == u"-": - self.currentToken = {"type": tokenTypes["Comment"], "data": u""} + if charStack[-1] == "-": + self.currentToken = {"type": tokenTypes["Comment"], "data": ""} self.state = self.commentStartState return True - elif charStack[-1] in (u'd', u'D'): + elif charStack[-1] in ('d', 'D'): matched = True - for expected in ((u'o', u'O'), (u'c', u'C'), (u't', u'T'), - (u'y', u'Y'), (u'p', u'P'), (u'e', u'E')): + for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'), + ('y', 'Y'), ('p', 'P'), ('e', 'E')): charStack.append(self.stream.char()) if charStack[-1] not in expected: matched = False break if matched: self.currentToken = {"type": tokenTypes["Doctype"], - "name": u"", - "publicId": None, "systemId": None, + "name": "", + "publicId": None, "systemId": None, "correct": True} self.state = self.doctypeState return True - elif (charStack[-1] == "[" and + elif (charStack[-1] == "[" and self.parser is not None and self.parser.tree.openElements and self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace): @@ -1163,7 +1154,7 @@ class HTMLTokenizer(object): return True self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-dashes-or-doctype"}) + "expected-dashes-or-doctype"}) while charStack: self.stream.unget(charStack.pop()) @@ -1174,41 +1165,41 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "-": self.state = self.commentStartDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data self.state = self.commentState return True - + def commentStartDashState(self): data = self.stream.char() if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "incorrect-comment"}) + "incorrect-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment"}) + "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: @@ -1216,95 +1207,94 @@ class HTMLTokenizer(object): self.state = self.commentState return True - def commentState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"\uFFFD" + self.currentToken["data"] += "\uFFFD" elif data is EOF: - self.tokenQueue.append({"type": tokenTypes["ParseError"], + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "eof-in-comment"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.currentToken["data"] += data + \ - self.stream.charsUntil((u"-", u"\u0000")) + self.stream.charsUntil(("-", "\u0000")) return True def commentEndDashState(self): data = self.stream.char() - if data == u"-": + if data == "-": self.state = self.commentEndState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"-\uFFFD" + self.currentToken["data"] += "-\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-dash"}) + "eof-in-comment-end-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"-" + data + self.currentToken["data"] += "-" + data self.state = self.commentState return True def commentEndState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--\uFFFD" + self.currentToken["data"] += "--\uFFFD" self.state = self.commentState elif data == "!": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-bang-after-double-dash-in-comment"}) + "unexpected-bang-after-double-dash-in-comment"}) self.state = self.commentEndBangState - elif data == u"-": + elif data == "-": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-dash-after-double-dash-in-comment"}) + "unexpected-dash-after-double-dash-in-comment"}) self.currentToken["data"] += data elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-double-dash"}) + "eof-in-comment-double-dash"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: # XXX self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-comment"}) - self.currentToken["data"] += u"--" + data + "unexpected-char-in-comment"}) + self.currentToken["data"] += "--" + data self.state = self.commentState return True def commentEndBangState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"-": + elif data == "-": self.currentToken["data"] += "--!" self.state = self.commentEndDashState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["data"] += u"--!\uFFFD" + self.currentToken["data"] += "--!\uFFFD" self.state = self.commentState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-comment-end-bang-state"}) + "eof-in-comment-end-bang-state"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - self.currentToken["data"] += u"--!" + data + self.currentToken["data"] += "--!" + data self.state = self.commentState return True @@ -1314,13 +1304,13 @@ class HTMLTokenizer(object): self.state = self.beforeDoctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "need-space-after-doctype"}) + "need-space-after-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeNameState return True @@ -1329,20 +1319,20 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-right-bracket"}) + "expected-doctype-name-but-got-right-bracket"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] = u"\uFFFD" + self.currentToken["name"] = "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-doctype-name-but-got-eof"}) + "expected-doctype-name-but-got-eof"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1356,18 +1346,18 @@ class HTMLTokenizer(object): if data in spaceCharacters: self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.state = self.afterDoctypeNameState - elif data == u">": + elif data == ">": self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) self.state = self.dataState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["name"] += u"\uFFFD" + self.currentToken["name"] += "\uFFFD" self.state = self.doctypeNameState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype-name"}) + "eof-in-doctype-name"}) self.currentToken["correct"] = False self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower) self.tokenQueue.append(self.currentToken) @@ -1380,21 +1370,21 @@ class HTMLTokenizer(object): data = self.stream.char() if data in spaceCharacters: pass - elif data == u">": + elif data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.currentToken["correct"] = False self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.tokenQueue.append(self.currentToken) self.state = self.dataState else: - if data in (u"p", u"P"): + if data in ("p", "P"): matched = True - for expected in ((u"u", u"U"), (u"b", u"B"), (u"l", u"L"), - (u"i", u"I"), (u"c", u"C")): + for expected in (("u", "U"), ("b", "B"), ("l", "L"), + ("i", "I"), ("c", "C")): data = self.stream.char() if data not in expected: matched = False @@ -1402,10 +1392,10 @@ class HTMLTokenizer(object): if matched: self.state = self.afterDoctypePublicKeywordState return True - elif data in (u"s", u"S"): + elif data in ("s", "S"): matched = True - for expected in ((u"y", u"Y"), (u"s", u"S"), (u"t", u"T"), - (u"e", u"E"), (u"m", u"M")): + for expected in (("y", "Y"), ("s", "S"), ("t", "T"), + ("e", "E"), ("m", "M")): data = self.stream.char() if data not in expected: matched = False @@ -1420,25 +1410,25 @@ class HTMLTokenizer(object): # and needs to be ungetted self.stream.unget(data) self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "expected-space-or-right-bracket-in-doctype", "datavars": - {"data": data}}) + "expected-space-or-right-bracket-in-doctype", "datavars": + {"data": data}}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypePublicKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypePublicIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypePublicIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1452,26 +1442,26 @@ class HTMLTokenizer(object): if data in spaceCharacters: pass elif data == "\"": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierDoubleQuotedState elif data == "'": - self.currentToken["publicId"] = u"" + self.currentToken["publicId"] = "" self.state = self.doctypePublicIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1480,19 +1470,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1504,19 +1494,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypePublicIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["publicId"] += u"\uFFFD" + self.currentToken["publicId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1533,27 +1523,27 @@ class HTMLTokenizer(object): self.state = self.dataState elif data == '"': self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) - self.currentToken["systemId"] = u"" + "unexpected-char-in-doctype"}) + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def betweenDoctypePublicAndSystemIdentifiersState(self): data = self.stream.char() if data in spaceCharacters: @@ -1562,36 +1552,36 @@ class HTMLTokenizer(object): self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data == '"': - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True - + def afterDoctypeSystemKeywordState(self): data = self.stream.char() if data in spaceCharacters: self.state = self.beforeDoctypeSystemIdentifierState elif data in ("'", '"'): self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1599,32 +1589,32 @@ class HTMLTokenizer(object): self.stream.unget(data) self.state = self.beforeDoctypeSystemIdentifierState return True - + def beforeDoctypeSystemIdentifierState(self): data = self.stream.char() if data in spaceCharacters: pass elif data == "\"": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierDoubleQuotedState elif data == "'": - self.currentToken["systemId"] = u"" + self.currentToken["systemId"] = "" self.state = self.doctypeSystemIdentifierSingleQuotedState elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.currentToken["correct"] = False self.state = self.bogusDoctypeState return True @@ -1633,19 +1623,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "\"": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1657,19 +1647,19 @@ class HTMLTokenizer(object): data = self.stream.char() if data == "'": self.state = self.afterDoctypeSystemIdentifierState - elif data == u"\u0000": - self.tokenQueue.append({"type": tokenTypes["ParseError"], + elif data == "\u0000": + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - self.currentToken["systemId"] += u"\uFFFD" + self.currentToken["systemId"] += "\uFFFD" elif data == ">": self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-end-of-doctype"}) + "unexpected-end-of-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState @@ -1686,19 +1676,19 @@ class HTMLTokenizer(object): self.state = self.dataState elif data is EOF: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "eof-in-doctype"}) + "eof-in-doctype"}) self.currentToken["correct"] = False self.tokenQueue.append(self.currentToken) self.state = self.dataState else: self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": - "unexpected-char-in-doctype"}) + "unexpected-char-in-doctype"}) self.state = self.bogusDoctypeState return True def bogusDoctypeState(self): data = self.stream.char() - if data == u">": + if data == ">": self.tokenQueue.append(self.currentToken) self.state = self.dataState elif data is EOF: @@ -1713,32 +1703,29 @@ class HTMLTokenizer(object): def cdataSectionState(self): data = [] while True: - data.append(self.stream.charsUntil(u"]")) - charStack = [] - - for expected in ["]", "]", ">"]: - charStack.append(self.stream.char()) - matched = True - if charStack[-1] == EOF: - data.extend(charStack[:-1]) - break - elif charStack[-1] != expected: - matched = False - data.extend(charStack) - break - - if matched: + data.append(self.stream.charsUntil("]")) + data.append(self.stream.charsUntil(">")) + char = self.stream.char() + if char == EOF: break + else: + assert char == ">" + if data[-1][-2:] == "]]": + data[-1] = data[-1][:-2] + break + else: + data.append(char) + data = "".join(data) - #Deal with null here rather than in the parser - nullCount = data.count(u"\u0000") + # Deal with null here rather than in the parser + nullCount = data.count("\u0000") if nullCount > 0: - for i in xrange(nullCount): - self.tokenQueue.append({"type": tokenTypes["ParseError"], + for i in range(nullCount): + self.tokenQueue.append({"type": tokenTypes["ParseError"], "data": "invalid-codepoint"}) - data = data.replace(u"\u0000", u"\uFFFD") + data = data.replace("\u0000", "\uFFFD") if data: - self.tokenQueue.append({"type": tokenTypes["Characters"], + self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data}) self.state = self.dataState return True diff --git a/libs/html5lib/treeadapters/__init__.py b/libs/html5lib/treeadapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/html5lib/treeadapters/sax.py b/libs/html5lib/treeadapters/sax.py new file mode 100644 index 00000000..ad47df95 --- /dev/null +++ b/libs/html5lib/treeadapters/sax.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from xml.sax.xmlreader import AttributesNSImpl + +from ..constants import adjustForeignAttributes, unadjustForeignAttributes + +prefix_mapping = {} +for prefix, localName, namespace in adjustForeignAttributes.values(): + if prefix is not None: + prefix_mapping[prefix] = namespace + + +def to_sax(walker, handler): + """Call SAX-like content handler based on treewalker walker""" + handler.startDocument() + for prefix, namespace in prefix_mapping.items(): + handler.startPrefixMapping(prefix, namespace) + + for token in walker: + type = token["type"] + if type == "Doctype": + continue + elif type in ("StartTag", "EmptyTag"): + attrs = AttributesNSImpl(token["data"], + unadjustForeignAttributes) + handler.startElementNS((token["namespace"], token["name"]), + token["name"], + attrs) + if type == "EmptyTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type == "EndTag": + handler.endElementNS((token["namespace"], token["name"]), + token["name"]) + elif type in ("Characters", "SpaceCharacters"): + handler.characters(token["data"]) + elif type == "Comment": + pass + else: + assert False, "Unknown token type" + + for prefix, namespace in prefix_mapping.items(): + handler.endPrefixMapping(prefix) + handler.endDocument() diff --git a/libs/html5lib/treebuilders/__init__.py b/libs/html5lib/treebuilders/__init__.py index 14f66d40..6a6b2a4c 100755 --- a/libs/html5lib/treebuilders/__init__.py +++ b/libs/html5lib/treebuilders/__init__.py @@ -7,7 +7,7 @@ implement several things: 1) A set of classes for various types of elements: Document, Doctype, Comment, Element. These must implement the interface of _base.treebuilders.Node (although comment nodes have a different -signature for their constructor, see treebuilders.simpletree.Comment) +signature for their constructor, see treebuilders.etree.Comment) Textual content may also be implemented as another node type, or not, as your tree implementation requires. @@ -24,73 +24,53 @@ getDocument - Returns the root node of the complete document tree testSerializer method on your treebuilder which accepts a node and returns a string containing Node and its children serialized according to the format used in the unittests - -The supplied simpletree module provides a python-only implementation -of a full treebuilder and is a useful reference for the semantics of -the various methods. """ +from __future__ import absolute_import, division, unicode_literals + +from ..utils import default_etree + treeBuilderCache = {} -import sys def getTreeBuilder(treeType, implementation=None, **kwargs): """Get a TreeBuilder class for various types of tree with built-in support - + treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" - - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. - "dom" - A generic builder for DOM implementations, defaulting to - a xml.dom.minidom based implementation for the sake of - backwards compatibility (as releases up until 0.10 had a - builder called "dom" that was a minidom implemenation). - "etree" - A generic builder for tree implementations exposing an - elementtree-like interface (known to work with - ElementTree, cElementTree and lxml.etree). - "beautifulsoup" - Beautiful soup (if installed) - + values are: + + "dom" - A generic builder for DOM implementations, defaulting to + a xml.dom.minidom based implementation. + "etree" - A generic builder for tree implementations exposing an + ElementTree-like interface, defaulting to + xml.etree.cElementTree if available and + xml.etree.ElementTree if not. + "lxml" - A etree-based builder for lxml.etree, handling + limitations of lxml's implementation. + implementation - (Currently applies to the "etree" and "dom" tree types). A module implementing the tree type e.g. - xml.etree.ElementTree or lxml.etree.""" - + xml.etree.ElementTree or xml.etree.cElementTree.""" + treeType = treeType.lower() if treeType not in treeBuilderCache: if treeType == "dom": - import dom - # XXX: Keep backwards compatibility by using minidom if no implementation is given - if implementation == None: + from . import dom + # Come up with a sane default (pref. from the stdlib) + if implementation is None: from xml.dom import minidom implementation = minidom - # XXX: NEVER cache here, caching is done in the dom submodule + # NEVER cache here, caching is done in the dom submodule return dom.getDomModule(implementation, **kwargs).TreeBuilder - elif treeType == "simpletree": - import simpletree - treeBuilderCache[treeType] = simpletree.TreeBuilder - elif treeType == "beautifulsoup": - import soup - treeBuilderCache[treeType] = soup.TreeBuilder elif treeType == "lxml": - import etree_lxml + from . import etree_lxml treeBuilderCache[treeType] = etree_lxml.TreeBuilder elif treeType == "etree": - # Come up with a sane default - if implementation == None: - try: - import xml.etree.cElementTree as ET - except ImportError: - try: - import xml.etree.ElementTree as ET - except ImportError: - try: - import cElementTree as ET - except ImportError: - import elementtree.ElementTree as ET - implementation = ET - import etree + from . import etree + if implementation is None: + implementation = default_etree # NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeBuilder else: - raise ValueError("""Unrecognised treebuilder "%s" """%treeType) + raise ValueError("""Unrecognised treebuilder "%s" """ % treeType) return treeBuilderCache.get(treeType) diff --git a/libs/html5lib/treebuilders/_base.py b/libs/html5lib/treebuilders/_base.py index f3782d28..8b97cc11 100755 --- a/libs/html5lib/treebuilders/_base.py +++ b/libs/html5lib/treebuilders/_base.py @@ -1,25 +1,34 @@ -from html5lib.constants import scopingElements, tableInsertModeElements, namespaces -try: - frozenset -except NameError: - # Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from ..constants import scopingElements, tableInsertModeElements, namespaces # The scope markers are inserted when entering object elements, # marquees, table cells, and table captions, and are used to prevent formatting # from "leaking" into tables, object elements, and marquees. Marker = None +listElementsMap = { + None: (frozenset(scopingElements), False), + "button": (frozenset(scopingElements | set([(namespaces["html"], "button")])), False), + "list": (frozenset(scopingElements | set([(namespaces["html"], "ol"), + (namespaces["html"], "ul")])), False), + "table": (frozenset([(namespaces["html"], "html"), + (namespaces["html"], "table")]), False), + "select": (frozenset([(namespaces["html"], "optgroup"), + (namespaces["html"], "option")]), True) +} + + class Node(object): def __init__(self, name): """Node representing an item in the tree. name - The tag name associated with the node parent - The parent of the current node (or None for the document node) - value - The value of the current node (applies to text nodes and + value - The value of the current node (applies to text nodes and comments attributes - a dict holding name, value pairs for attributes of the node - childNodes - a list of child nodes of the current node. This must + childNodes - a list of child nodes of the current node. This must include all elements but not necessarily other node types _flags - A list of miscellaneous flags that can be set on the node """ @@ -30,14 +39,14 @@ class Node(object): self.childNodes = [] self._flags = [] - def __unicode__(self): - attributesStr = " ".join(["%s=\"%s\""%(name, value) - for name, value in - self.attributes.iteritems()]) + def __str__(self): + attributesStr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in + self.attributes.items()]) if attributesStr: - return "<%s %s>"%(self.name,attributesStr) + return "<%s %s>" % (self.name, attributesStr) else: - return "<%s>"%(self.name) + return "<%s>" % (self.name) def __repr__(self): return "<%s>" % (self.name) @@ -48,14 +57,14 @@ class Node(object): raise NotImplementedError def insertText(self, data, insertBefore=None): - """Insert data as text in the current node, positioned before the + """Insert data as text in the current node, positioned before the start of node insertBefore or to the end of the node's text. """ raise NotImplementedError def insertBefore(self, node, refNode): - """Insert node as a child of the current node, before refNode in the - list of child nodes. Raises ValueError if refNode is not a child of + """Insert node as a child of the current node, before refNode in the + list of child nodes. Raises ValueError if refNode is not a child of the current node""" raise NotImplementedError @@ -65,11 +74,11 @@ class Node(object): raise NotImplementedError def reparentChildren(self, newParent): - """Move all the children of the current node to newParent. - This is needed so that trees that don't store text as nodes move the + """Move all the children of the current node to newParent. + This is needed so that trees that don't store text as nodes move the text in the correct way """ - #XXX - should this method be made more general? + # XXX - should this method be made more general? for child in self.childNodes: newParent.appendChild(child) self.childNodes = [] @@ -80,12 +89,12 @@ class Node(object): """ raise NotImplementedError - def hasContent(self): """Return true if the node has children or text, false otherwise """ raise NotImplementedError + class ActiveFormattingElements(list): def append(self, node): equalCount = 0 @@ -103,12 +112,13 @@ class ActiveFormattingElements(list): def nodesEqual(self, node1, node2): if not node1.nameTuple == node2.nameTuple: return False - + if not node1.attributes == node2.attributes: return False - + return True + class TreeBuilder(object): """Base treebuilder implementation documentClass - the class to use for the bottommost node of a document @@ -117,19 +127,19 @@ class TreeBuilder(object): doctypeClass - the class to use for doctypes """ - #Document class + # Document class documentClass = None - #The class to use for creating a node + # The class to use for creating a node elementClass = None - #The class to use for creating comments + # The class to use for creating comments commentClass = None - #The class to use for creating doctypes + # The class to use for creating doctypes doctypeClass = None - - #Fragment class + + # Fragment class fragmentClass = None def __init__(self, namespaceHTMLElements): @@ -138,12 +148,12 @@ class TreeBuilder(object): else: self.defaultNamespace = None self.reset() - + def reset(self): self.openElements = [] self.activeFormattingElements = ActiveFormattingElements() - #XXX - rename these to headElement, formElement + # XXX - rename these to headElement, formElement self.headPointer = None self.formPointer = None @@ -153,30 +163,20 @@ class TreeBuilder(object): def elementInScope(self, target, variant=None): - #If we pass a node in we match that. if we pass a string - #match any node with that name + # If we pass a node in we match that. if we pass a string + # match any node with that name exactNode = hasattr(target, "nameTuple") - listElementsMap = { - None:(scopingElements, False), - "button":(scopingElements | set([(namespaces["html"], "button")]), False), - "list":(scopingElements | set([(namespaces["html"], "ol"), - (namespaces["html"], "ul")]), False), - "table":(set([(namespaces["html"], "html"), - (namespaces["html"], "table")]), False), - "select":(set([(namespaces["html"], "optgroup"), - (namespaces["html"], "option")]), True) - } listElements, invert = listElementsMap[variant] for node in reversed(self.openElements): if (node.name == target and not exactNode or - node == target and exactNode): + node == target and exactNode): return True - elif (invert ^ (node.nameTuple in listElements)): + elif (invert ^ (node.nameTuple in listElements)): return False - assert False # We should never reach this point + assert False # We should never reach this point def reconstructActiveFormattingElements(self): # Within this algorithm the order of steps described in the @@ -196,7 +196,7 @@ class TreeBuilder(object): # Step 6 while entry != Marker and entry not in self.openElements: if i == 0: - #This will be reset to 0 below + # This will be reset to 0 below i = -1 break i -= 1 @@ -209,13 +209,13 @@ class TreeBuilder(object): # Step 8 entry = self.activeFormattingElements[i] - clone = entry.cloneNode() #Mainly to get a new copy of the attributes + clone = entry.cloneNode() # Mainly to get a new copy of the attributes # Step 9 - element = self.insertElement({"type":"StartTag", - "name":clone.name, - "namespace":clone.namespace, - "data":clone.attributes}) + element = self.insertElement({"type": "StartTag", + "name": clone.name, + "namespace": clone.namespace, + "data": clone.attributes}) # Step 10 self.activeFormattingElements[i] = element @@ -260,7 +260,7 @@ class TreeBuilder(object): if parent is None: parent = self.openElements[-1] parent.appendChild(self.commentClass(token["data"])) - + def createElement(self, token): """Create an element but don't insert it anywhere""" name = token["name"] @@ -282,10 +282,10 @@ class TreeBuilder(object): self.insertElement = self.insertElementNormal insertFromTable = property(_getInsertFromTable, _setInsertFromTable) - + def insertElementNormal(self, token): name = token["name"] - assert type(name) == unicode, "Element %s not unicode"%name + assert isinstance(name, text_type), "Element %s not unicode" % name namespace = token.get("namespace", self.defaultNamespace) element = self.elementClass(name, namespace) element.attributes = token["data"] @@ -294,13 +294,13 @@ class TreeBuilder(object): return element def insertElementTable(self, token): - """Create an element and insert it into the tree""" + """Create an element and insert it into the tree""" element = self.createElement(token) if self.openElements[-1].name not in tableInsertModeElements: return self.insertElementNormal(token) else: - #We should be in the InTable mode. This means we want to do - #special magic element rearranging + # We should be in the InTable mode. This means we want to do + # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() if insertBefore is None: parent.appendChild(element) @@ -315,7 +315,7 @@ class TreeBuilder(object): parent = self.openElements[-1] if (not self.insertFromTable or (self.insertFromTable and - self.openElements[-1].name + self.openElements[-1].name not in tableInsertModeElements)): parent.insertText(data) else: @@ -323,14 +323,14 @@ class TreeBuilder(object): # special magic element rearranging parent, insertBefore = self.getTableMisnestedNodePosition() parent.insertText(data, insertBefore) - + def getTableMisnestedNodePosition(self): """Get the foster parent element, and sibling to insert before (or None) when inserting a misnested table node""" # The foster parent element is the one which comes before the most # recently opened table element # XXX - this is really inelegant - lastTable=None + lastTable = None fosterParent = None insertBefore = None for elm in self.openElements[::-1]: @@ -354,7 +354,7 @@ class TreeBuilder(object): name = self.openElements[-1].name # XXX td, th and tr are not actually needed if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) - and name != exclude): + and name != exclude): self.openElements.pop() # XXX This is not entirely what the specification says. We should # investigate it more closely. @@ -363,10 +363,10 @@ class TreeBuilder(object): def getDocument(self): "Return the final tree" return self.document - + def getFragment(self): "Return the final fragment" - #assert self.innerHTML + # assert self.innerHTML fragment = self.fragmentClass() self.openElements[0].reparentChildren(fragment) return fragment diff --git a/libs/html5lib/treebuilders/dom.py b/libs/html5lib/treebuilders/dom.py index 9578da2b..61e5ed79 100644 --- a/libs/html5lib/treebuilders/dom.py +++ b/libs/html5lib/treebuilders/dom.py @@ -1,45 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals -from xml.dom import minidom, Node, XML_NAMESPACE, XMLNS_NAMESPACE -try: - from types import ModuleType -except: - from new import module as ModuleType -import re + +from xml.dom import minidom, Node import weakref -import _base -from html5lib import constants, ihatexml -from html5lib.constants import namespaces +from . import _base +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory -moduleCache = {} - -def getDomModule(DomImplementation): - name = "_" + DomImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType(name) - objs = getDomBuilder(DomImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getDomBuilder(DomImplementation): Dom = DomImplementation + class AttrList(object): def __init__(self, element): self.element = element + def __iter__(self): - return self.element.attributes.items().__iter__() + return list(self.element.attributes.items()).__iter__() + def __setitem__(self, name, value): self.element.setAttribute(name, value) + def __len__(self): - return len(self.element.attributes.items()) + return len(list(self.element.attributes.items())) + def items(self): return [(item[0], item[1]) for item in - self.element.attributes.items()] + list(self.element.attributes.items())] + def keys(self): - return self.element.attributes.keys() + return list(self.element.attributes.keys()) + def __getitem__(self, name): return self.element.getAttribute(name) @@ -48,68 +41,68 @@ def getDomBuilder(DomImplementation): raise NotImplementedError else: return self.element.hasAttribute(name) - + class NodeBuilder(_base.Node): def __init__(self, element): _base.Node.__init__(self, element.nodeName) self.element = element - namespace = property(lambda self:hasattr(self.element, "namespaceURI") + namespace = property(lambda self: hasattr(self.element, "namespaceURI") and self.element.namespaceURI or None) def appendChild(self, node): node.parent = self self.element.appendChild(node.element) - + def insertText(self, data, insertBefore=None): text = self.element.ownerDocument.createTextNode(data) if insertBefore: self.element.insertBefore(text, insertBefore.element) else: self.element.appendChild(text) - + def insertBefore(self, node, refNode): self.element.insertBefore(node.element, refNode.element) node.parent = self - + def removeChild(self, node): if node.element.parentNode == self.element: self.element.removeChild(node.element) node.parent = None - + def reparentChildren(self, newParent): while self.element.hasChildNodes(): child = self.element.firstChild self.element.removeChild(child) newParent.element.appendChild(child) self.childNodes = [] - + def getAttributes(self): return AttrList(self.element) - + def setAttributes(self, attributes): if attributes: - for name, value in attributes.items(): + for name, value in list(attributes.items()): if isinstance(name, tuple): if name[0] is not None: qualifiedName = (name[0] + ":" + name[1]) else: qualifiedName = name[1] - self.element.setAttributeNS(name[2], qualifiedName, + self.element.setAttributeNS(name[2], qualifiedName, value) else: self.element.setAttribute( name, value) attributes = property(getAttributes, setAttributes) - + def cloneNode(self): return NodeBuilder(self.element.cloneNode(False)) - + def hasContent(self): return self.element.hasChildNodes() def getNameTuple(self): - if self.namespace == None: + if self.namespace is None: return namespaces["html"], self.name else: return self.namespace, self.name @@ -118,9 +111,9 @@ def getDomBuilder(DomImplementation): class TreeBuilder(_base.TreeBuilder): def documentClass(self): - self.dom = Dom.getDOMImplementation().createDocument(None,None,None) + self.dom = Dom.getDOMImplementation().createDocument(None, None, None) return weakref.proxy(self) - + def insertDoctype(self, token): name = token["name"] publicId = token["publicId"] @@ -131,7 +124,7 @@ def getDomBuilder(DomImplementation): self.document.appendChild(NodeBuilder(doctype)) if Dom == minidom: doctype.ownerDocument = self.dom - + def elementClass(self, name, namespace=None): if namespace is None and self.defaultNamespace is None: node = self.dom.createElement(name) @@ -139,70 +132,72 @@ def getDomBuilder(DomImplementation): node = self.dom.createElementNS(namespace, name) return NodeBuilder(node) - + def commentClass(self, data): return NodeBuilder(self.dom.createComment(data)) - + def fragmentClass(self): return NodeBuilder(self.dom.createDocumentFragment()) - + def appendChild(self, node): self.dom.appendChild(node.element) - + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): return self.dom - + def getFragment(self): return _base.TreeBuilder.getFragment(self).element - + def insertText(self, data, parent=None): - data=data - if parent <> self: + data = data + if parent != self: _base.TreeBuilder.insertText(self, data, parent) else: # HACK: allow text nodes as children of the document node if hasattr(self.dom, '_child_node_types'): if not Node.TEXT_NODE in self.dom._child_node_types: - self.dom._child_node_types=list(self.dom._child_node_types) + self.dom._child_node_types = list(self.dom._child_node_types) self.dom._child_node_types.append(Node.TEXT_NODE) self.dom.appendChild(self.dom.createTextNode(data)) - + + implementation = DomImplementation name = None - + def testSerializer(element): element.normalize() rv = [] + def serializeElement(element, indent=0): if element.nodeType == Node.DOCUMENT_TYPE_NODE: if element.name: if element.publicId or element.systemId: publicId = element.publicId or "" systemId = element.systemId or "" - rv.append( """|%s"""%( - ' '*indent, element.name, publicId, systemId)) + rv.append("""|%s""" % + (' ' * indent, element.name, publicId, systemId)) else: - rv.append("|%s"%(' '*indent, element.name)) + rv.append("|%s" % (' ' * indent, element.name)) else: - rv.append("|%s"%(' '*indent,)) + rv.append("|%s" % (' ' * indent,)) elif element.nodeType == Node.DOCUMENT_NODE: rv.append("#document") elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE: rv.append("#document-fragment") elif element.nodeType == Node.COMMENT_NODE: - rv.append("|%s"%(' '*indent, element.nodeValue)) + rv.append("|%s" % (' ' * indent, element.nodeValue)) elif element.nodeType == Node.TEXT_NODE: - rv.append("|%s\"%s\"" %(' '*indent, element.nodeValue)) + rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue)) else: if (hasattr(element, "namespaceURI") and - element.namespaceURI != None): - name = "%s %s"%(constants.prefixes[element.namespaceURI], - element.nodeName) + element.namespaceURI is not None): + name = "%s %s" % (constants.prefixes[element.namespaceURI], + element.nodeName) else: name = element.nodeName - rv.append("|%s<%s>"%(' '*indent, name)) + rv.append("|%s<%s>" % (' ' * indent, name)) if element.hasAttributes(): attributes = [] for i in range(len(element.attributes)): @@ -211,81 +206,22 @@ def getDomBuilder(DomImplementation): value = attr.value ns = attr.namespaceURI if ns: - name = "%s %s"%(constants.prefixes[ns], attr.localName) + name = "%s %s" % (constants.prefixes[ns], attr.localName) else: name = attr.nodeName attributes.append((name, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) indent += 2 for child in element.childNodes: serializeElement(child, indent) serializeElement(element, 0) - + return "\n".join(rv) - - def dom2sax(node, handler, nsmap={'xml':XML_NAMESPACE}): - if node.nodeType == Node.ELEMENT_NODE: - if not nsmap: - handler.startElement(node.nodeName, node.attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElement(node.nodeName) - else: - attributes = dict(node.attributes.itemsNS()) - - # gather namespace declarations - prefixes = [] - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if (attr.namespaceURI == XMLNS_NAMESPACE or - (attr.namespaceURI == None and attr.nodeName.startswith('xmlns'))): - prefix = (attr.nodeName != 'xmlns' and attr.nodeName or None) - handler.startPrefixMapping(prefix, attr.nodeValue) - prefixes.append(prefix) - nsmap = nsmap.copy() - nsmap[prefix] = attr.nodeValue - del attributes[(attr.namespaceURI, attr.nodeName)] - - # apply namespace declarations - for attrname in node.attributes.keys(): - attr = node.getAttributeNode(attrname) - if attr.namespaceURI == None and ':' in attr.nodeName: - prefix = attr.nodeName.split(':')[0] - if nsmap.has_key(prefix): - del attributes[(attr.namespaceURI, attr.nodeName)] - attributes[(nsmap[prefix],attr.nodeName)]=attr.nodeValue - - # SAX events - ns = node.namespaceURI or nsmap.get(None,None) - handler.startElementNS((ns,node.nodeName), node.nodeName, attributes) - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endElementNS((ns, node.nodeName), node.nodeName) - for prefix in prefixes: handler.endPrefixMapping(prefix) - - elif node.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: - handler.characters(node.nodeValue) - - elif node.nodeType == Node.DOCUMENT_NODE: - handler.startDocument() - for child in node.childNodes: dom2sax(child, handler, nsmap) - handler.endDocument() - - elif node.nodeType == Node.DOCUMENT_FRAGMENT_NODE: - for child in node.childNodes: dom2sax(child, handler, nsmap) - - else: - # ATTRIBUTE_NODE - # ENTITY_NODE - # PROCESSING_INSTRUCTION_NODE - # COMMENT_NODE - # DOCUMENT_TYPE_NODE - # NOTATION_NODE - pass - + return locals() -# Keep backwards compatibility with things that directly load -# classes/functions from this module -for key, value in getDomModule(minidom).__dict__.items(): - globals()[key] = value + +# The actual means to get a module! +getDomModule = moduleFactoryFactory(getDomBuilder) diff --git a/libs/html5lib/treebuilders/etree.py b/libs/html5lib/treebuilders/etree.py index 95be4755..2c8ed19f 100755 --- a/libs/html5lib/treebuilders/etree.py +++ b/libs/html5lib/treebuilders/etree.py @@ -1,32 +1,21 @@ -try: - from types import ModuleType -except: - from new import module as ModuleType -import re -import types +from __future__ import absolute_import, division, unicode_literals +from six import text_type -import _base -from html5lib import ihatexml -from html5lib import constants -from html5lib.constants import namespaces +import re + +from . import _base +from .. import ihatexml +from .. import constants +from ..constants import namespaces +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation, fullTree=False): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation, fullTree) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation, fullTree=False): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag + class Element(_base.Node): def __init__(self, name, namespace=None): self._name = name @@ -45,16 +34,16 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) return etree_tag - + def _setName(self, name): self._name = name self._element.tag = self._getETreeTag(self._name, self._namespace) - + def _getName(self): return self._name - + name = property(_getName, _setName) def _setNamespace(self, namespace): @@ -65,81 +54,82 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): return self._namespace namespace = property(_getNamespace, _setNamespace) - + def _getAttributes(self): return self._element.attrib - + def _setAttributes(self, attributes): - #Delete existing attributes first - #XXX - there may be a better way to do this... - for key in self._element.attrib.keys(): + # Delete existing attributes first + # XXX - there may be a better way to do this... + for key in list(self._element.attrib.keys()): del self._element.attrib[key] - for key, value in attributes.iteritems(): + for key, value in attributes.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], key[1]) + name = "{%s}%s" % (key[2], key[1]) else: name = key self._element.set(name, value) - + attributes = property(_getAttributes, _setAttributes) - + def _getChildNodes(self): - return self._childNodes + return self._childNodes + def _setChildNodes(self, value): del self._element[:] self._childNodes = [] for element in value: self.insertChild(element) - + childNodes = property(_getChildNodes, _setChildNodes) - + def hasContent(self): """Return true if the node has children or text""" return bool(self._element.text or len(self._element)) - + def appendChild(self, node): self._childNodes.append(node) self._element.append(node._element) node.parent = self - + def insertBefore(self, node, refNode): index = list(self._element).index(refNode._element) self._element.insert(index, node._element) node.parent = self - + def removeChild(self, node): self._element.remove(node._element) - node.parent=None - + node.parent = None + def insertText(self, data, insertBefore=None): if not(len(self._element)): if not self._element.text: self._element.text = "" self._element.text += data elif insertBefore is None: - #Insert the text as the tail of the last child element + # Insert the text as the tail of the last child element if not self._element[-1].tail: self._element[-1].tail = "" self._element[-1].tail += data else: - #Insert the text before the specified node + # Insert the text before the specified node children = list(self._element) index = children.index(insertBefore._element) if index > 0: - if not self._element[index-1].tail: - self._element[index-1].tail = "" - self._element[index-1].tail += data + if not self._element[index - 1].tail: + self._element[index - 1].tail = "" + self._element[index - 1].tail += data else: if not self._element.text: self._element.text = "" self._element.text += data - + def cloneNode(self): element = type(self)(self.name, self.namespace) - for name, value in self.attributes.iteritems(): + for name, value in self.attributes.items(): element.attributes[name] = value return element - + def reparentChildren(self, newParent): if newParent.childNodes: newParent.childNodes[-1]._element.tail += self._element.text @@ -150,60 +140,60 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): newParent._element.text += self._element.text self._element.text = "" _base.Node.reparentChildren(self, newParent) - + class Comment(Element): def __init__(self, data): - #Use the superclass constructor to set all properties on the - #wrapper element + # Use the superclass constructor to set all properties on the + # wrapper element self._element = ElementTree.Comment(data) self.parent = None self._childNodes = [] self._flags = [] - + def _getData(self): return self._element.text - + def _setData(self, value): self._element.text = value - + data = property(_getData, _setData) - + class DocumentType(Element): def __init__(self, name, publicId, systemId): - Element.__init__(self, "") + Element.__init__(self, "") self._element.text = name self.publicId = publicId self.systemId = systemId def _getPublicId(self): - return self._element.get(u"publicId", "") + return self._element.get("publicId", "") def _setPublicId(self, value): if value is not None: - self._element.set(u"publicId", value) + self._element.set("publicId", value) publicId = property(_getPublicId, _setPublicId) - + def _getSystemId(self): - return self._element.get(u"systemId", "") + return self._element.get("systemId", "") def _setSystemId(self, value): if value is not None: - self._element.set(u"systemId", value) + self._element.set("systemId", value) systemId = property(_getSystemId, _setSystemId) - + class Document(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_ROOT") + class DocumentFragment(Element): def __init__(self): - Element.__init__(self, "") - + Element.__init__(self, "DOCUMENT_FRAGMENT") + def testSerializer(element): rv = [] - finalText = None + def serializeElement(element, indent=0): if not(hasattr(element, "tag")): element = element.getroot() @@ -211,20 +201,23 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": + rv.append("""""" % + (element.text, publicId, systemId)) + else: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": rv.append("#document") - if element.text: - rv.append("|%s\"%s\""%(' '*(indent+2), element.text)) - if element.tail: - finalText = element.tail - elif element.tag == ElementTree.Comment: - rv.append("|%s"%(' '*indent, element.text)) + if element.text is not None: + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + elif element.tag == ElementTreeCommentType: + rv.append("|%s" % (' ' * indent, element.text)) else: - assert type(element.tag) in types.StringTypes, "Expected unicode, got %s"%type(element.tag) + assert isinstance(element.tag, text_type), \ + "Expected unicode, got %s, %s" % (type(element.tag), element.tag) nsmatch = tag_regexp.match(element.tag) if nsmatch is None: @@ -232,113 +225,113 @@ def getETreeBuilder(ElementTreeImplementation, fullTree=False): else: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - name = "%s %s"%(prefix, name) - rv.append("|%s<%s>"%(' '*indent, name)) + name = "%s %s" % (prefix, name) + rv.append("|%s<%s>" % (' ' * indent, name)) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: attr_string = name attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 for child in element: serializeElement(child, indent) if element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) - - if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) - + return "\n".join(rv) - + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] - finalText = None filter = ihatexml.InfosetFilter() + def serializeElement(element): - if type(element) == type(ElementTree.ElementTree): + if isinstance(element, ElementTree.ElementTree): element = element.getroot() - + if element.tag == "": if element.get("publicId") or element.get("systemId"): publicId = element.get("publicId") or "" systemId = element.get("systemId") or "" - rv.append( """"""%( - element.text, publicId, systemId)) - else: - rv.append(""%(element.text,)) - elif element.tag == "": - if element.text: - rv.append(element.text) - if element.tail: - finalText = element.tail - - for child in element: - serializeElement(child) - - elif type(element.tag) == type(ElementTree.Comment): - rv.append(""%(element.text,)) - else: - #This is assumed to be an ordinary element - if not element.attrib: - rv.append("<%s>"%(filter.fromXmlName(element.tag),)) + rv.append("""""" % + (element.text, publicId, systemId)) else: - attr = " ".join(["%s=\"%s\""%( - filter.fromXmlName(name), value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) - if element.text: + rv.append("" % (element.text,)) + elif element.tag == "DOCUMENT_ROOT": + if element.text is not None: rv.append(element.text) - + if element.tail is not None: + raise TypeError("Document node cannot have tail") + if hasattr(element, "attrib") and len(element.attrib): + raise TypeError("Document node cannot have attributes") + for child in element: serializeElement(child) - - rv.append("%s>"%(element.tag,)) - + + elif element.tag == ElementTreeCommentType: + rv.append("" % (element.text,)) + else: + # This is assumed to be an ordinary element + if not element.attrib: + rv.append("<%s>" % (filter.fromXmlName(element.tag),)) + else: + attr = " ".join(["%s=\"%s\"" % ( + filter.fromXmlName(name), value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) + if element.text: + rv.append(element.text) + + for child in element: + serializeElement(child) + + rv.append("%s>" % (element.tag,)) + if element.tail: rv.append(element.tail) - + serializeElement(element) - - if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) - + return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = Element commentClass = Comment fragmentClass = DocumentFragment - + implementation = ElementTreeImplementation + def testSerializer(self, element): return testSerializer(element) - + def getDocument(self): if fullTree: return self.document._element else: if self.defaultNamespace is not None: return self.document._element.find( - "{%s}html"%self.defaultNamespace) + "{%s}html" % self.defaultNamespace) else: return self.document._element.find("html") - + def getFragment(self): return _base.TreeBuilder.getFragment(self)._element - + return locals() + + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treebuilders/etree_lxml.py b/libs/html5lib/treebuilders/etree_lxml.py index eee1e3b2..35d08efa 100644 --- a/libs/html5lib/treebuilders/etree_lxml.py +++ b/libs/html5lib/treebuilders/etree_lxml.py @@ -1,20 +1,3 @@ -import warnings -import re - -import _base -from html5lib.constants import DataLossWarning -import html5lib.constants as constants -import etree as etree_builders -from html5lib import ihatexml - -try: - import lxml.etree as etree -except ImportError: - pass - -fullTree = True -tag_regexp = re.compile("{([^}]*)}(.*)") - """Module for supporting the lxml.etree library. The idea here is to use as much of the native library as possible, without using fragile hacks like custom element names that break between releases. The downside of this is that we cannot represent @@ -26,12 +9,34 @@ Docypes with no name When any of these things occur, we emit a DataLossWarning """ +from __future__ import absolute_import, division, unicode_literals + +import warnings +import re +import sys + +from . import _base +from ..constants import DataLossWarning +from .. import constants +from . import etree as etree_builders +from .. import ihatexml + +import lxml.etree as etree + + +fullTree = True +tag_regexp = re.compile("{([^}]*)}(.*)") + +comment_type = etree.Comment("asd").tag + + class DocumentType(object): def __init__(self, name, publicId, systemId): - self.name = name + self.name = name self.publicId = publicId self.systemId = systemId + class Document(object): def __init__(self): self._elementTree = None @@ -42,118 +47,126 @@ class Document(object): def _getChildNodes(self): return self._childNodes - + childNodes = property(_getChildNodes) + def testSerializer(element): rv = [] finalText = None - filter = ihatexml.InfosetFilter() + infosetFilter = ihatexml.InfosetFilter() + def serializeElement(element, indent=0): if not hasattr(element, "tag"): - if hasattr(element, "getroot"): - #Full tree case + if hasattr(element, "getroot"): + # Full tree case rv.append("#document") if element.docinfo.internalDTD: - if not (element.docinfo.public_id or + if not (element.docinfo.public_id or element.docinfo.system_url): - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name else: - dtd_str = """"""%( - element.docinfo.root_name, + dtd_str = """""" % ( + element.docinfo.root_name, element.docinfo.public_id, element.docinfo.system_url) - rv.append("|%s%s"%(' '*(indent+2), dtd_str)) + rv.append("|%s%s" % (' ' * (indent + 2), dtd_str)) next_element = element.getroot() while next_element.getprevious() is not None: next_element = next_element.getprevious() while next_element is not None: - serializeElement(next_element, indent+2) + serializeElement(next_element, indent + 2) next_element = next_element.getnext() - elif isinstance(element, basestring): - #Text in a fragment - rv.append("|%s\"%s\""%(' '*indent, element)) + elif isinstance(element, str) or isinstance(element, bytes): + # Text in a fragment + assert isinstance(element, str) or sys.version_info.major == 2 + rv.append("|%s\"%s\"" % (' ' * indent, element)) else: - #Fragment case + # Fragment case rv.append("#document-fragment") for next_element in element: - serializeElement(next_element, indent+2) - elif type(element.tag) == type(etree.Comment): - rv.append("|%s"%(' '*indent, element.text)) + serializeElement(next_element, indent + 2) + elif element.tag == comment_type: + rv.append("|%s" % (' ' * indent, element.text)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * indent, element.tail)) else: + assert isinstance(element, etree._Element) nsmatch = etree_builders.tag_regexp.match(element.tag) if nsmatch is not None: ns = nsmatch.group(1) tag = nsmatch.group(2) prefix = constants.prefixes[ns] - rv.append("|%s<%s %s>"%(' '*indent, prefix, - filter.fromXmlName(tag))) + rv.append("|%s<%s %s>" % (' ' * indent, prefix, + infosetFilter.fromXmlName(tag))) else: - rv.append("|%s<%s>"%(' '*indent, - filter.fromXmlName(element.tag))) + rv.append("|%s<%s>" % (' ' * indent, + infosetFilter.fromXmlName(element.tag))) if hasattr(element, "attrib"): attributes = [] - for name, value in element.attrib.iteritems(): + for name, value in element.attrib.items(): nsmatch = tag_regexp.match(name) if nsmatch is not None: ns, name = nsmatch.groups() - name = filter.fromXmlName(name) + name = infosetFilter.fromXmlName(name) prefix = constants.prefixes[ns] - attr_string = "%s %s"%(prefix, name) + attr_string = "%s %s" % (prefix, name) else: - attr_string = filter.fromXmlName(name) + attr_string = infosetFilter.fromXmlName(name) attributes.append((attr_string, value)) for name, value in sorted(attributes): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) if element.text: - rv.append("|%s\"%s\"" %(' '*(indent+2), element.text)) + rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text)) indent += 2 - for child in element.getchildren(): + for child in element: serializeElement(child, indent) - if hasattr(element, "tail") and element.tail: - rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail)) + if hasattr(element, "tail") and element.tail: + rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail)) serializeElement(element, 0) if finalText is not None: - rv.append("|%s\"%s\""%(' '*2, finalText)) + rv.append("|%s\"%s\"" % (' ' * 2, finalText)) return "\n".join(rv) + def tostring(element): """Serialize an element and its child nodes to a string""" rv = [] finalText = None + def serializeElement(element): if not hasattr(element, "tag"): if element.docinfo.internalDTD: if element.docinfo.doctype: dtd_str = element.docinfo.doctype else: - dtd_str = ""%element.docinfo.root_name + dtd_str = "" % element.docinfo.root_name rv.append(dtd_str) serializeElement(element.getroot()) - - elif type(element.tag) == type(etree.Comment): - rv.append(""%(element.text,)) - + + elif element.tag == comment_type: + rv.append("" % (element.text,)) + else: - #This is assumed to be an ordinary element + # This is assumed to be an ordinary element if not element.attrib: - rv.append("<%s>"%(element.tag,)) + rv.append("<%s>" % (element.tag,)) else: - attr = " ".join(["%s=\"%s\""%(name, value) - for name, value in element.attrib.iteritems()]) - rv.append("<%s %s>"%(element.tag, attr)) + attr = " ".join(["%s=\"%s\"" % (name, value) + for name, value in element.attrib.items()]) + rv.append("<%s %s>" % (element.tag, attr)) if element.text: rv.append(element.text) - for child in element.getchildren(): + for child in element: serializeElement(child) - rv.append("%s>"%(element.tag,)) + rv.append("%s>" % (element.tag,)) if hasattr(element, "tail") and element.tail: rv.append(element.tail) @@ -161,56 +174,57 @@ def tostring(element): serializeElement(element) if finalText is not None: - rv.append("%s\""%(' '*2, finalText)) + rv.append("%s\"" % (' ' * 2, finalText)) return "".join(rv) - + class TreeBuilder(_base.TreeBuilder): documentClass = Document doctypeClass = DocumentType elementClass = None commentClass = None - fragmentClass = Document + fragmentClass = Document + implementation = etree - def __init__(self, namespaceHTMLElements, fullTree = False): + def __init__(self, namespaceHTMLElements, fullTree=False): builder = etree_builders.getETreeModule(etree, fullTree=fullTree) - filter = self.filter = ihatexml.InfosetFilter() + infosetFilter = self.infosetFilter = ihatexml.InfosetFilter() self.namespaceHTMLElements = namespaceHTMLElements class Attributes(dict): def __init__(self, element, value={}): self._element = element dict.__init__(self, value) - for key, value in self.iteritems(): + for key, value in self.items(): if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value def __setitem__(self, key, value): dict.__setitem__(self, key, value) if isinstance(key, tuple): - name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1])) + name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: - name = filter.coerceAttribute(key) + name = infosetFilter.coerceAttribute(key) self._element._element.attrib[name] = value class Element(builder.Element): def __init__(self, name, namespace): - name = filter.coerceElement(name) + name = infosetFilter.coerceElement(name) builder.Element.__init__(self, name, namespace=namespace) self._attributes = Attributes(self) def _setName(self, name): - self._name = filter.coerceElement(name) + self._name = infosetFilter.coerceElement(name) self._element.tag = self._getETreeTag( self._name, self._namespace) - + def _getName(self): - return filter.fromXmlName(self._name) - + return infosetFilter.fromXmlName(self._name) + name = property(_getName, _setName) def _getAttributes(self): @@ -218,24 +232,23 @@ class TreeBuilder(_base.TreeBuilder): def _setAttributes(self, attributes): self._attributes = Attributes(self, attributes) - + attributes = property(_getAttributes, _setAttributes) def insertText(self, data, insertBefore=None): - data = filter.coerceCharacters(data) + data = infosetFilter.coerceCharacters(data) builder.Element.insertText(self, data, insertBefore) def appendChild(self, child): builder.Element.appendChild(self, child) - class Comment(builder.Comment): def __init__(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) builder.Comment.__init__(self, data) def _setData(self, data): - data = filter.coerceComment(data) + data = infosetFilter.coerceComment(data) self._element.text = data def _getData(self): @@ -245,9 +258,9 @@ class TreeBuilder(_base.TreeBuilder): self.elementClass = Element self.commentClass = builder.Comment - #self.fragmentClass = builder.DocumentFragment + # self.fragmentClass = builder.DocumentFragment _base.TreeBuilder.__init__(self, namespaceHTMLElements) - + def reset(self): _base.TreeBuilder.reset(self) self.insertComment = self.insertCommentInitial @@ -262,13 +275,13 @@ class TreeBuilder(_base.TreeBuilder): return self.document._elementTree else: return self.document._elementTree.getroot() - + def getFragment(self): fragment = [] element = self.openElements[0]._element if element.text: fragment.append(element.text) - fragment.extend(element.getchildren()) + fragment.extend(list(element)) if element.tail: fragment.append(element.tail) return fragment @@ -278,59 +291,79 @@ class TreeBuilder(_base.TreeBuilder): publicId = token["publicId"] systemId = token["systemId"] - if not name or ihatexml.nonXmlNameBMPRegexp.search(name) or name[0] == '"': - warnings.warn("lxml cannot represent null or non-xml doctype", DataLossWarning) + if not name: + warnings.warn("lxml cannot represent empty doctype", DataLossWarning) + self.doctype = None + else: + coercedName = self.infosetFilter.coerceElement(name) + if coercedName != name: + warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning) + + doctype = self.doctypeClass(coercedName, publicId, systemId) + self.doctype = doctype - doctype = self.doctypeClass(name, publicId, systemId) - self.doctype = doctype - def insertCommentInitial(self, data, parent=None): self.initial_comments.append(data) - + + def insertCommentMain(self, data, parent=None): + if (parent == self.document and + self.document._elementTree.getroot()[-1].tag == comment_type): + warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning) + super(TreeBuilder, self).insertComment(data, parent) + def insertRoot(self, token): """Create the document root""" - #Because of the way libxml2 works, it doesn't seem to be possible to - #alter information like the doctype after the tree has been parsed. - #Therefore we need to use the built-in parser to create our iniial - #tree, after which we can add elements like normal + # Because of the way libxml2 works, it doesn't seem to be possible to + # alter information like the doctype after the tree has been parsed. + # Therefore we need to use the built-in parser to create our iniial + # tree, after which we can add elements like normal docStr = "" - if self.doctype and self.doctype.name and not self.doctype.name.startswith('"'): - docStr += "= 0 and sysid.find('"') >= 0: + warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning) + sysid = sysid.replace("'", 'U00027') + if sysid.find("'") >= 0: + docStr += '"%s"' % sysid + else: + docStr += "'%s'" % sysid + else: + docStr += "''" docStr += ">" + if self.doctype.name != token["name"]: + warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning) docStr += "" - - try: - root = etree.fromstring(docStr) - except etree.XMLSyntaxError: - print docStr - raise - - #Append the initial comments: + root = etree.fromstring(docStr) + + # Append the initial comments: for comment_token in self.initial_comments: root.addprevious(etree.Comment(comment_token["data"])) - - #Create the root document and add the ElementTree to it + + # Create the root document and add the ElementTree to it self.document = self.documentClass() self.document._elementTree = root.getroottree() - + # Give the root element the right name name = token["name"] namespace = token.get("namespace", self.defaultNamespace) if namespace is None: etree_tag = name else: - etree_tag = "{%s}%s"%(namespace, name) + etree_tag = "{%s}%s" % (namespace, name) root.tag = etree_tag - - #Add the root element to the internal child/open data structures + + # Add the root element to the internal child/open data structures root_element = self.elementClass(name, namespace) root_element._element = root self.document._childNodes.append(root_element) self.openElements.append(root_element) - - #Reset to the default insert comment function - self.insertComment = super(TreeBuilder, self).insertComment + + # Reset to the default insert comment function + self.insertComment = self.insertCommentMain diff --git a/libs/html5lib/treebuilders/simpletree.py b/libs/html5lib/treebuilders/simpletree.py deleted file mode 100755 index 67fe7583..00000000 --- a/libs/html5lib/treebuilders/simpletree.py +++ /dev/null @@ -1,256 +0,0 @@ -import _base -from html5lib.constants import voidElements, namespaces, prefixes -from xml.sax.saxutils import escape - -# Really crappy basic implementation of a DOM-core like thing -class Node(_base.Node): - type = -1 - def __init__(self, name): - self.name = name - self.parent = None - self.value = None - self.childNodes = [] - self._flags = [] - - def __iter__(self): - for node in self.childNodes: - yield node - for item in node: - yield item - - def __unicode__(self): - return self.name - - def toxml(self): - raise NotImplementedError - - def printTree(self, indent=0): - tree = '\n|%s%s' % (' '* indent, unicode(self)) - for child in self.childNodes: - tree += child.printTree(indent + 2) - return tree - - def appendChild(self, node): - assert isinstance(node, Node) - if (isinstance(node, TextNode) and self.childNodes and - isinstance(self.childNodes[-1], TextNode)): - self.childNodes[-1].value += node.value - else: - self.childNodes.append(node) - node.parent = self - - def insertText(self, data, insertBefore=None): - assert isinstance(data, unicode), "data %s is of type %s expected unicode"%(repr(data), type(data)) - if insertBefore is None: - self.appendChild(TextNode(data)) - else: - self.insertBefore(TextNode(data), insertBefore) - - def insertBefore(self, node, refNode): - index = self.childNodes.index(refNode) - if (isinstance(node, TextNode) and index > 0 and - isinstance(self.childNodes[index - 1], TextNode)): - self.childNodes[index - 1].value += node.value - else: - self.childNodes.insert(index, node) - node.parent = self - - def removeChild(self, node): - try: - self.childNodes.remove(node) - except: - # XXX - raise - node.parent = None - - def cloneNode(self): - raise NotImplementedError - - def hasContent(self): - """Return true if the node has children or text""" - return bool(self.childNodes) - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class Document(Node): - type = 1 - def __init__(self): - Node.__init__(self, None) - - def __str__(self): - return "#document" - - def __unicode__(self): - return str(self) - - def appendChild(self, child): - Node.appendChild(self, child) - - def toxml(self, encoding="utf=8"): - result = "" - for child in self.childNodes: - result += child.toxml() - return result.encode(encoding) - - def hilite(self, encoding="utf-8"): - result = "" - for child in self.childNodes: - result += child.hilite() - return result.encode(encoding) + "" - - def printTree(self): - tree = unicode(self) - for child in self.childNodes: - tree += child.printTree(2) - return tree - - def cloneNode(self): - return Document() - -class DocumentFragment(Document): - type = 2 - def __str__(self): - return "#document-fragment" - - def __unicode__(self): - return str(self) - - def cloneNode(self): - return DocumentFragment() - -class DocumentType(Node): - type = 3 - def __init__(self, name, publicId, systemId): - Node.__init__(self, name) - self.publicId = publicId - self.systemId = systemId - - def __unicode__(self): - if self.publicId or self.systemId: - publicId = self.publicId or "" - systemId = self.systemId or "" - return """"""%( - self.name, publicId, systemId) - - else: - return u"" % self.name - - - toxml = __unicode__ - - def hilite(self): - return '<!DOCTYPE %s>' % self.name - - def cloneNode(self): - return DocumentType(self.name, self.publicId, self.systemId) - -class TextNode(Node): - type = 4 - def __init__(self, value): - Node.__init__(self, None) - self.value = value - - def __unicode__(self): - return u"\"%s\"" % self.value - - def toxml(self): - return escape(self.value) - - hilite = toxml - - def cloneNode(self): - return TextNode(self.value) - -class Element(Node): - type = 5 - def __init__(self, name, namespace=None): - Node.__init__(self, name) - self.namespace = namespace - self.attributes = {} - - def __unicode__(self): - if self.namespace == None: - return u"<%s>" % self.name - else: - return u"<%s %s>"%(prefixes[self.namespace], self.name) - - def toxml(self): - result = '<' + self.name - if self.attributes: - for name,value in self.attributes.iteritems(): - result += u' %s="%s"' % (name, escape(value,{'"':'"'})) - if self.childNodes: - result += '>' - for child in self.childNodes: - result += child.toxml() - result += u'%s>' % self.name - else: - result += u'/>' - return result - - def hilite(self): - result = '<%s' % self.name - if self.attributes: - for name, value in self.attributes.iteritems(): - result += ' %s="%s"' % (name, escape(value, {'"':'"'})) - if self.childNodes: - result += ">" - for child in self.childNodes: - result += child.hilite() - elif self.name in voidElements: - return result + ">" - return result + '</%s>' % self.name - - def printTree(self, indent): - tree = '\n|%s%s' % (' '*indent, unicode(self)) - indent += 2 - if self.attributes: - for name, value in sorted(self.attributes.iteritems()): - if isinstance(name, tuple): - name = "%s %s"%(name[0], name[1]) - tree += '\n|%s%s="%s"' % (' ' * indent, name, value) - for child in self.childNodes: - tree += child.printTree(indent) - return tree - - def cloneNode(self): - newNode = Element(self.name) - if hasattr(self, 'namespace'): - newNode.namespace = self.namespace - for attr, value in self.attributes.iteritems(): - newNode.attributes[attr] = value - return newNode - -class CommentNode(Node): - type = 6 - def __init__(self, data): - Node.__init__(self, None) - self.data = data - - def __unicode__(self): - return "" % self.data - - def toxml(self): - return "" % self.data - - def hilite(self): - return '<!--%s-->' % escape(self.data) - - def cloneNode(self): - return CommentNode(self.data) - -class TreeBuilder(_base.TreeBuilder): - documentClass = Document - doctypeClass = DocumentType - elementClass = Element - commentClass = CommentNode - fragmentClass = DocumentFragment - - def testSerializer(self, node): - return node.printTree() diff --git a/libs/html5lib/treebuilders/soup.py b/libs/html5lib/treebuilders/soup.py deleted file mode 100644 index 9bc5ff0e..00000000 --- a/libs/html5lib/treebuilders/soup.py +++ /dev/null @@ -1,236 +0,0 @@ -import warnings - -warnings.warn("BeautifulSoup 3.x (as of 3.1) is not fully compatible with html5lib and support will be removed in the future", DeprecationWarning) - -from BeautifulSoup import BeautifulSoup, Tag, NavigableString, Comment, Declaration - -import _base -from html5lib.constants import namespaces, DataLossWarning - -class AttrList(object): - def __init__(self, element): - self.element = element - self.attrs = dict(self.element.attrs) - def __iter__(self): - return self.attrs.items().__iter__() - def __setitem__(self, name, value): - "set attr", name, value - self.element[name] = value - def items(self): - return self.attrs.items() - def keys(self): - return self.attrs.keys() - def __getitem__(self, name): - return self.attrs[name] - def __contains__(self, name): - return name in self.attrs.keys() - def __eq__(self, other): - if len(self.keys()) != len(other.keys()): - return False - for item in self.keys(): - if item not in other: - return False - if self[item] != other[item]: - return False - return True - -class Element(_base.Node): - def __init__(self, element, soup, namespace): - _base.Node.__init__(self, element.name) - self.element = element - self.soup = soup - self.namespace = namespace - - def _nodeIndex(self, node, refNode): - # Finds a node by identity rather than equality - for index in range(len(self.element.contents)): - if id(self.element.contents[index]) == id(refNode.element): - return index - return None - - def appendChild(self, node): - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[-1].__class__ == NavigableString): - # Concatenate new text onto old text node - # (TODO: This has O(n^2) performance, for input like "aaa...") - newStr = NavigableString(self.element.contents[-1]+node.element) - - # Remove the old text node - # (Can't simply use .extract() by itself, because it fails if - # an equal text node exists within the parent node) - oldElement = self.element.contents[-1] - del self.element.contents[-1] - oldElement.parent = None - oldElement.extract() - - self.element.insert(len(self.element.contents), newStr) - else: - self.element.insert(len(self.element.contents), node.element) - node.parent = self - - def getAttributes(self): - return AttrList(self.element) - - def setAttributes(self, attributes): - if attributes: - for name, value in attributes.items(): - self.element[name] = value - - attributes = property(getAttributes, setAttributes) - - def insertText(self, data, insertBefore=None): - text = TextNode(NavigableString(data), self.soup) - if insertBefore: - self.insertBefore(text, insertBefore) - else: - self.appendChild(text) - - def insertBefore(self, node, refNode): - index = self._nodeIndex(node, refNode) - if (node.element.__class__ == NavigableString and self.element.contents - and self.element.contents[index-1].__class__ == NavigableString): - # (See comments in appendChild) - newStr = NavigableString(self.element.contents[index-1]+node.element) - oldNode = self.element.contents[index-1] - del self.element.contents[index-1] - oldNode.parent = None - oldNode.extract() - - self.element.insert(index-1, newStr) - else: - self.element.insert(index, node.element) - node.parent = self - - def removeChild(self, node): - index = self._nodeIndex(node.parent, node) - del node.parent.element.contents[index] - node.element.parent = None - node.element.extract() - node.parent = None - - def reparentChildren(self, newParent): - while self.element.contents: - child = self.element.contents[0] - child.extract() - if isinstance(child, Tag): - newParent.appendChild(Element(child, self.soup, namespaces["html"])) - else: - newParent.appendChild(TextNode(child, self.soup)) - - def cloneNode(self): - node = Element(Tag(self.soup, self.element.name), self.soup, self.namespace) - for key,value in self.attributes: - node.attributes[key] = value - return node - - def hasContent(self): - return self.element.contents - - def getNameTuple(self): - if self.namespace == None: - return namespaces["html"], self.name - else: - return self.namespace, self.name - - nameTuple = property(getNameTuple) - -class TextNode(Element): - def __init__(self, element, soup): - _base.Node.__init__(self, None) - self.element = element - self.soup = soup - - def cloneNode(self): - raise NotImplementedError - -class TreeBuilder(_base.TreeBuilder): - def __init__(self, namespaceHTMLElements): - if namespaceHTMLElements: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - _base.TreeBuilder.__init__(self, namespaceHTMLElements) - - def documentClass(self): - self.soup = BeautifulSoup("") - return Element(self.soup, self.soup, None) - - def insertDoctype(self, token): - name = token["name"] - publicId = token["publicId"] - systemId = token["systemId"] - - if publicId: - self.soup.insert(0, Declaration("DOCTYPE %s PUBLIC \"%s\" \"%s\""%(name, publicId, systemId or ""))) - elif systemId: - self.soup.insert(0, Declaration("DOCTYPE %s SYSTEM \"%s\""% - (name, systemId))) - else: - self.soup.insert(0, Declaration("DOCTYPE %s"%name)) - - def elementClass(self, name, namespace): - if namespace is not None: - warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning) - return Element(Tag(self.soup, name), self.soup, namespace) - - def commentClass(self, data): - return TextNode(Comment(data), self.soup) - - def fragmentClass(self): - self.soup = BeautifulSoup("") - self.soup.name = "[document_fragment]" - return Element(self.soup, self.soup, None) - - def appendChild(self, node): - self.soup.insert(len(self.soup.contents), node.element) - - def testSerializer(self, element): - return testSerializer(element) - - def getDocument(self): - return self.soup - - def getFragment(self): - return _base.TreeBuilder.getFragment(self).element - -def testSerializer(element): - import re - rv = [] - def serializeElement(element, indent=0): - if isinstance(element, Declaration): - doctype_regexp = r'DOCTYPE\s+(?P[^\s]*)( PUBLIC "(?P.*)" "(?P.*)"| SYSTEM "(?P.*)")?' - m = re.compile(doctype_regexp).match(element.string) - assert m is not None, "DOCTYPE did not match expected format" - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') or "" - else: - systemId = m.group('systemId2') - - if publicId is not None or systemId is not None: - rv.append("""|%s"""% - (' '*indent, name, publicId or "", systemId or "")) - else: - rv.append("|%s"%(' '*indent, name)) - - elif isinstance(element, BeautifulSoup): - if element.name == "[document_fragment]": - rv.append("#document-fragment") - else: - rv.append("#document") - - elif isinstance(element, Comment): - rv.append("|%s"%(' '*indent, element.string)) - elif isinstance(element, unicode): - rv.append("|%s\"%s\"" %(' '*indent, element)) - else: - rv.append("|%s<%s>"%(' '*indent, element.name)) - if element.attrs: - for name, value in sorted(element.attrs): - rv.append('|%s%s="%s"' % (' '*(indent+2), name, value)) - indent += 2 - if hasattr(element, "contents"): - for child in element.contents: - serializeElement(child, indent) - serializeElement(element, 0) - - return "\n".join(rv) diff --git a/libs/html5lib/treewalkers/__init__.py b/libs/html5lib/treewalkers/__init__.py index 3a606a8b..18124e75 100644 --- a/libs/html5lib/treewalkers/__init__.py +++ b/libs/html5lib/treewalkers/__init__.py @@ -8,23 +8,27 @@ implements a 'serialize' method taking a tree as sole argument and returning an iterator generating tokens. """ +from __future__ import absolute_import, division, unicode_literals + +import sys + +from ..utils import default_etree + treeWalkerCache = {} + def getTreeWalker(treeType, implementation=None, **kwargs): """Get a TreeWalker class for various types of tree with built-in support treeType - the name of the tree type required (case-insensitive). Supported - values are "simpletree", "dom", "etree" and "beautifulsoup" + values are: - "simpletree" - a built-in DOM-ish tree type with support for some - more pythonic idioms. "dom" - The xml.dom.minidom DOM implementation "pulldom" - The xml.dom.pulldom event stream "etree" - A generic walker for tree implementations exposing an elementtree-like interface (known to work with ElementTree, cElementTree and lxml.etree). "lxml" - Optimized walker for lxml.etree - "beautifulsoup" - Beautiful soup (if installed) "genshi" - a Genshi stream implementation - (Currently applies to the "etree" tree type only). A module @@ -33,20 +37,21 @@ def getTreeWalker(treeType, implementation=None, **kwargs): treeType = treeType.lower() if treeType not in treeWalkerCache: - if treeType in ("dom", "pulldom", "simpletree"): - mod = __import__(treeType, globals()) + if treeType in ("dom", "pulldom"): + name = "%s.%s" % (__name__, treeType) + __import__(name) + mod = sys.modules[name] treeWalkerCache[treeType] = mod.TreeWalker elif treeType == "genshi": - import genshistream + from . import genshistream treeWalkerCache[treeType] = genshistream.TreeWalker - elif treeType == "beautifulsoup": - import soup - treeWalkerCache[treeType] = soup.TreeWalker elif treeType == "lxml": - import lxmletree + from . import lxmletree treeWalkerCache[treeType] = lxmletree.TreeWalker elif treeType == "etree": - import etree + from . import etree + if implementation is None: + implementation = default_etree # XXX: NEVER cache here, caching is done in the etree submodule return etree.getETreeModule(implementation, **kwargs).TreeWalker return treeWalkerCache.get(treeType) diff --git a/libs/html5lib/treewalkers/_base.py b/libs/html5lib/treewalkers/_base.py index 5929ba05..34252e50 100644 --- a/libs/html5lib/treewalkers/_base.py +++ b/libs/html5lib/treewalkers/_base.py @@ -1,94 +1,9 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type, string_types + import gettext _ = gettext.gettext -from html5lib.constants import voidElements, spaceCharacters -spaceCharacters = u"".join(spaceCharacters) - -class TreeWalker(object): - def __init__(self, tree): - self.tree = tree - - def __iter__(self): - raise NotImplementedError - - def error(self, msg): - return {"type": "SerializeError", "data": msg} - - def normalizeAttrs(self, attrs): - newattrs = {} - if attrs: - #TODO: treewalkers should always have attrs - for (namespace,name),value in attrs.iteritems(): - namespace = unicode(namespace) if namespace else None - name = unicode(name) - value = unicode(value) - newattrs[(namespace,name)] = value - return newattrs - - def emptyTag(self, namespace, name, attrs, hasChildren=False): - yield {"type": "EmptyTag", "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - if hasChildren: - yield self.error(_("Void element has children")) - - def startTag(self, namespace, name, attrs): - return {"type": "StartTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": self.normalizeAttrs(attrs)} - - def endTag(self, namespace, name): - return {"type": "EndTag", - "name": unicode(name), - "namespace":unicode(namespace), - "data": {}} - - def text(self, data): - data = unicode(data) - middle = data.lstrip(spaceCharacters) - left = data[:len(data)-len(middle)] - if left: - yield {"type": "SpaceCharacters", "data": left} - data = middle - middle = data.rstrip(spaceCharacters) - right = data[len(middle):] - if middle: - yield {"type": "Characters", "data": middle} - if right: - yield {"type": "SpaceCharacters", "data": right} - - def comment(self, data): - return {"type": "Comment", "data": unicode(data)} - - def doctype(self, name, publicId=None, systemId=None, correct=True): - return {"type": "Doctype", - "name": name is not None and unicode(name) or u"", - "publicId": publicId, - "systemId": systemId, - "correct": correct} - - def entity(self, name): - return {"type": "Entity", "name": unicode(name)} - - def unknown(self, nodeType): - return self.error(_("Unknown node type: ") + nodeType) - -class RecursiveTreeWalker(TreeWalker): - def walkChildren(self, node): - raise NodeImplementedError - - def element(self, node, namespace, name, attrs, hasChildren): - if name in voidElements: - for token in self.emptyTag(namespace, name, attrs, hasChildren): - yield token - else: - yield self.startTag(name, attrs) - if hasChildren: - for token in self.walkChildren(node): - yield token - yield self.endTag(name) - from xml.dom import Node DOCUMENT = Node.DOCUMENT_NODE @@ -99,16 +14,127 @@ COMMENT = Node.COMMENT_NODE ENTITY = Node.ENTITY_NODE UNKNOWN = "<#UNKNOWN#>" +from ..constants import voidElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +def to_text(s, blank_if_none=True): + """Wrapper around six.text_type to convert None to empty string""" + if s is None: + if blank_if_none: + return "" + else: + return None + elif isinstance(s, text_type): + return s + else: + return text_type(s) + + +def is_text_or_none(string): + """Wrapper around isinstance(string_types) or is None""" + return string is None or isinstance(string, string_types) + + +class TreeWalker(object): + def __init__(self, tree): + self.tree = tree + + def __iter__(self): + raise NotImplementedError + + def error(self, msg): + return {"type": "SerializeError", "data": msg} + + def emptyTag(self, namespace, name, attrs, hasChildren=False): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + yield {"type": "EmptyTag", "name": to_text(name, False), + "namespace": to_text(namespace), + "data": attrs} + if hasChildren: + yield self.error(_("Void element has children")) + + def startTag(self, namespace, name, attrs): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(name) + assert all((namespace is None or isinstance(namespace, string_types)) and + isinstance(name, string_types) and + isinstance(value, string_types) + for (namespace, name), value in attrs.items()) + + return {"type": "StartTag", + "name": text_type(name), + "namespace": to_text(namespace), + "data": dict(((to_text(namespace, False), to_text(name)), + to_text(value, False)) + for (namespace, name), value in attrs.items())} + + def endTag(self, namespace, name): + assert namespace is None or isinstance(namespace, string_types), type(namespace) + assert isinstance(name, string_types), type(namespace) + + return {"type": "EndTag", + "name": to_text(name, False), + "namespace": to_text(namespace), + "data": {}} + + def text(self, data): + assert isinstance(data, string_types), type(data) + + data = to_text(data) + middle = data.lstrip(spaceCharacters) + left = data[:len(data) - len(middle)] + if left: + yield {"type": "SpaceCharacters", "data": left} + data = middle + middle = data.rstrip(spaceCharacters) + right = data[len(middle):] + if middle: + yield {"type": "Characters", "data": middle} + if right: + yield {"type": "SpaceCharacters", "data": right} + + def comment(self, data): + assert isinstance(data, string_types), type(data) + + return {"type": "Comment", "data": text_type(data)} + + def doctype(self, name, publicId=None, systemId=None, correct=True): + assert is_text_or_none(name), type(name) + assert is_text_or_none(publicId), type(publicId) + assert is_text_or_none(systemId), type(systemId) + + return {"type": "Doctype", + "name": to_text(name), + "publicId": to_text(publicId), + "systemId": to_text(systemId), + "correct": to_text(correct)} + + def entity(self, name): + assert isinstance(name, string_types), type(name) + + return {"type": "Entity", "name": text_type(name)} + + def unknown(self, nodeType): + return self.error(_("Unknown node type: ") + nodeType) + + class NonRecursiveTreeWalker(TreeWalker): def getNodeDetails(self, node): raise NotImplementedError - + def getFirstChild(self, node): raise NotImplementedError - + def getNextSibling(self, node): raise NotImplementedError - + def getParentNode(self, node): raise NotImplementedError @@ -118,7 +144,6 @@ class NonRecursiveTreeWalker(TreeWalker): details = self.getNodeDetails(currentNode) type, details = details[0], details[1:] hasChildren = False - endTag = None if type == DOCTYPE: yield self.doctype(*details) @@ -130,12 +155,11 @@ class NonRecursiveTreeWalker(TreeWalker): elif type == ELEMENT: namespace, name, attributes, hasChildren = details if name in voidElements: - for token in self.emptyTag(namespace, name, attributes, + for token in self.emptyTag(namespace, name, attributes, hasChildren): yield token hasChildren = False else: - endTag = name yield self.startTag(namespace, name, attributes) elif type == COMMENT: @@ -149,12 +173,12 @@ class NonRecursiveTreeWalker(TreeWalker): else: yield self.unknown(details[0]) - + if hasChildren: firstChild = self.getFirstChild(currentNode) else: firstChild = None - + if firstChild is not None: currentNode = firstChild else: diff --git a/libs/html5lib/treewalkers/dom.py b/libs/html5lib/treewalkers/dom.py index 383b46cb..a01287a9 100644 --- a/libs/html5lib/treewalkers/dom.py +++ b/libs/html5lib/treewalkers/dom.py @@ -1,10 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom import Node import gettext _ = gettext.gettext -import _base -from html5lib.constants import voidElements +from . import _base + class TreeWalker(_base.NonRecursiveTreeWalker): def getNodeDetails(self, node): @@ -16,10 +18,13 @@ class TreeWalker(_base.NonRecursiveTreeWalker): elif node.nodeType == Node.ELEMENT_NODE: attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value - return (_base.ELEMENT, node.namespaceURI, node.nodeName, + if attr.namespaceURI: + attrs[(attr.namespaceURI, attr.localName)] = attr.value + else: + attrs[(None, attr.name)] = attr.value + return (_base.ELEMENT, node.namespaceURI, node.nodeName, attrs, node.hasChildNodes()) elif node.nodeType == Node.COMMENT_NODE: diff --git a/libs/html5lib/treewalkers/etree.py b/libs/html5lib/treewalkers/etree.py index 13b03194..fd8a9cc9 100644 --- a/libs/html5lib/treewalkers/etree.py +++ b/libs/html5lib/treewalkers/etree.py @@ -1,33 +1,28 @@ +from __future__ import absolute_import, division, unicode_literals + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + OrderedDict = dict import gettext _ = gettext.gettext -try: - from types import ModuleType -except: - from new import module as ModuleType -import copy import re -import _base -from html5lib.constants import voidElements +from six import text_type + +from . import _base +from ..utils import moduleFactoryFactory tag_regexp = re.compile("{([^}]*)}(.*)") -moduleCache = {} - -def getETreeModule(ElementTreeImplementation): - name = "_" + ElementTreeImplementation.__name__+"builder" - if name in moduleCache: - return moduleCache[name] - else: - mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder") - objs = getETreeBuilder(ElementTreeImplementation) - mod.__dict__.update(objs) - moduleCache[name] = mod - return mod def getETreeBuilder(ElementTreeImplementation): ElementTree = ElementTreeImplementation + ElementTreeCommentType = ElementTree.Comment("asd").tag class TreeWalker(_base.NonRecursiveTreeWalker): """Given the particular ElementTree representation, this implementation, @@ -35,16 +30,16 @@ def getETreeBuilder(ElementTreeImplementation): content: 1. The current element - + 2. The index of the element relative to its parent - + 3. A stack of ancestor elements - + 4. A flag "text", "tail" or None to indicate if the current node is a text node; either the text or tail of the current element (1) """ def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Element + if isinstance(node, tuple): # It might be the root Element elt, key, parents, flag = node if flag in ("text", "tail"): return _base.TEXT, getattr(elt, flag) @@ -54,41 +49,41 @@ def getETreeBuilder(ElementTreeImplementation): if not(hasattr(node, "tag")): node = node.getroot() - if node.tag in ("", ""): + if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"): return (_base.DOCUMENT,) elif node.tag == "": - return (_base.DOCTYPE, node.text, + return (_base.DOCTYPE, node.text, node.get("publicId"), node.get("systemId")) - elif node.tag == ElementTree.Comment: + elif node.tag == ElementTreeCommentType: return _base.COMMENT, node.text else: - assert type(node.tag) in (str, unicode), type(node.tag) - #This is assumed to be an ordinary element + assert type(node.tag) == text_type, type(node.tag) + # This is assumed to be an ordinary element match = tag_regexp.match(node.tag) if match: namespace, tag = match.groups() else: namespace = None tag = node.tag - attrs = {} - for name, value in node.attrib.items(): + attrs = OrderedDict() + for name, value in list(node.attrib.items()): match = tag_regexp.match(name) if match: - attrs[(match.group(1),match.group(2))] = value + attrs[(match.group(1), match.group(2))] = value else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, tag, + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, tag, attrs, len(node) or node.text) - + def getFirstChild(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: element, key, parents, flag = node, None, [], None - + if flag in ("text", "tail"): return None else: @@ -99,13 +94,13 @@ def getETreeBuilder(ElementTreeImplementation): return element[0], 0, parents, None else: return None - + def getNextSibling(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if len(element): parents.append(element) @@ -116,16 +111,16 @@ def getETreeBuilder(ElementTreeImplementation): if element.tail and flag != "tail": return element, key, parents, "tail" elif key < len(parents[-1]) - 1: - return parents[-1][key+1], key+1, parents, None + return parents[-1][key + 1], key + 1, parents, None else: return None - + def getParentNode(self, node): if isinstance(node, tuple): element, key, parents, flag = node else: return None - + if flag == "text": if not parents: return element @@ -139,3 +134,5 @@ def getETreeBuilder(ElementTreeImplementation): return parent, list(parents[-1]).index(parent), parents, None return locals() + +getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/libs/html5lib/treewalkers/genshistream.py b/libs/html5lib/treewalkers/genshistream.py index ef71a83e..f559c45d 100644 --- a/libs/html5lib/treewalkers/genshistream.py +++ b/libs/html5lib/treewalkers/genshistream.py @@ -1,50 +1,49 @@ +from __future__ import absolute_import, division, unicode_literals + +from genshi.core import QName from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT -from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -from genshi.output import NamespaceFlattener +from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT -import _base +from . import _base + +from ..constants import voidElements, namespaces -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): - depth = 0 - ignore_until = None + # Buffer the events so we can pass in the following one previous = None for event in self.tree: if previous is not None: - if previous[0] == START: - depth += 1 - if ignore_until <= depth: - ignore_until = None - if ignore_until is None: - for token in self.tokens(previous, event): - yield token - if token["type"] == "EmptyTag": - ignore_until = depth - if previous[0] == END: - depth -= 1 - previous = event - if previous is not None: - if ignore_until is None or ignore_until <= depth: - for token in self.tokens(previous, None): + for token in self.tokens(previous, event): yield token - elif ignore_until is not None: - raise ValueError("Illformed DOM event stream: void element without END_ELEMENT") + previous = event + + # Don't forget the final event! + if previous is not None: + for token in self.tokens(previous, None): + yield token def tokens(self, event, next): kind, data, pos = event if kind == START: - tag, attrib = data + tag, attribs = data name = tag.localname namespace = tag.namespace - if tag in voidElements: - for token in self.emptyTag(namespace, name, list(attrib), - not next or next[0] != END + converted_attribs = {} + for k, v in attribs: + if isinstance(k, QName): + converted_attribs[(k.namespace, k.localname)] = v + else: + converted_attribs[(None, k)] = v + + if namespace == namespaces["html"] and name in voidElements: + for token in self.emptyTag(namespace, name, converted_attribs, + not next or next[0] != END or next[1] != tag): yield token else: - yield self.startTag(namespace, name, list(attrib)) + yield self.startTag(namespace, name, converted_attribs) elif kind == END: name = data.localname @@ -62,8 +61,8 @@ class TreeWalker(_base.TreeWalker): elif kind == DOCTYPE: yield self.doctype(*data) - elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, \ - START_CDATA, END_CDATA, PI): + elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, + START_CDATA, END_CDATA, PI): pass else: diff --git a/libs/html5lib/treewalkers/lxmletree.py b/libs/html5lib/treewalkers/lxmletree.py index 46f4908c..375cc2e8 100644 --- a/libs/html5lib/treewalkers/lxmletree.py +++ b/libs/html5lib/treewalkers/lxmletree.py @@ -1,186 +1,208 @@ -from lxml import etree -from html5lib.treebuilders.etree import tag_regexp - -from gettext import gettext -_ = gettext - -import _base - -from html5lib.constants import voidElements -from html5lib import ihatexml - -class Root(object): - def __init__(self, et): - self.elementtree = et - self.children = [] - if et.docinfo.internalDTD: - self.children.append(Doctype(self, et.docinfo.root_name, - et.docinfo.public_id, - et.docinfo.system_url)) - root = et.getroot() - node = root - - while node.getprevious() is not None: - node = node.getprevious() - while node is not None: - self.children.append(node) - node = node.getnext() - - self.text = None - self.tail = None - - def __getitem__(self, key): - return self.children[key] - - def getnext(self): - return None - - def __len__(self): - return 1 - -class Doctype(object): - def __init__(self, root_node, name, public_id, system_id): - self.root_node = root_node - self.name = name - self.public_id = public_id - self.system_id = system_id - - self.text = None - self.tail = None - - def getnext(self): - return self.root_node.children[1] - -class FragmentRoot(Root): - def __init__(self, children): - self.children = [FragmentWrapper(self, child) for child in children] - self.text = self.tail = None - - def getnext(self): - return None - -class FragmentWrapper(object): - def __init__(self, fragment_root, obj): - self.root_node = fragment_root - self.obj = obj - if hasattr(self.obj, 'text'): - self.text = self.obj.text - else: - self.text = None - if hasattr(self.obj, 'tail'): - self.tail = self.obj.tail - else: - self.tail = None - self.isstring = isinstance(obj, basestring) - - def __getattr__(self, name): - return getattr(self.obj, name) - - def getnext(self): - siblings = self.root_node.children - idx = siblings.index(self) - if idx < len(siblings) - 1: - return siblings[idx + 1] - else: - return None - - def __getitem__(self, key): - return self.obj[key] - - def __nonzero__(self): - return bool(self.obj) - - def getparent(self): - return None - - def __str__(self): - return str(self.obj) - - def __unicode__(self): - return unicode(self.obj) - - def __len__(self): - return len(self.obj) - - -class TreeWalker(_base.NonRecursiveTreeWalker): - def __init__(self, tree): - if hasattr(tree, "getroot"): - tree = Root(tree) - elif isinstance(tree, list): - tree = FragmentRoot(tree) - _base.NonRecursiveTreeWalker.__init__(self, tree) - self.filter = ihatexml.InfosetFilter() - def getNodeDetails(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - return _base.TEXT, getattr(node, key) - - elif isinstance(node, Root): - return (_base.DOCUMENT,) - - elif isinstance(node, Doctype): - return _base.DOCTYPE, node.name, node.public_id, node.system_id - - elif isinstance(node, FragmentWrapper) and node.isstring: - return _base.TEXT, node - - elif node.tag == etree.Comment: - return _base.COMMENT, node.text - - elif node.tag == etree.Entity: - return _base.ENTITY, node.text[1:-1] # strip &; - - else: - #This is assumed to be an ordinary element - match = tag_regexp.match(node.tag) - if match: - namespace, tag = match.groups() - else: - namespace = None - tag = node.tag - attrs = {} - for name, value in node.attrib.items(): - match = tag_regexp.match(name) - if match: - attrs[(match.group(1),match.group(2))] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), - attrs, len(node) > 0 or node.text) - - def getFirstChild(self, node): - assert not isinstance(node, tuple), _("Text nodes have no children") - - assert len(node) or node.text, "Node has no children" - if node.text: - return (node, "text") - else: - return node[0] - - def getNextSibling(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - # XXX: we cannot use a "bool(node) and node[0] or None" construct here - # because node[0] might evaluate to False if it has no child element - if len(node): - return node[0] - else: - return None - else: # tail - return node.getnext() - - return node.tail and (node, "tail") or node.getnext() - - def getParentNode(self, node): - if isinstance(node, tuple): # Text node - node, key = node - assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key - if key == "text": - return node - # else: fallback to "normal" processing - - return node.getparent() +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from lxml import etree +from ..treebuilders.etree import tag_regexp + +from gettext import gettext +_ = gettext + +from . import _base + +from .. import ihatexml + + +def ensure_str(s): + if s is None: + return None + elif isinstance(s, text_type): + return s + else: + return s.decode("utf-8", "strict") + + +class Root(object): + def __init__(self, et): + self.elementtree = et + self.children = [] + if et.docinfo.internalDTD: + self.children.append(Doctype(self, + ensure_str(et.docinfo.root_name), + ensure_str(et.docinfo.public_id), + ensure_str(et.docinfo.system_url))) + root = et.getroot() + node = root + + while node.getprevious() is not None: + node = node.getprevious() + while node is not None: + self.children.append(node) + node = node.getnext() + + self.text = None + self.tail = None + + def __getitem__(self, key): + return self.children[key] + + def getnext(self): + return None + + def __len__(self): + return 1 + + +class Doctype(object): + def __init__(self, root_node, name, public_id, system_id): + self.root_node = root_node + self.name = name + self.public_id = public_id + self.system_id = system_id + + self.text = None + self.tail = None + + def getnext(self): + return self.root_node.children[1] + + +class FragmentRoot(Root): + def __init__(self, children): + self.children = [FragmentWrapper(self, child) for child in children] + self.text = self.tail = None + + def getnext(self): + return None + + +class FragmentWrapper(object): + def __init__(self, fragment_root, obj): + self.root_node = fragment_root + self.obj = obj + if hasattr(self.obj, 'text'): + self.text = ensure_str(self.obj.text) + else: + self.text = None + if hasattr(self.obj, 'tail'): + self.tail = ensure_str(self.obj.tail) + else: + self.tail = None + self.isstring = isinstance(obj, str) or isinstance(obj, bytes) + # Support for bytes here is Py2 + if self.isstring: + self.obj = ensure_str(self.obj) + + def __getattr__(self, name): + return getattr(self.obj, name) + + def getnext(self): + siblings = self.root_node.children + idx = siblings.index(self) + if idx < len(siblings) - 1: + return siblings[idx + 1] + else: + return None + + def __getitem__(self, key): + return self.obj[key] + + def __bool__(self): + return bool(self.obj) + + def getparent(self): + return None + + def __str__(self): + return str(self.obj) + + def __unicode__(self): + return str(self.obj) + + def __len__(self): + return len(self.obj) + + +class TreeWalker(_base.NonRecursiveTreeWalker): + def __init__(self, tree): + if hasattr(tree, "getroot"): + tree = Root(tree) + elif isinstance(tree, list): + tree = FragmentRoot(tree) + _base.NonRecursiveTreeWalker.__init__(self, tree) + self.filter = ihatexml.InfosetFilter() + + def getNodeDetails(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + return _base.TEXT, ensure_str(getattr(node, key)) + + elif isinstance(node, Root): + return (_base.DOCUMENT,) + + elif isinstance(node, Doctype): + return _base.DOCTYPE, node.name, node.public_id, node.system_id + + elif isinstance(node, FragmentWrapper) and node.isstring: + return _base.TEXT, node.obj + + elif node.tag == etree.Comment: + return _base.COMMENT, ensure_str(node.text) + + elif node.tag == etree.Entity: + return _base.ENTITY, ensure_str(node.text)[1:-1] # strip &; + + else: + # This is assumed to be an ordinary element + match = tag_regexp.match(ensure_str(node.tag)) + if match: + namespace, tag = match.groups() + else: + namespace = None + tag = ensure_str(node.tag) + attrs = {} + for name, value in list(node.attrib.items()): + name = ensure_str(name) + value = ensure_str(value) + match = tag_regexp.match(name) + if match: + attrs[(match.group(1), match.group(2))] = value + else: + attrs[(None, name)] = value + return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag), + attrs, len(node) > 0 or node.text) + + def getFirstChild(self, node): + assert not isinstance(node, tuple), _("Text nodes have no children") + + assert len(node) or node.text, "Node has no children" + if node.text: + return (node, "text") + else: + return node[0] + + def getNextSibling(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + # XXX: we cannot use a "bool(node) and node[0] or None" construct here + # because node[0] might evaluate to False if it has no child element + if len(node): + return node[0] + else: + return None + else: # tail + return node.getnext() + + return (node, "tail") if node.tail else node.getnext() + + def getParentNode(self, node): + if isinstance(node, tuple): # Text node + node, key = node + assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key + if key == "text": + return node + # else: fallback to "normal" processing + + return node.getparent() diff --git a/libs/html5lib/treewalkers/pulldom.py b/libs/html5lib/treewalkers/pulldom.py index 1f8b95b8..0b0f515f 100644 --- a/libs/html5lib/treewalkers/pulldom.py +++ b/libs/html5lib/treewalkers/pulldom.py @@ -1,9 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + from xml.dom.pulldom import START_ELEMENT, END_ELEMENT, \ COMMENT, IGNORABLE_WHITESPACE, CHARACTERS -import _base +from . import _base + +from ..constants import voidElements -from html5lib.constants import voidElements class TreeWalker(_base.TreeWalker): def __iter__(self): @@ -11,7 +14,7 @@ class TreeWalker(_base.TreeWalker): previous = None for event in self.tree: if previous is not None and \ - (ignore_until is None or previous[1] is ignore_until): + (ignore_until is None or previous[1] is ignore_until): if previous[1] is ignore_until: ignore_until = None for token in self.tokens(previous, event): @@ -31,9 +34,9 @@ class TreeWalker(_base.TreeWalker): name = node.nodeName namespace = node.namespaceURI attrs = {} - for attr in node.attributes.keys(): + for attr in list(node.attributes.keys()): attr = node.getAttributeNode(attr) - attrs[(attr.namespaceURI,attr.localName)] = attr.value + attrs[(attr.namespaceURI, attr.localName)] = attr.value if name in voidElements: for token in self.emptyTag(namespace, name, diff --git a/libs/html5lib/treewalkers/simpletree.py b/libs/html5lib/treewalkers/simpletree.py deleted file mode 100644 index 9e6bd4c5..00000000 --- a/libs/html5lib/treewalkers/simpletree.py +++ /dev/null @@ -1,78 +0,0 @@ -import gettext -_ = gettext.gettext - -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - """Given that simpletree has no performant way of getting a node's - next sibling, this implementation returns "nodes" as tuples with the - following content: - - 1. The parent Node (Element, Document or DocumentFragment) - - 2. The child index of the current node in its parent's children list - - 3. A list used as a stack of all ancestors. It is a pair tuple whose - first item is a parent Node and second item is a child index. - """ - - def getNodeDetails(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - node = parent.childNodes[idx] - - # testing node.type allows us not to import treebuilders.simpletree - if node.type in (1, 2): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif node.type == 3: # DocumentType - return _base.DOCTYPE, node.name, node.publicId, node.systemId - - elif node.type == 4: # TextNode - return _base.TEXT, node.value - - elif node.type == 5: # Element - attrs = {} - for name, value in node.attributes.items(): - if isinstance(name, tuple): - attrs[(name[2],name[1])] = value - else: - attrs[(None,name)] = value - return (_base.ELEMENT, node.namespace, node.name, - attrs, node.hasContent()) - - elif node.type == 6: # CommentNode - return _base.COMMENT, node.data - - else: - return _node.UNKNOWN, node.type - - def getFirstChild(self, node): - if isinstance(node, tuple): # It might be the root Node - parent, idx, parents = node - parents.append((parent, idx)) - node = parent.childNodes[idx] - else: - parents = [] - - assert node.hasContent(), "Node has no children" - return (node, 0, parents) - - def getNextSibling(self, node): - assert isinstance(node, tuple), "Node is not a tuple: " + str(node) - parent, idx, parents = node - idx += 1 - if len(parent.childNodes) > idx: - return (parent, idx, parents) - else: - return None - - def getParentNode(self, node): - assert isinstance(node, tuple) - parent, idx, parents = node - if parents: - parent, idx = parents.pop() - return parent, idx, parents - else: - # HACK: We could return ``parent`` but None will stop the algorithm the same way - return None diff --git a/libs/html5lib/treewalkers/soup.py b/libs/html5lib/treewalkers/soup.py deleted file mode 100644 index fca65ecb..00000000 --- a/libs/html5lib/treewalkers/soup.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -import gettext -_ = gettext.gettext - -from BeautifulSoup import BeautifulSoup, Declaration, Comment, Tag -from html5lib.constants import namespaces -import _base - -class TreeWalker(_base.NonRecursiveTreeWalker): - doctype_regexp = re.compile( - r'DOCTYPE\s+(?P[^\s]*)(\s*PUBLIC\s*"(?P.*)"\s*"(?P.*)"|\s*SYSTEM\s*"(?P.*)")?') - def getNodeDetails(self, node): - if isinstance(node, BeautifulSoup): # Document or DocumentFragment - return (_base.DOCUMENT,) - - elif isinstance(node, Declaration): # DocumentType - string = unicode(node.string) - #Slice needed to remove markup added during unicode conversion, - #but only in some versions of BeautifulSoup/Python - if string.startswith(''): - string = string[2:-1] - m = self.doctype_regexp.match(string) - #This regexp approach seems wrong and fragile - #but beautiful soup stores the doctype as a single thing and we want the seperate bits - #It should work as long as the tree is created by html5lib itself but may be wrong if it's - #been modified at all - #We could just feed to it a html5lib tokenizer, I guess... - assert m is not None, "DOCTYPE did not match expected format" - - name = m.group('name') - publicId = m.group('publicId') - if publicId is not None: - systemId = m.group('systemId1') - else: - systemId = m.group('systemId2') - return _base.DOCTYPE, name, publicId or "", systemId or "" - - elif isinstance(node, Comment): - string = unicode(node.string) - if string.startswith(''): - string = string[4:-3] - return _base.COMMENT, string - - elif isinstance(node, unicode): # TextNode - return _base.TEXT, node - - elif isinstance(node, Tag): # Element - return (_base.ELEMENT, namespaces["html"], node.name, - dict(node.attrs).items(), node.contents) - else: - return _base.UNKNOWN, node.__class__.__name__ - - def getFirstChild(self, node): - return node.contents[0] - - def getNextSibling(self, node): - return node.nextSibling - - def getParentNode(self, node): - return node.parent diff --git a/libs/html5lib/trie/__init__.py b/libs/html5lib/trie/__init__.py new file mode 100644 index 00000000..a8cca8a9 --- /dev/null +++ b/libs/html5lib/trie/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + +from .py import Trie as PyTrie + +Trie = PyTrie + +try: + from .datrie import Trie as DATrie +except ImportError: + pass +else: + Trie = DATrie diff --git a/libs/html5lib/trie/_base.py b/libs/html5lib/trie/_base.py new file mode 100644 index 00000000..724486b1 --- /dev/null +++ b/libs/html5lib/trie/_base.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division, unicode_literals + +from collections import Mapping + + +class Trie(Mapping): + """Abstract base class for tries""" + + def keys(self, prefix=None): + keys = super().keys() + + if prefix is None: + return set(keys) + + # Python 2.6: no set comprehensions + return set([x for x in keys if x.startswith(prefix)]) + + def has_keys_with_prefix(self, prefix): + for key in self.keys(): + if key.startswith(prefix): + return True + + return False + + def longest_prefix(self, prefix): + if prefix in self: + return prefix + + for i in range(1, len(prefix) + 1): + if prefix[:-i] in self: + return prefix[:-i] + + raise KeyError(prefix) + + def longest_prefix_item(self, prefix): + lprefix = self.longest_prefix(prefix) + return (lprefix, self[lprefix]) diff --git a/libs/html5lib/trie/datrie.py b/libs/html5lib/trie/datrie.py new file mode 100644 index 00000000..51f3d046 --- /dev/null +++ b/libs/html5lib/trie/datrie.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, unicode_literals + +from datrie import Trie as DATrie +from six import text_type + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + chars = set() + for key in data.keys(): + if not isinstance(key, text_type): + raise TypeError("All keys must be strings") + for char in key: + chars.add(char) + + self._data = DATrie("".join(chars)) + for key, value in data.items(): + self._data[key] = value + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + raise NotImplementedError() + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + return self._data.keys(prefix) + + def has_keys_with_prefix(self, prefix): + return self._data.has_keys_with_prefix(prefix) + + def longest_prefix(self, prefix): + return self._data.longest_prefix(prefix) + + def longest_prefix_item(self, prefix): + return self._data.longest_prefix_item(prefix) diff --git a/libs/html5lib/trie/py.py b/libs/html5lib/trie/py.py new file mode 100644 index 00000000..c2ba3da7 --- /dev/null +++ b/libs/html5lib/trie/py.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division, unicode_literals +from six import text_type + +from bisect import bisect_left + +from ._base import Trie as ABCTrie + + +class Trie(ABCTrie): + def __init__(self, data): + if not all(isinstance(x, text_type) for x in data.keys()): + raise TypeError("All keys must be strings") + + self._data = data + self._keys = sorted(data.keys()) + self._cachestr = "" + self._cachepoints = (0, len(data)) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self, prefix=None): + if prefix is None or prefix == "" or not self._keys: + return set(self._keys) + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + start = i = bisect_left(self._keys, prefix, lo, hi) + else: + start = i = bisect_left(self._keys, prefix) + + keys = set() + if start == len(self._keys): + return keys + + while self._keys[i].startswith(prefix): + keys.add(self._keys[i]) + i += 1 + + self._cachestr = prefix + self._cachepoints = (start, i) + + return keys + + def has_keys_with_prefix(self, prefix): + if prefix in self._data: + return True + + if prefix.startswith(self._cachestr): + lo, hi = self._cachepoints + i = bisect_left(self._keys, prefix, lo, hi) + else: + i = bisect_left(self._keys, prefix) + + if i == len(self._keys): + return False + + return self._keys[i].startswith(prefix) diff --git a/libs/html5lib/utils.py b/libs/html5lib/utils.py index d53f6788..2f41f4df 100644 --- a/libs/html5lib/utils.py +++ b/libs/html5lib/utils.py @@ -1,9 +1,16 @@ +from __future__ import absolute_import, division, unicode_literals + +from types import ModuleType + try: - frozenset -except NameError: - #Import from the sets module for python 2.3 - from sets import Set as set - from sets import ImmutableSet as frozenset + import xml.etree.cElementTree as default_etree +except ImportError: + import xml.etree.ElementTree as default_etree + + +__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", + "surrogatePairToCodepoint", "moduleFactoryFactory"] + class MethodDispatcher(dict): """Dict with 2 special properties: @@ -23,7 +30,7 @@ class MethodDispatcher(dict): # twice as fast. Please do careful performance testing before changing # anything here. _dictEntries = [] - for name,value in items: + for name, value in items: if type(name) in (list, tuple, frozenset, set): for item in name: _dictEntries.append((item, value)) @@ -35,141 +42,41 @@ class MethodDispatcher(dict): def __getitem__(self, key): return dict.get(self, key, self.default) -#Pure python implementation of deque taken from the ASPN Python Cookbook -#Original code by Raymond Hettinger -class deque(object): +# Some utility functions to dal with weirdness around UCS2 vs UCS4 +# python builds - def __init__(self, iterable=(), maxsize=-1): - if not hasattr(self, 'data'): - self.left = self.right = 0 - self.data = {} - self.maxsize = maxsize - self.extend(iterable) - - def append(self, x): - self.data[self.right] = x - self.right += 1 - if self.maxsize != -1 and len(self) > self.maxsize: - self.popleft() - - def appendleft(self, x): - self.left -= 1 - self.data[self.left] = x - if self.maxsize != -1 and len(self) > self.maxsize: - self.pop() - - def pop(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - self.right -= 1 - elem = self.data[self.right] - del self.data[self.right] - return elem - - def popleft(self): - if self.left == self.right: - raise IndexError('cannot pop from empty deque') - elem = self.data[self.left] - del self.data[self.left] - self.left += 1 - return elem - - def clear(self): - self.data.clear() - self.left = self.right = 0 - - def extend(self, iterable): - for elem in iterable: - self.append(elem) - - def extendleft(self, iterable): - for elem in iterable: - self.appendleft(elem) - - def rotate(self, n=1): - if self: - n %= len(self) - for i in xrange(n): - self.appendleft(self.pop()) - - def __getitem__(self, i): - if i < 0: - i += len(self) - try: - return self.data[i + self.left] - except KeyError: - raise IndexError - - def __setitem__(self, i, value): - if i < 0: - i += len(self) - try: - self.data[i + self.left] = value - except KeyError: - raise IndexError - - def __delitem__(self, i): - size = len(self) - if not (-size <= i < size): - raise IndexError - data = self.data - if i < 0: - i += size - for j in xrange(self.left+i, self.right-1): - data[j] = data[j+1] - self.pop() - - def __len__(self): - return self.right - self.left - - def __cmp__(self, other): - if type(self) != type(other): - return cmp(type(self), type(other)) - return cmp(list(self), list(other)) - - def __repr__(self, _track=[]): - if id(self) in _track: - return '...' - _track.append(id(self)) - r = 'deque(%r)' % (list(self),) - _track.remove(id(self)) - return r - - def __getstate__(self): - return (tuple(self),) - - def __setstate__(self, s): - self.__init__(s[0]) - - def __hash__(self): - raise TypeError - - def __copy__(self): - return self.__class__(self) - - def __deepcopy__(self, memo={}): - from copy import deepcopy - result = self.__class__() - memo[id(self)] = result - result.__init__(deepcopy(tuple(self), memo)) - return result - -#Some utility functions to dal with weirdness around UCS2 vs UCS4 -#python builds - -def encodingType(): - if len() == 2: - return "UCS2" - else: - return "UCS4" - -def isSurrogatePair(data): +def isSurrogatePair(data): return (len(data) == 2 and ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF) + def surrogatePairToCodepoint(data): - char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + + char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 + (ord(data[1]) - 0xDC00)) return char_val + +# Module Factory Factory (no, this isn't Java, I know) +# Here to stop this being duplicated all over the place. + + +def moduleFactoryFactory(factory): + moduleCache = {} + + def moduleFactory(baseModule, *args, **kwargs): + if isinstance(ModuleType.__name__, type("")): + name = "_%s_factory" % baseModule.__name__ + else: + name = b"_%s_factory" % baseModule.__name__ + + if name in moduleCache: + return moduleCache[name] + else: + mod = ModuleType(name) + objs = factory(baseModule, *args, **kwargs) + mod.__dict__.update(objs) + moduleCache[name] = mod + return mod + + return moduleFactory diff --git a/libs/httplib2/__init__.py b/libs/httplib2/__init__.py index 01151f7f..9780d4e5 100644 --- a/libs/httplib2/__init__.py +++ b/libs/httplib2/__init__.py @@ -3,7 +3,7 @@ from __future__ import generators httplib2 A caching http interface that supports ETags and gzip -to conserve bandwidth. +to conserve bandwidth. Requires Python 2.3 or later @@ -15,17 +15,17 @@ Changelog: __author__ = "Joe Gregorio (joe@bitworking.org)" __copyright__ = "Copyright 2006, Joe Gregorio" __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", - "James Antill", - "Xavier Verges Farrero", - "Jonathan Feinberg", - "Blair Zajac", - "Sam Ruby", - "Louis Nyffenegger"] + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] __license__ = "MIT" -__version__ = "$Rev$" +__version__ = "0.8" -import re -import sys +import re +import sys import email import email.Utils import email.Message @@ -35,6 +35,7 @@ import gzip import zlib import httplib import urlparse +import urllib import base64 import os import copy @@ -42,10 +43,10 @@ import calendar import time import random import errno -# remove depracated warning in python2.6 try: from hashlib import sha1 as _sha, md5 as _md5 except ImportError: + # prior to Python 2.5, these were separate modules import sha import md5 _sha = sha.new @@ -54,21 +55,38 @@ import hmac from gettext import gettext as _ import socket -# Try using local version, followed by system, and none if neither are found try: - import lib.socks as socks + from httplib2 import socks except ImportError: try: - import socks as socks - except ImportError: + import socks + except (ImportError, AttributeError): socks = None # Build the appropriate socket wrapper for ssl try: import ssl # python 2.6 - _ssl_wrap_socket = ssl.wrap_socket -except ImportError: - def _ssl_wrap_socket(sock, key_file, cert_file): + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") ssl_sock = socket.ssl(sock, key_file, cert_file) return httplib.FakeSocket(sock, ssl_sock) @@ -84,15 +102,19 @@ def has_timeout(timeout): # python 2.6 return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) return (timeout is not None) -__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', - 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', - 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', - 'debuglevel'] +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] # The httplib debug level, set to a non-zero value to get debug output debuglevel = 0 +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 # Python 2.3 support if sys.version_info < (2,4): @@ -113,8 +135,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'): # All exceptions raised here derive from HttpLib2Error class HttpLib2Error(Exception): pass -# Some exceptions can be caught and optionally -# be turned back into responses. +# Some exceptions can be caught and optionally +# be turned back into responses. class HttpLib2ErrorWithResponse(HttpLib2Error): def __init__(self, desc, response, content): self.response = response @@ -127,8 +149,18 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class MalformedHeader(HttpLib2Error): pass class RelativeURIError(HttpLib2Error): pass class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert # Open Items: # ----------- @@ -152,6 +184,16 @@ class ServerNotFoundError(HttpLib2Error): pass # requesting that URI again. DEFAULT_MAX_REDIRECTS = 5 +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + # Which headers are hop-by-hop headers by default HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] @@ -176,7 +218,7 @@ def urlnorm(uri): raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) authority = authority.lower() scheme = scheme.lower() - if not path: + if not path: path = "/" # Could do syntax based normalization of the URI before # computing the digest. See Section 6.2.2 of Std 66. @@ -228,7 +270,7 @@ def _parse_cache_control(headers): parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] retval = dict(parts_with_args + parts_wo_args) - return retval + return retval # Whether to use a strict mode to parse WWW-Authenticate headers # Might lead to bad results in case of ill-formed header value, @@ -249,25 +291,30 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): per auth_scheme.""" retval = {} if headers.has_key(headername): - authenticate = headers[headername].strip() - www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED - while authenticate: - # Break off the scheme at the beginning of the line - if headername == 'authentication-info': - (auth_scheme, the_rest) = ('digest', authenticate) - else: - (auth_scheme, the_rest) = authenticate.split(" ", 1) - # Now loop over all the key value pairs that come after the scheme, - # being careful not to roll into the next scheme - match = www_auth.search(the_rest) - auth_params = {} - while match: - if match and len(match.groups()) == 3: - (key, value, the_rest) = match.groups() - auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + try: + + authenticate = headers[headername].strip() + www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED + while authenticate: + # Break off the scheme at the beginning of the line + if headername == 'authentication-info': + (auth_scheme, the_rest) = ('digest', authenticate) + else: + (auth_scheme, the_rest) = authenticate.split(" ", 1) + # Now loop over all the key value pairs that come after the scheme, + # being careful not to roll into the next scheme match = www_auth.search(the_rest) - retval[auth_scheme.lower()] = auth_params - authenticate = the_rest.strip() + auth_params = {} + while match: + if match and len(match.groups()) == 3: + (key, value, the_rest) = match.groups() + auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) + match = www_auth.search(the_rest) + retval[auth_scheme.lower()] = auth_params + authenticate = the_rest.strip() + + except ValueError: + raise MalformedHeader("WWW-Authenticate") return retval @@ -279,17 +326,17 @@ def _entry_disposition(response_headers, request_headers): 1. Cache-Control: max-stale 2. Age: headers are not used in the calculations. - Not that this algorithm is simpler than you might think + Not that this algorithm is simpler than you might think because we are operating as a private (non-shared) cache. This lets us ignore 's-maxage'. We can also ignore 'proxy-invalidate' since we aren't a proxy. - We will never return a stale document as - fresh as a design decision, and thus the non-implementation - of 'max-stale'. This also lets us safely ignore 'must-revalidate' + We will never return a stale document as + fresh as a design decision, and thus the non-implementation + of 'max-stale'. This also lets us safely ignore 'must-revalidate' since we operate as if every server has sent 'must-revalidate'. Since we are private we get to ignore both 'public' and 'private' parameters. We also ignore 'no-transform' since - we don't do any transformations. + we don't do any transformations. The 'no-store' parameter is handled at a higher level. So the only Cache-Control parameters we look at are: @@ -298,7 +345,7 @@ def _entry_disposition(response_headers, request_headers): max-age min-fresh """ - + retval = "STALE" cc = _parse_cache_control(request_headers) cc_response = _parse_cache_control(response_headers) @@ -340,10 +387,10 @@ def _entry_disposition(response_headers, request_headers): min_fresh = int(cc['min-fresh']) except ValueError: min_fresh = 0 - current_age += min_fresh + current_age += min_fresh if freshness_lifetime > current_age: retval = "FRESH" - return retval + return retval def _decompressContent(response, new_content): content = new_content @@ -391,7 +438,7 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey): if status == 304: status = 200 - status_header = 'status: %d\r\n' % response_headers.status + status_header = 'status: %d\r\n' % status header_str = info.as_string() @@ -408,10 +455,10 @@ def _wsse_username_token(cnonce, iso_now, password): return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() -# For credentials we need two things, first +# For credentials we need two things, first # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # Then we also need a list of URIs that have already demanded authentication -# That list is tricky since sub-URIs can take the same auth, or the +# That list is tricky since sub-URIs can take the same auth, or the # auth scheme may change as you descend the tree. # So we also need each Auth instance to be able to tell us # how close to the 'top' it is. @@ -435,7 +482,7 @@ class Authentication(object): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate - Authorization header. Over-rise this in sub-classes.""" + Authorization header. Over-ride this in sub-classes.""" pass def response(self, response, content): @@ -443,7 +490,7 @@ class Authentication(object): or such returned from the last authorized response. Over-rise this in sub-classes if necessary. - Return TRUE is the request is to be retried, for + Return TRUE is the request is to be retried, for example Digest may return stale=true. """ return False @@ -461,7 +508,7 @@ class BasicAuthentication(Authentication): class DigestAuthentication(Authentication): - """Only do qop='auth' and MD5, since that + """Only do qop='auth' and MD5, since that is all Apache currently implements""" def __init__(self, credentials, host, request_uri, headers, response, content, http): Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) @@ -474,7 +521,7 @@ class DigestAuthentication(Authentication): self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() if self.challenge['algorithm'] != 'MD5': raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) - self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) + self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.challenge['nc'] = 1 def request(self, method, request_uri, headers, content, cnonce = None): @@ -482,23 +529,24 @@ class DigestAuthentication(Authentication): H = lambda x: _md5(x).hexdigest() KD = lambda s, d: H("%s:%s" % (s, d)) A2 = "".join([method, ":", request_uri]) - self.challenge['cnonce'] = cnonce or _cnonce() - request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], - '%08x' % self.challenge['nc'], - self.challenge['cnonce'], - self.challenge['qop'], H(A2) - )) - headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( - self.credentials[0], + self.challenge['cnonce'] = cnonce or _cnonce() + request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % ( + self.challenge['nonce'], + '%08x' % self.challenge['nc'], + self.challenge['cnonce'], + self.challenge['qop'], H(A2))) + headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['nonce'], - request_uri, + request_uri, self.challenge['algorithm'], request_digest, self.challenge['qop'], self.challenge['nc'], - self.challenge['cnonce'], - ) + self.challenge['cnonce']) + if self.challenge.get('opaque'): + headers['authorization'] += ', opaque="%s"' % self.challenge['opaque'] self.challenge['nc'] += 1 def response(self, response, content): @@ -506,14 +554,14 @@ class DigestAuthentication(Authentication): challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return True else: updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) if updated_challenge.has_key('nextnonce'): self.challenge['nonce'] = updated_challenge['nextnonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return False @@ -547,9 +595,8 @@ class HmacDigestAuthentication(Authentication): else: self.pwhashmod = _sha self.key = "".join([self.credentials[0], ":", - self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), - ":", self.challenge['realm'] - ]) + self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), + ":", self.challenge['realm']]) self.key = self.pwhashmod.new(self.key).hexdigest().lower() def request(self, method, request_uri, headers, content): @@ -561,16 +608,15 @@ class HmacDigestAuthentication(Authentication): cnonce = _cnonce() request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() - headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( - self.credentials[0], + headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( + self.credentials[0], self.challenge['realm'], self.challenge['snonce'], cnonce, - request_uri, + request_uri, created, request_digest, - keylist, - ) + keylist) def response(self, response, content): challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {}) @@ -583,7 +629,7 @@ class WsseAuthentication(Authentication): """This is thinly tested and should not be relied upon. At this time there isn't any third party server to test against. Blogger and TypePad implemented this algorithm at one point - but Blogger has since switched to Basic over HTTPS and + but Blogger has since switched to Basic over HTTPS and TypePad has implemented it wrong, by never issuing a 401 challenge but instead requiring your client to telepathically know that their endpoint is expecting WSSE profile="UsernameToken".""" @@ -593,7 +639,7 @@ class WsseAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['Authorization'] = 'WSSE profile="UsernameToken"' + headers['authorization'] = 'WSSE profile="UsernameToken"' iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) cnonce = _cnonce() password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1]) @@ -629,7 +675,7 @@ class GoogleLoginAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth AUTH_SCHEME_CLASSES = { @@ -644,13 +690,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] class FileCache(object): """Uses a local directory as a store for cached files. - Not really safe to use if multiple threads or processes are going to + Not really safe to use if multiple threads or processes are going to be running on the same cache. """ def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior self.cache = cache self.safe = safe - if not os.path.exists(cache): + if not os.path.exists(cache): os.makedirs(self.cache) def get(self, key): @@ -660,7 +706,7 @@ class FileCache(object): f = file(cacheFullPath, "rb") retval = f.read() f.close() - except IOError, e: + except IOError: pass return retval @@ -688,34 +734,127 @@ class Credentials(object): def iter(self, domain): for (cdomain, name, password) in self.credentials: if cdomain == "" or domain == cdomain: - yield (name, password) + yield (name, password) class KeyCerts(Credentials): """Identical to Credentials except that name/password are mapped to key/cert.""" pass +class AllHosts(object): + pass class ProxyInfo(object): - """Collect information required to use a proxy.""" - def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None): - """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX - constants. For example: + """Collect information required to use a proxy.""" + bypass_hosts = () -p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000) - """ - self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: - def astuple(self): - return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, - self.proxy_user, self.proxy_pass) + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass - def isgood(self): - return socks and (self.proxy_host != None) and (self.proxy_port != None) + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) class HTTPConnectionWithTimeout(httplib.HTTPConnection): - """HTTPConnection subclass that supports timeouts""" + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): httplib.HTTPConnection.__init__(self, host, port, strict) @@ -725,27 +864,46 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): def connect(self): """Connect to the host and port specified in __init__.""" # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') msg = "getaddrinfo returns an empty list" - for res in socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM): + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: - if self.proxy_info and self.proxy_info.isgood(): + if use_proxy: self.sock = socks.socksocket(af, socktype, proto) - self.sock.setproxy(*self.proxy_info.astuple()) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) else: self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Different from httplib: support timeouts. if has_timeout(self.timeout): self.sock.settimeout(self.timeout) # End of difference from httplib. if self.debuglevel > 0: - print "connect: (%s, %s)" % (self.host, self.port) + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) - self.sock.connect(sa) + self.sock.connect((self.host, self.port) + sa[2:]) except socket.error, msg: if self.debuglevel > 0: - print 'connect fail:', (self.host, self.port) + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) if self.sock: self.sock.close() self.sock = None @@ -755,56 +913,265 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): raise socket.error, msg class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): - "This class allows communication via SSL." + """ + This class allows communication via SSL. + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=None, proxy_info=None): - httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file, - cert_file=cert_file, strict=strict) + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) self.timeout = timeout self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False def connect(self): "Connect to a host on a given (SSL) port." + msg = "getaddrinfo returns an empty list" if self.proxy_info and self.proxy_info.isgood(): - sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) - sock.setproxy(*self.proxy_info.astuple()) + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() else: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - if has_timeout(self.timeout): - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) - self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file) + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=5): + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass class Http(object): """An HTTP client that handles: -- all methods -- caching -- ETags -- compression, -- HTTPS -- Basic -- Digest -- WSSE -and more. + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. """ - def __init__(self, cache=None, timeout=None, proxy_info=None): - """The value of proxy_info is a ProxyInfo instance. + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. -If 'cache' is a string then it is used as a directory name -for a disk cache. Otherwise it must be an object that supports -the same interface as FileCache.""" + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + # Map domain name to an httplib connection self.connections = {} # The location of the cache, for now a directory # where cached responses are held. - if cache and isinstance(cache, str): + if cache and isinstance(cache, basestring): self.cache = FileCache(cache) else: self.cache = cache @@ -820,10 +1187,10 @@ the same interface as FileCache.""" # If set to False then no redirects are followed, even safe ones. self.follow_redirects = True - + # Which HTTP methods do we apply optimistic concurrency to, i.e. # which methods get an "if-match:" etag header added to them. - self.optimistic_concurrency_methods = ["PUT"] + self.optimistic_concurrency_methods = ["PUT", "PATCH"] # If 'follow_redirects' is True, and this is set to True then # all redirecs are followed, including unsafe ones. @@ -831,10 +1198,27 @@ the same interface as FileCache.""" self.ignore_etag = False - self.force_exception_to_status_code = False + self.force_exception_to_status_code = False self.timeout = timeout + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + def _auth_from_challenge(self, host, request_uri, headers, response, content): """A generator that creates Authorization objects that can be applied to requests. @@ -862,37 +1246,56 @@ the same interface as FileCache.""" self.authorizations = [] def _conn_request(self, conn, request_uri, method, body, headers): - for i in range(2): + for i in range(RETRIES): try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() conn.request(method, request_uri, body, headers) + except socket.timeout: + raise except socket.gaierror: conn.close() raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise except socket.error, e: - if not hasattr(e, 'errno'): # I don't know what this is so lets raise it if it happens + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused raise - elif e.errno == errno.ECONNREFUSED: # Connection refused - raise - # Just because the server closed the connection doesn't apparently mean - # that the server didn't send a response. - pass except httplib.HTTPException: # Just because the server closed the connection doesn't apparently mean # that the server didn't send a response. - pass + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue try: response = conn.getresponse() except (socket.error, httplib.HTTPException): - if i == 0: + if i < RETRIES-1: conn.close() conn.connect() continue else: + conn.close() raise else: content = "" if method == "HEAD": - response.close() + conn.close() else: content = response.read() response = Response(response) @@ -908,12 +1311,12 @@ the same interface as FileCache.""" auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auth = auths and sorted(auths)[0][1] or None - if auth: + if auth: auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers) - if auth: + if auth: if auth.response(response, body): auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers ) @@ -921,7 +1324,7 @@ the same interface as FileCache.""" if response.status == 401: for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): - authorization.request(method, request_uri, headers, body) + authorization.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) if response.status != 401: self.authorizations.append(authorization) @@ -944,26 +1347,31 @@ the same interface as FileCache.""" if response.status == 301 and method in ["GET", "HEAD"]: response['-x-permanent-redirect-url'] = response['location'] if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) if headers.has_key('if-none-match'): del headers['if-none-match'] if headers.has_key('if-modified-since'): del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] if response.has_key('location'): location = response['location'] old_response = copy.deepcopy(response) if not old_response.has_key('content-location'): - old_response['content-location'] = absolute_uri - redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) response.previous = old_response else: - raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) - elif response.status in [200, 203] and method == "GET": + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: # Don't cache 206's since we aren't going to handle byte range requests if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) return (response, content) @@ -978,24 +1386,25 @@ the same interface as FileCache.""" def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): """ Performs a single HTTP request. -The 'uri' is the URI of the HTTP resource and can begin -with either 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. -There is no restriction on the methods allowed. + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'body' is the entity body to be sent with the request. It is a string -object. + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. -Any extra headers that are to be sent with the request should be provided in the -'headers' dictionary. + The 'body' is the entity body to be sent with the request. It is a + string object. -The maximum number of redirect to follow before raising an -exception is 'redirections. The default is 5. + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. -The return value is a tuple of (response, content), the first -being and instance of the 'Response' class, the second being -a string that contains the response entity body. + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. """ try: if headers is None: @@ -1004,7 +1413,7 @@ a string that contains the response entity body. headers = self._normalize_headers(headers) if not headers.has_key('user-agent'): - headers['user-agent'] = "Python-httplib2/%s" % __version__ + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ uri = iri2uri(uri) @@ -1014,21 +1423,38 @@ a string that contains the response entity body. scheme = 'https' authority = domain_port[0] + proxy_info = self._get_proxy_info(scheme, authority) + conn_key = scheme+":"+authority if conn_key in self.connections: conn = self.connections[conn_key] else: if not connection_type: - connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout + connection_type = SCHEME_TO_CONNECTION[scheme] certs = list(self.certificates.iter(authority)) - if scheme == 'https' and certs: - conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], - cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) else: - conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info) + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) conn.set_debuglevel(debuglevel) - if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers: + if 'range' not in headers and 'accept-encoding' not in headers: headers['accept-encoding'] = 'gzip, deflate' info = email.Message.Message() @@ -1048,7 +1474,7 @@ a string that contains the response entity body. feedparser.feed(info) info = feedparser.close() feedparser._parse = None - except IndexError, ValueError: + except (IndexError, ValueError): self.cache.delete(cachekey) cachekey = None cached_value = None @@ -1071,13 +1497,15 @@ a string that contains the response entity body. for header in vary_headers: key = '-varied-%s' % header value = info[key] - if headers.get(header, '') != value: - cached_value = None - break + if headers.get(header, None) != value: + cached_value = None + break if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: if info.has_key('-x-permanent-redirect-url'): # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) response.previous = Response(info) response.previous.fromcache = True @@ -1085,13 +1513,13 @@ a string that contains the response entity body. # Determine our course of action: # Is the cached entry fresh or stale? # Has the client requested a non-cached response? - # - # There seems to be three possible answers: + # + # There seems to be three possible answers: # 1. [FRESH] Return the cache entry w/o doing a GET # 2. [STALE] Do the GET (but add in cache validators if available) # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request - entry_disposition = _entry_disposition(info, headers) - + entry_disposition = _entry_disposition(info, headers) + if entry_disposition == "FRESH": if not cached_value: info['status'] = '504' @@ -1113,7 +1541,7 @@ a string that contains the response entity body. if response.status == 304 and method == "GET": # Rewrite the cache entry with the new end-to-end headers - # Take all headers that are in response + # Take all headers that are in response # and overwrite their values in info. # unless they are hop-by-hop, or are listed in the connection header. @@ -1125,14 +1553,14 @@ a string that contains the response entity body. _updateCache(headers, merged_response, content, self.cache, cachekey) response = merged_response response.status = 200 - response.fromcache = True + response.fromcache = True elif response.status == 200: content = new_content else: self.cache.delete(cachekey) - content = new_content - else: + content = new_content + else: cc = _parse_cache_control(headers) if cc.has_key('only-if-cached'): info['status'] = '504' @@ -1146,34 +1574,47 @@ a string that contains the response entity body. response = e.response content = e.content response.status = 500 - response.reason = str(e) - elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): + response.reason = str(e) + elif isinstance(e, socket.timeout): content = "Request Timeout" - response = Response( { - "content-type": "text/plain", - "status": "408", - "content-length": len(content) - }) + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) response.reason = "Request Timeout" else: - content = str(e) - response = Response( { - "content-type": "text/plain", - "status": "400", - "content-length": len(content) - }) - response.reason = "Bad Request" + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" else: raise - + return (response, content) - + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + class Response(dict): """An object more like email.Message than httplib.HTTPResponse.""" - + """Is this response from our local cache""" fromcache = False @@ -1189,27 +1630,28 @@ class Response(dict): previous = None def __init__(self, info): - # info is either an email.Message or + # info is either an email.Message or # an httplib.HTTPResponse object. if isinstance(info, httplib.HTTPResponse): - for key, value in info.getheaders(): - self[key.lower()] = value + for key, value in info.getheaders(): + self[key.lower()] = value self.status = info.status self['status'] = str(self.status) self.reason = info.reason self.version = info.version elif isinstance(info, email.Message.Message): - for key, value in info.items(): - self[key] = value + for key, value in info.items(): + self[key.lower()] = value self.status = int(self['status']) else: - for key, value in info.iteritems(): - self[key] = value + for key, value in info.iteritems(): + self[key.lower()] = value self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) def __getattr__(self, name): if name == 'dict': - return self - else: - raise AttributeError, name + return self + else: + raise AttributeError, name diff --git a/libs/httplib2/cacerts.txt b/libs/httplib2/cacerts.txt new file mode 100644 index 00000000..d8a0027c --- /dev/null +++ b/libs/httplib2/cacerts.txt @@ -0,0 +1,739 @@ +# Certifcate Authority certificates for validating SSL connections. +# +# This file contains PEM format certificates generated from +# http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Netscape security libraries. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1994-2000 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +Verisign/RSA Secure Server CA +============================= + +-----BEGIN CERTIFICATE----- +MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD +VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0 +MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV +BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy +dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ +ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII +0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI +uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI +hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3 +YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc +1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA== +-----END CERTIFICATE----- + +Thawte Personal Basic CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDITCCAoqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFBlcnNvbmFsIEJhc2lj +IENBMSgwJgYJKoZIhvcNAQkBFhlwZXJzb25hbC1iYXNpY0B0aGF3dGUuY29tMB4X +DTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgcsxCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQZXJzb25hbCBCYXNpYyBD +QTEoMCYGCSqGSIb3DQEJARYZcGVyc29uYWwtYmFzaWNAdGhhd3RlLmNvbTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53 +dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK +wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7 +G1sY0b8jkyECAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQF +AAOBgQAt4plrsD16iddZopQBHyvdEktTwq1/qqcAXJFAVyVKOKqEcLnZgA+le1z7 +c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P +9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ== +-----END CERTIFICATE----- + +Thawte Personal Premium CA +========================== + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBzzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEjMCEGA1UEAxMaVGhhd3RlIFBlcnNvbmFsIFByZW1p +dW0gQ0ExKjAoBgkqhkiG9w0BCQEWG3BlcnNvbmFsLXByZW1pdW1AdGhhd3RlLmNv +bTAeFw05NjAxMDEwMDAwMDBaFw0yMDEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlDYXBlIFRvd24xGjAY +BgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9u +IFNlcnZpY2VzIERpdmlzaW9uMSMwIQYDVQQDExpUaGF3dGUgUGVyc29uYWwgUHJl +bWl1bSBDQTEqMCgGCSqGSIb3DQEJARYbcGVyc29uYWwtcHJlbWl1bUB0aGF3dGUu +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJZtn4B0TPuYwu8KHvE0Vs +Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI +Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD +ZicRFTuqW/KY3TZCstqIdQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH +b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh +KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGJ +-----END CERTIFICATE----- + +Thawte Personal Freemail CA +=========================== + +-----BEGIN CERTIFICATE----- +MIIDLTCCApagAwIBAgIBADANBgkqhkiG9w0BAQQFADCB0TELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEkMCIGA1UEAxMbVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIENBMSswKQYJKoZIhvcNAQkBFhxwZXJzb25hbC1mcmVlbWFpbEB0aGF3dGUu +Y29tMB4XDTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgdExCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEa +MBgGA1UEChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBG +cmVlbWFpbCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhh +d3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1GnX1LCUZFtx6UfY +DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E +rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq +uzgkCGqYx7Hao5iR/Xnb5VrEHLkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zAN +BgkqhkiG9w0BAQQFAAOBgQDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP +MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa +/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei +gQ== +-----END CERTIFICATE----- + +Thawte Server CA +================ + +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +Thawte Premium Server CA +======================== + +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +Equifax Secure CA +================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05 +NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD +VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp +bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N +H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR +4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN +BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo +EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5 +FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx +lA== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh +YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7 +FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg +J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc +r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G2 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM +HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK +qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj +cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y +cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP +T8qAkbYp +-----END CERTIFICATE----- + +Verisign Class 1 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- + +Verisign Class 2 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Equifax Secure Global eBusiness CA +================================== + +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 1 +============================= + +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 2 +============================= + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +Thawte Time Stamping CA +======================= + +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBizELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzAN +BgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAd +BgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNOTcwMTAxMDAwMDAwWhcN +MjAxMjMxMjM1OTU5WjCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4g +Q2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsG +A1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1l +c3RhbXBpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYrWHhhRYZT +6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa +Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL +8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEEBQADgYEAZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC +9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ +pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnRUEzGLhQOEkbdYATAUOK8oyvyxUBkZ +CayJSdM= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== + +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ + +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +Entrust.net Secure Server Certification Authority +================================================= + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +Go Daddy Certification Authority Root Certificate Bundle +======================================================== + +-----BEGIN CERTIFICATE----- +MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx +ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw +MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH +QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j +b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j +b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H +KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm +VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR +SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT +cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ +6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu +MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS +kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB +BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f +BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv +c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH +AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO +BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG +OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU +A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o +0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX +RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH +qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV +U+4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh +bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu +Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g +QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe +BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX +DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE +YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC +ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q +N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO +r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN +f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH +U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU +TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb +VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg +SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv +biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg +MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw +AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv +ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu +Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd +IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv +bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1 +QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O +WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf +SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +GeoTrust Global CA +================== + +-----BEGIN CERTIFICATE----- +MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 +aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw +WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE +AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m +OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu +T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c +JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR +Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz +PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm +aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM +TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g +LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO +BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv +dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB +AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL +NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W +b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S +-----END CERTIFICATE----- + diff --git a/libs/httplib2/iri2uri.py b/libs/httplib2/iri2uri.py index 70667edf..d88c91fd 100644 --- a/libs/httplib2/iri2uri.py +++ b/libs/httplib2/iri2uri.py @@ -16,7 +16,7 @@ import urlparse # Convert an IRI to a URI following the rules in RFC 3987 -# +# # The characters we need to enocde and escape are defined in the spec: # # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD @@ -28,28 +28,28 @@ import urlparse # / %xD0000-DFFFD / %xE1000-EFFFD escape_range = [ - (0xA0, 0xD7FF ), - (0xE000, 0xF8FF ), - (0xF900, 0xFDCF ), - (0xFDF0, 0xFFEF), - (0x10000, 0x1FFFD ), - (0x20000, 0x2FFFD ), - (0x30000, 0x3FFFD), - (0x40000, 0x4FFFD ), - (0x50000, 0x5FFFD ), - (0x60000, 0x6FFFD), - (0x70000, 0x7FFFD ), - (0x80000, 0x8FFFD ), - (0x90000, 0x9FFFD), - (0xA0000, 0xAFFFD ), - (0xB0000, 0xBFFFD ), - (0xC0000, 0xCFFFD), - (0xD0000, 0xDFFFD ), - (0xE1000, 0xEFFFD), - (0xF0000, 0xFFFFD ), - (0x100000, 0x10FFFD) + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), ] - + def encode(c): retval = c i = ord(c) @@ -63,19 +63,19 @@ def encode(c): def iri2uri(uri): - """Convert an IRI to a URI. Note that IRIs must be + """Convert an IRI to a URI. Note that IRIs must be passed in a unicode strings. That is, do not utf-8 encode - the IRI before passing it into the function.""" + the IRI before passing it into the function.""" if isinstance(uri ,unicode): (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) authority = authority.encode('idna') # For each character in 'ucschar' or 'iprivate' # 1. encode as utf-8 - # 2. then %-encode each octet of that utf-8 + # 2. then %-encode each octet of that utf-8 uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = "".join([encode(c) for c in uri]) return uri - + if __name__ == "__main__": import unittest @@ -83,7 +83,7 @@ if __name__ == "__main__": def test_uris(self): """Test that URIs are invariant under the transformation.""" - invariant = [ + invariant = [ u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"http://www.ietf.org/rfc/rfc2396.txt", u"ldap://[2001:db8::7]/c=GB?objectClass?one", @@ -94,7 +94,7 @@ if __name__ == "__main__": u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] for uri in invariant: self.assertEqual(uri, iri2uri(uri)) - + def test_iri(self): """ Test that the right type of escaping is done for each part of the URI.""" self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) @@ -107,4 +107,4 @@ if __name__ == "__main__": unittest.main() - + diff --git a/libs/httplib2/socks.py b/libs/httplib2/socks.py new file mode 100644 index 00000000..0991f4cf --- /dev/null +++ b/libs/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/libs/pyasn1/__init__.py b/libs/pyasn1/__init__.py index 7de39fe5..88aff79c 100644 --- a/libs/pyasn1/__init__.py +++ b/libs/pyasn1/__init__.py @@ -1 +1,8 @@ -majorVersionId = '1' +import sys + +# http://www.python.org/dev/peps/pep-0396/ +__version__ = '0.1.7' + +if sys.version_info[:2] < (2, 4): + raise RuntimeError('PyASN1 requires Python 2.4 or later') + diff --git a/libs/pyasn1/codec/__init__.py b/libs/pyasn1/codec/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/__init__.py +++ b/libs/pyasn1/codec/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/__init__.py b/libs/pyasn1/codec/ber/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/ber/__init__.py +++ b/libs/pyasn1/codec/ber/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/ber/decoder.py b/libs/pyasn1/codec/ber/decoder.py index ae9311cb..be0cf490 100644 --- a/libs/pyasn1/codec/ber/decoder.py +++ b/libs/pyasn1/codec/ber/decoder.py @@ -1,21 +1,24 @@ # BER decoder from pyasn1.type import tag, base, univ, char, useful, tagmap from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import oct2int, octs2ints -from pyasn1 import error +from pyasn1.compat.octets import oct2int, octs2ints, isOctetsType +from pyasn1 import debug, error class AbstractDecoder: protoComponent = None def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Decoder not implemented for %s' % (tagSet,)) def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % tagSet) + length, state, decodeFun, substrateFun): + raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % (tagSet,)) class AbstractSimpleDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatSimple,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(value, tagSet) elif value is None: @@ -24,7 +27,10 @@ class AbstractSimpleDecoder(AbstractDecoder): return asn1Spec.clone(value) class AbstractConstructedDecoder(AbstractDecoder): + tagFormats = (tag.tagFormatConstructed,) def _createComponent(self, asn1Spec, tagSet, value=None): + if tagSet[0][1] not in self.tagFormats: + raise error.PyAsn1Error('Invalid tag format %r for %r' % (tagSet[0], self.protoComponent,)) if asn1Spec is None: return self.protoComponent.clone(tagSet) else: @@ -32,19 +38,34 @@ class AbstractConstructedDecoder(AbstractDecoder): class EndOfOctetsDecoder(AbstractSimpleDecoder): def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return eoo.endOfOctets, substrate[:length] + length, state, decodeFun, substrateFun): + return eoo.endOfOctets, substrate[length:] class ExplicitTagDecoder(AbstractSimpleDecoder): + protoComponent = univ.Any('') + tagFormats = (tag.tagFormatConstructed,) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - return decodeFun(substrate[:length], asn1Spec, tagSet, length) + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) + head, tail = substrate[:length], substrate[length:] + value, _ = decodeFun(head, asn1Spec, tagSet, length) + return value, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): + if substrateFun: + return substrateFun( + self._createComponent(asn1Spec, tagSet, ''), + substrate, length + ) value, substrate = decodeFun(substrate, asn1Spec, tagSet, length) terminator, substrate = decodeFun(substrate) - if terminator == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(terminator) and \ + terminator == eoo.endOfOctets: return value, substrate else: raise error.PyAsn1Error('Missing end-of-octets terminator') @@ -71,79 +92,71 @@ class IntegerDecoder(AbstractSimpleDecoder): '\xfb': -5 } - def _valueFilter(self, value): - try: - return int(value) - except OverflowError: - return value - def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: - raise error.PyAsn1Error('Empty substrate') - if substrate in self.precomputedValues: - value = self.precomputedValues[substrate] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0), tail + if head in self.precomputedValues: + value = self.precomputedValues[head] else: - firstOctet = oct2int(substrate[0]) + firstOctet = oct2int(head[0]) if firstOctet & 0x80: value = -1 else: value = 0 - for octet in substrate: + for octet in head: value = value << 8 | oct2int(octet) - value = self._valueFilter(value) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class BooleanDecoder(IntegerDecoder): protoComponent = univ.Boolean(0) - def _valueFilter(self, value): - if value: - return 1 - else: - return 0 + def _createComponent(self, asn1Spec, tagSet, value=None): + return IntegerDecoder._createComponent(self, asn1Spec, tagSet, value and 1 or 0) class BitStringDecoder(AbstractSimpleDecoder): protoComponent = univ.BitString(()) + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - if not substrate: - raise error.PyAsn1Error('Missing initial octet') - trailingBits = oct2int(substrate[0]) + if not head: + raise error.PyAsn1Error('Empty substrate') + trailingBits = oct2int(head[0]) if trailingBits > 7: raise error.PyAsn1Error( 'Trailing bits overflow %s' % trailingBits ) - substrate = substrate[1:] - lsb = p = 0; l = len(substrate)-1; b = () + head = head[1:] + lsb = p = 0; l = len(head)-1; b = () while p <= l: if p == l: lsb = trailingBits j = 7 - o = oct2int(substrate[p]) + o = oct2int(head[p]) while j >= lsb: b = b + ((o>>j)&0x01,) j = j - 1 p = p + 1 - return self._createComponent(asn1Spec, tagSet, b), '' + return self._createComponent(asn1Spec, tagSet, b), tail r = self._createComponent(asn1Spec, tagSet, ()) - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -154,27 +167,29 @@ class BitStringDecoder(AbstractSimpleDecoder): class OctetStringDecoder(AbstractSimpleDecoder): protoComponent = univ.OctetString('') + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check? - return self._createComponent(asn1Spec, tagSet, substrate), '' + return self._createComponent(asn1Spec, tagSet, head), tail r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun(substrate) + if substrateFun: + return substrateFun(r, substrate, length) + while head: + component, head = decodeFun(head) r = r + component - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet, '') - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -186,93 +201,89 @@ class OctetStringDecoder(AbstractSimpleDecoder): class NullDecoder(AbstractSimpleDecoder): protoComponent = univ.Null('') def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if substrate: - raise error.PyAsn1Error('Unexpected substrate for Null') - return r, substrate + if head: + raise error.PyAsn1Error('Unexpected %d-octet substrate for Null' % length) + return r, tail class ObjectIdentifierDecoder(AbstractSimpleDecoder): protoComponent = univ.ObjectIdentifier(()) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - oid = (); index = 0 - # Get the first subid - subId = oct2int(substrate[index]) - oid = oid + divmod(subId, 40) - index = index + 1 - substrateLen = len(substrate) - + # Get the first subid + subId = oct2int(head[0]) + oid = divmod(subId, 40) + + index = 1 + substrateLen = len(head) while index < substrateLen: - subId = oct2int(substrate[index]) - if subId < 128: - oid = oid + (subId,) - index = index + 1 - else: + subId = oct2int(head[index]) + index = index + 1 + if subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in sub-ID OID + # encoding, tolerating it opens a vulnerability. + # See http://www.cosic.esat.kuleuven.be/publications/article-1432.pdf page 7 + raise error.PyAsn1Error('Invalid leading 0x80 in sub-OID') + elif subId > 128: # Construct subid from a number of octets nextSubId = subId subId = 0 - while nextSubId >= 128 and index < substrateLen: + while nextSubId >= 128: subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (oid,) + ) + nextSubId = oct2int(head[index]) index = index + 1 - nextSubId = oct2int(substrate[index]) - if index == substrateLen: - raise error.SubstrateUnderrunError( - 'Short substrate for OID %s' % oid - ) subId = (subId << 7) + nextSubId - oid = oid + (subId,) - index = index + 1 - return self._createComponent(asn1Spec, tagSet, oid), substrate[index:] + oid = oid + (subId,) + return self._createComponent(asn1Spec, tagSet, oid), tail class RealDecoder(AbstractSimpleDecoder): protoComponent = univ.Real() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] - if not length: - raise error.SubstrateUnderrunError('Short substrate for Real') - fo = oct2int(substrate[0]); substrate = substrate[1:] - if fo & 0x40: # infinite value - value = fo & 0x01 and '-inf' or 'inf' - elif fo & 0x80: # binary enoding - if fo & 0x11 == 0: - n = 1 - elif fo & 0x01: - n = 2 - elif fo & 0x02: - n = 3 - else: - n = oct2int(substrate[0]) - eo, substrate = substrate[:n], substrate[n:] - if not eo or not substrate: + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: + return self._createComponent(asn1Spec, tagSet, 0.0), tail + fo = oct2int(head[0]); head = head[1:] + if fo & 0x80: # binary enoding + n = (fo & 0x03) + 1 + if n == 4: + n = oct2int(head[0]) + eo, head = head[:n], head[n:] + if not eo or not head: raise error.PyAsn1Error('Real exponent screwed') - e = 0 + e = oct2int(eo[0]) & 0x80 and -1 or 0 while eo: # exponent e <<= 8 e |= oct2int(eo[0]) eo = eo[1:] p = 0 - while substrate: # value + while head: # value p <<= 8 - p |= oct2int(substrate[0]) - substrate = substrate[1:] + p |= oct2int(head[0]) + head = head[1:] if fo & 0x40: # sign bit p = -p value = (p, 2, e) + elif fo & 0x40: # infinite value + value = fo & 0x01 and '-inf' or 'inf' elif fo & 0xc0 == 0: # character encoding try: if fo & 0x3 == 0x1: # NR1 - value = (int(substrate), 10, 0) + value = (int(head), 10, 0) elif fo & 0x3 == 0x2: # NR2 - value = float(substrate) + value = float(head) elif fo & 0x3 == 0x3: # NR3 - value = float(substrate) + value = float(head) else: raise error.SubstrateUnderrunError( 'Unknown NR (tag %s)' % fo @@ -281,13 +292,11 @@ class RealDecoder(AbstractSimpleDecoder): raise error.SubstrateUnderrunError( 'Bad character Real syntax' ) - elif fo & 0xc0 == 0x40: # special real value - pass else: raise error.SubstrateUnderrunError( 'Unknown encoding (tag %s)' % fo ) - return self._createComponent(asn1Spec, tagSet, value), substrate + return self._createComponent(asn1Spec, tagSet, value), tail class SequenceDecoder(AbstractConstructedDecoder): protoComponent = univ.Sequence() @@ -301,17 +310,15 @@ class SequenceDecoder(AbstractConstructedDecoder): return r.getComponentPositionNearType(t, idx) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) idx = 0 - if not decodeFun: - return r, substrate - while substrate: + if substrateFun: + return substrateFun(r, substrate, length) + while head: asn1Spec = self._getComponentTagMap(r, idx) - component, substrate = decodeFun( - substrate, asn1Spec - ) + component, head = decodeFun(head, asn1Spec) idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx ) @@ -319,18 +326,19 @@ class SequenceDecoder(AbstractConstructedDecoder): idx = idx + 1 r.setDefaultComponents() r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) idx = 0 while substrate: asn1Spec = self._getComponentTagMap(r, idx) - if not decodeFun: - return r, substrate component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break idx = self._getComponentPositionByType( r, component.getEffectiveTagSet(), idx @@ -348,32 +356,31 @@ class SequenceDecoder(AbstractConstructedDecoder): class SequenceOfDecoder(AbstractConstructedDecoder): protoComponent = univ.SequenceOf() def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate - while substrate: - component, substrate = decodeFun( - substrate, asn1Spec - ) + while head: + component, head = decodeFun(head, asn1Spec) r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 r.verifySizeSpec() - return r, substrate + return r, tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) asn1Spec = r.getComponentType() idx = 0 - if not decodeFun: - return r, substrate while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r.setComponentByPosition(idx, component, asn1Spec is None) idx = idx + 1 @@ -401,43 +408,68 @@ class SetOfDecoder(SequenceOfDecoder): class ChoiceDecoder(AbstractConstructedDecoder): protoComponent = univ.Choice() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): - substrate = substrate[:length] + length, state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] r = self._createComponent(asn1Spec, tagSet) - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) if r.getTagSet() == tagSet: # explicitly tagged Choice - component, substrate = decodeFun( - substrate, r.getComponentTagMap() + component, head = decodeFun( + head, r.getComponentTagMap() ) else: - component, substrate = decodeFun( - substrate, r.getComponentTagMap(), tagSet, length, state + component, head = decodeFun( + head, r.getComponentTagMap(), tagSet, length, state ) if isinstance(component, univ.Choice): effectiveTagSet = component.getEffectiveTagSet() else: effectiveTagSet = component.getTagSet() r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) + return r, tail + + def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, + length, state, decodeFun, substrateFun): + r = self._createComponent(asn1Spec, tagSet) + if substrateFun: + return substrateFun(r, substrate, length) + if r.getTagSet() == tagSet: # explicitly tagged Choice + component, substrate = decodeFun(substrate, r.getComponentTagMap()) + eooMarker, substrate = decodeFun(substrate) # eat up EOO marker + if not eoo.endOfOctets.isSameTypeWith(eooMarker) or \ + eooMarker != eoo.endOfOctets: + raise error.PyAsn1Error('No EOO seen before substrate ends') + else: + component, substrate= decodeFun( + substrate, r.getComponentTagMap(), tagSet, length, state + ) + if isinstance(component, univ.Choice): + effectiveTagSet = component.getEffectiveTagSet() + else: + effectiveTagSet = component.getTagSet() + r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None) return r, substrate - indefLenValueDecoder = valueDecoder - class AnyDecoder(AbstractSimpleDecoder): protoComponent = univ.Any() + tagFormats = (tag.tagFormatSimple, tag.tagFormatConstructed) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is None or \ asn1Spec is not None and tagSet != asn1Spec.getTagSet(): # untagged Any container, recover inner header substrate length = length + len(fullSubstrate) - len(substrate) substrate = fullSubstrate - substrate = substrate[:length] - return self._createComponent(asn1Spec, tagSet, value=substrate), '' + if substrateFun: + return substrateFun(self._createComponent(asn1Spec, tagSet), + substrate, length) + head, tail = substrate[:length], substrate[length:] + return self._createComponent(asn1Spec, tagSet, value=head), tail def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, - length, state, decodeFun): + length, state, decodeFun, substrateFun): if asn1Spec is not None and tagSet == asn1Spec.getTagSet(): # tagged Any type -- consume header substrate header = '' @@ -450,11 +482,12 @@ class AnyDecoder(AbstractSimpleDecoder): # Any components do not inherit initial tag asn1Spec = self.protoComponent - if not decodeFun: - return r, substrate + if substrateFun: + return substrateFun(r, substrate, length) while substrate: component, substrate = decodeFun(substrate, asn1Spec) - if component == eoo.endOfOctets: + if eoo.endOfOctets.isSameTypeWith(component) and \ + component == eoo.endOfOctets: break r = r + component else: @@ -550,7 +583,10 @@ class Decoder: self.__tagSetCache = {} def __call__(self, substrate, asn1Spec=None, tagSet=None, - length=None, state=stDecodeTag, recursiveFlag=1): + length=None, state=stDecodeTag, recursiveFlag=1, + substrateFun=None): + if debug.logger & debug.flagDecoder: + debug.logger('decoder called at scope %s with state %d, working with up to %d octets of substrate: %s' % (debug.scope, state, len(substrate), debug.hexdump(substrate))) fullSubstrate = substrate while state != stStop: if state == stDecodeTag: @@ -559,6 +595,9 @@ class Decoder: raise error.SubstrateUnderrunError( 'Short octet stream on tag decoding' ) + if not isOctetsType(substrate) and \ + not isinstance(substrate, univ.OctetString): + raise error.PyAsn1Error('Bad octet stream type') firstOctet = substrate[0] substrate = substrate[1:] @@ -598,6 +637,7 @@ class Decoder: else: tagSet = lastTag + tagSet state = stDecodeLength + debug.logger and debug.logger & debug.flagDecoder and debug.logger('tag decoded into %r, decoding length' % tagSet) if state == stDecodeLength: # Decode length if not substrate: @@ -625,12 +665,13 @@ class Decoder: for char in lengthString: length = (length << 8) | oct2int(char) size = size + 1 - state = stGetValueDecoder substrate = substrate[size:] if length != -1 and len(substrate) < length: raise error.SubstrateUnderrunError( '%d-octet short' % (length - len(substrate)) ) + state = stGetValueDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value length decoded into %d, payload substrate is: %s' % (length, debug.hexdump(length == -1 and substrate or substrate[:length]))) if state == stGetValueDecoder: if asn1Spec is None: state = stGetValueDecoderByTag @@ -669,14 +710,27 @@ class Decoder: state = stDecodeValue else: state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by a built-in type, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(concreteDecoder is None and '?' or concreteDecoder.protoComponent.__class__.__name__) if state == stGetValueDecoderByAsn1Spec: if isinstance(asn1Spec, (dict, tagmap.TagMap)): if tagSet in asn1Spec: __chosenSpec = asn1Spec[tagSet] else: __chosenSpec = None + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('candidate ASN.1 spec is a map of:') + for t, v in asn1Spec.getPosMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + if asn1Spec.getNegMap(): + debug.logger('but neither of: ') + for i in asn1Spec.getNegMap().items(): + debug.logger(' %r -> %s' % (t, v.__class__.__name__)) + debug.logger('new candidate ASN.1 spec is %s, chosen by %r' % (__chosenSpec is None and '' or __chosenSpec.__class__.__name__, tagSet)) else: __chosenSpec = asn1Spec + debug.logger and debug.logger & debug.flagDecoder and debug.logger('candidate ASN.1 spec is %s' % asn1Spec.__class__.__name__) if __chosenSpec is not None and ( tagSet == __chosenSpec.getTagSet() or \ tagSet in __chosenSpec.getTagMap() @@ -687,9 +741,11 @@ class Decoder: __chosenSpec.typeId in self.__typeMap: # ambiguous type concreteDecoder = self.__typeMap[__chosenSpec.typeId] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen for an ambiguous type by type ID %s' % (__chosenSpec.typeId,)) elif baseTagSet in self.__tagMap: # base type or tagged subtype concreteDecoder = self.__tagMap[baseTagSet] + debug.logger and debug.logger & debug.flagDecoder and debug.logger('value decoder chosen by base %r' % (baseTagSet,)) else: concreteDecoder = None if concreteDecoder: @@ -700,8 +756,13 @@ class Decoder: elif tagSet == self.__endOfOctetsTagSet: concreteDecoder = self.__tagMap[tagSet] state = stDecodeValue + debug.logger and debug.logger & debug.flagDecoder and debug.logger('end-of-octets found') else: + concreteDecoder = None state = stTryAsExplicitTag + if debug.logger and debug.logger & debug.flagDecoder: + debug.logger('codec %s chosen by ASN.1 spec, decoding %s' % (state == stDecodeValue and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(__chosenSpec is None and '?' or __chosenSpec.__class__.__name__) if state == stTryAsExplicitTag: if tagSet and \ tagSet[0][1] == tag.tagFormatConstructed and \ @@ -710,34 +771,35 @@ class Decoder: concreteDecoder = explicitTagDecoder state = stDecodeValue else: + concreteDecoder = None state = self.defaultErrorState + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state == stDecodeValue and 'value' or 'as failure')) if state == stDumpRawValue: concreteDecoder = self.defaultRawDecoder + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s chosen, decoding value' % concreteDecoder.__class__.__name__) state = stDecodeValue if state == stDecodeValue: - if recursiveFlag: - decodeFun = self - else: - decodeFun = None + if recursiveFlag == 0 and not substrateFun: # legacy + substrateFun = lambda a,b,c: (a,b[:c]) if length == -1: # indef length value, substrate = concreteDecoder.indefLenValueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) else: - value, _substrate = concreteDecoder.valueDecoder( + value, substrate = concreteDecoder.valueDecoder( fullSubstrate, substrate, asn1Spec, tagSet, length, - stGetValueDecoder, decodeFun + stGetValueDecoder, self, substrateFun ) - if recursiveFlag: - substrate = substrate[length:] - else: - substrate = _substrate state = stStop + debug.logger and debug.logger & debug.flagDecoder and debug.logger('codec %s yields type %s, value:\n%s\n...remaining substrate is: %s' % (concreteDecoder.__class__.__name__, value.__class__.__name__, value.prettyPrint(), substrate and debug.hexdump(substrate) or '')) if state == stErrorCondition: raise error.PyAsn1Error( '%r not in asn1Spec: %r' % (tagSet, asn1Spec) ) + if debug.logger and debug.logger & debug.flagDecoder: + debug.scope.pop() + debug.logger('decoder left scope %s, call completed' % debug.scope) return value, substrate decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/ber/encoder.py b/libs/pyasn1/codec/ber/encoder.py index 2149b0ba..173949d0 100644 --- a/libs/pyasn1/codec/ber/encoder.py +++ b/libs/pyasn1/codec/ber/encoder.py @@ -1,8 +1,8 @@ # BER encoder from pyasn1.type import base, tag, univ, char, useful from pyasn1.codec.ber import eoo -from pyasn1.compat.octets import int2oct, ints2octs, null, str2octs -from pyasn1 import error +from pyasn1.compat.octets import int2oct, oct2int, ints2octs, null, str2octs +from pyasn1 import debug, error class Error(Exception): pass @@ -78,9 +78,24 @@ class ExplicitlyTaggedItemEncoder(AbstractItemEncoder): explicitlyTaggedItemEncoder = ExplicitlyTaggedItemEncoder() +class BooleanEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 + _true = ints2octs((1,)) + _false = ints2octs((0,)) + def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + return value and self._true or self._false, 0 + class IntegerEncoder(AbstractItemEncoder): supportIndefLenMode = 0 + supportCompactZero = False def encodeValue(self, encodeFun, value, defMode, maxChunkSize): + if value == 0: # shortcut for zero value + if self.supportCompactZero: + # this seems to be a correct way for encoding zeros + return null, 0 + else: + # this seems to be a widespread way for encoding zeros + return ints2octs((0,)), 0 octets = [] value = int(value) # to save on ops on asn1 type while 1: @@ -149,18 +164,15 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): index = 5 else: if len(oid) < 2: - raise error.PyAsn1Error('Short OID %s' % value) + raise error.PyAsn1Error('Short OID %s' % (value,)) # Build the first twos - index = 0 - subid = oid[index] * 40 - subid = subid + oid[index+1] - if subid < 0 or subid > 0xff: + if oid[0] > 6 or oid[1] > 39 or oid[0] == 6 and oid[1] > 15: raise error.PyAsn1Error( - 'Initial sub-ID overflow %s in OID %s' % (oid[index:], value) + 'Initial sub-ID overflow %s in OID %s' % (oid[:2], value) ) - octets = (subid,) - index = index + 2 + octets = (oid[0] * 40 + oid[1],) + index = 2 # Cycle through subids for subid in oid[index:]: @@ -184,6 +196,7 @@ class ObjectIdentifierEncoder(AbstractItemEncoder): return ints2octs(octets), 0 class RealEncoder(AbstractItemEncoder): + supportIndefLenMode = 0 def encodeValue(self, encodeFun, value, defMode, maxChunkSize): if value.isPlusInfinity(): return int2oct(0x40), 0 @@ -206,9 +219,11 @@ class RealEncoder(AbstractItemEncoder): m >>= 1 e += 1 eo = null - while e: + while e not in (0, -1): eo = int2oct(e&0xff) + eo e >>= 8 + if e == 0 and eo and oct2int(eo[0]) & 0x80: + eo = int2oct(0) + eo n = len(eo) if n > 0xff: raise error.PyAsn1Error('Real exponent overflow') @@ -268,7 +283,7 @@ class AnyEncoder(OctetStringEncoder): tagMap = { eoo.endOfOctets.tagSet: EndOfOctetsEncoder(), - univ.Boolean.tagSet: IntegerEncoder(), + univ.Boolean.tagSet: BooleanEncoder(), univ.Integer.tagSet: IntegerEncoder(), univ.BitString.tagSet: BitStringEncoder(), univ.OctetString.tagSet: OctetStringEncoder(), @@ -313,6 +328,7 @@ class Encoder: self.__typeMap = typeMap def __call__(self, value, defMode=1, maxChunkSize=0): + debug.logger & debug.flagEncoder and debug.logger('encoder called in %sdef mode, chunk size %s for type %s, value:\n%s' % (not defMode and 'in' or '', maxChunkSize, value.__class__.__name__, value.prettyPrint())) tagSet = value.getTagSet() if len(tagSet) > 1: concreteEncoder = explicitlyTaggedItemEncoder @@ -322,13 +338,16 @@ class Encoder: elif tagSet in self.__tagMap: concreteEncoder = self.__tagMap[tagSet] else: - baseTagSet = value.baseTagSet - if baseTagSet in self.__tagMap: - concreteEncoder = self.__tagMap[baseTagSet] + tagSet = value.baseTagSet + if tagSet in self.__tagMap: + concreteEncoder = self.__tagMap[tagSet] else: - raise Error('No encoder for %s' % value) - return concreteEncoder.encode( + raise Error('No encoder for %s' % (value,)) + debug.logger & debug.flagEncoder and debug.logger('using value codec %s chosen by %r' % (concreteEncoder.__class__.__name__, tagSet)) + substrate = concreteEncoder.encode( self, value, defMode, maxChunkSize ) + debug.logger & debug.flagEncoder and debug.logger('built %s octets of substrate: %s\nencoder completed' % (len(substrate), debug.hexdump(substrate))) + return substrate encode = Encoder(tagMap, typeMap) diff --git a/libs/pyasn1/codec/cer/__init__.py b/libs/pyasn1/codec/cer/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/cer/__init__.py +++ b/libs/pyasn1/codec/cer/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/cer/decoder.py b/libs/pyasn1/codec/cer/decoder.py index 71395d22..9fd37c13 100644 --- a/libs/pyasn1/codec/cer/decoder.py +++ b/libs/pyasn1/codec/cer/decoder.py @@ -7,22 +7,25 @@ from pyasn1 import error class BooleanDecoder(decoder.AbstractSimpleDecoder): protoComponent = univ.Boolean(0) def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length, - state, decodeFun): - substrate = substrate[:length] - if not substrate: + state, decodeFun, substrateFun): + head, tail = substrate[:length], substrate[length:] + if not head: raise error.PyAsn1Error('Empty substrate') - byte = oct2int(substrate[0]) + byte = oct2int(head[0]) + # CER/DER specifies encoding of TRUE as 0xFF and FALSE as 0x0, while + # BER allows any non-zero value as TRUE; cf. sections 8.2.2. and 11.1 + # in http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf if byte == 0xff: value = 1 elif byte == 0x00: value = 0 else: raise error.PyAsn1Error('Boolean CER violation: %s' % byte) - return self._createComponent(asn1Spec, tagSet, value), substrate[1:] + return self._createComponent(asn1Spec, tagSet, value), tail tagMap = decoder.tagMap.copy() tagMap.update({ - univ.Boolean.tagSet: BooleanDecoder(), + univ.Boolean.tagSet: BooleanDecoder() }) typeMap = decoder.typeMap diff --git a/libs/pyasn1/codec/der/__init__.py b/libs/pyasn1/codec/der/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/codec/der/__init__.py +++ b/libs/pyasn1/codec/der/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/codec/der/decoder.py b/libs/pyasn1/codec/der/decoder.py index 0f5a24ca..604abec2 100644 --- a/libs/pyasn1/codec/der/decoder.py +++ b/libs/pyasn1/codec/der/decoder.py @@ -2,4 +2,8 @@ from pyasn1.type import univ from pyasn1.codec.cer import decoder -decode = decoder.Decoder(decoder.tagMap, decoder.typeMap) +tagMap = decoder.tagMap +typeMap = decoder.typeMap +Decoder = decoder.Decoder + +decode = Decoder(tagMap, typeMap) diff --git a/libs/pyasn1/compat/__init__.py b/libs/pyasn1/compat/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/compat/__init__.py +++ b/libs/pyasn1/compat/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/compat/octets.py b/libs/pyasn1/compat/octets.py index d0303eaa..f7f2a29b 100644 --- a/libs/pyasn1/compat/octets.py +++ b/libs/pyasn1/compat/octets.py @@ -8,6 +8,7 @@ if version_info[0] <= 2: octs2ints = lambda s: [ oct2int(x) for x in s ] str2octs = lambda x: x octs2str = lambda x: x + isOctetsType = lambda s: isinstance(s, str) else: ints2octs = bytes int2oct = lambda x: ints2octs((x,)) @@ -16,3 +17,4 @@ else: octs2ints = lambda s: [ x for x in s ] str2octs = lambda x: x.encode() octs2str = lambda x: x.decode() + isOctetsType = lambda s: isinstance(s, bytes) diff --git a/libs/pyasn1/debug.py b/libs/pyasn1/debug.py new file mode 100644 index 00000000..c27cb1d4 --- /dev/null +++ b/libs/pyasn1/debug.py @@ -0,0 +1,65 @@ +import sys +from pyasn1.compat.octets import octs2ints +from pyasn1 import error +from pyasn1 import __version__ + +flagNone = 0x0000 +flagEncoder = 0x0001 +flagDecoder = 0x0002 +flagAll = 0xffff + +flagMap = { + 'encoder': flagEncoder, + 'decoder': flagDecoder, + 'all': flagAll + } + +class Debug: + defaultPrinter = sys.stderr.write + def __init__(self, *flags): + self._flags = flagNone + self._printer = self.defaultPrinter + self('running pyasn1 version %s' % __version__) + for f in flags: + if f not in flagMap: + raise error.PyAsn1Error('bad debug flag %s' % (f,)) + self._flags = self._flags | flagMap[f] + self('debug category \'%s\' enabled' % f) + + def __str__(self): + return 'logger %s, flags %x' % (self._printer, self._flags) + + def __call__(self, msg): + self._printer('DBG: %s\n' % msg) + + def __and__(self, flag): + return self._flags & flag + + def __rand__(self, flag): + return flag & self._flags + +logger = 0 + +def setLogger(l): + global logger + logger = l + +def hexdump(octets): + return ' '.join( + [ '%s%.2X' % (n%16 == 0 and ('\n%.5d: ' % n) or '', x) + for n,x in zip(range(len(octets)), octs2ints(octets)) ] + ) + +class Scope: + def __init__(self): + self._list = [] + + def __str__(self): return '.'.join(self._list) + + def push(self, token): + self._list.append(token) + + def pop(self): + return self._list.pop() + +scope = Scope() diff --git a/libs/pyasn1/type/__init__.py b/libs/pyasn1/type/__init__.py index e69de29b..8c3066b2 100644 --- a/libs/pyasn1/type/__init__.py +++ b/libs/pyasn1/type/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/libs/pyasn1/type/base.py b/libs/pyasn1/type/base.py index db31671e..40873719 100644 --- a/libs/pyasn1/type/base.py +++ b/libs/pyasn1/type/base.py @@ -120,7 +120,12 @@ class AbstractSimpleAsn1Item(Asn1ItemBase): def prettyIn(self, value): return value def prettyOut(self, value): return str(value) - def prettyPrint(self, scope=0): return self.prettyOut(self._value) + def prettyPrint(self, scope=0): + if self._value is noValue: + return '' + else: + return self.prettyOut(self._value) + # XXX Compatibility stub def prettyPrinter(self, scope=0): return self.prettyPrint(scope) diff --git a/libs/pyasn1/type/namedtype.py b/libs/pyasn1/type/namedtype.py index aa9c5678..48967a5f 100644 --- a/libs/pyasn1/type/namedtype.py +++ b/libs/pyasn1/type/namedtype.py @@ -60,12 +60,12 @@ class NamedTypes: tagMap = self.__namedTypes[idx].getType().getTagMap() for t in tagMap.getPosMap(): if t in self.__tagToPosIdx: - raise error.PyAsn1Error('Duplicate type %s' % t) + raise error.PyAsn1Error('Duplicate type %s' % (t,)) self.__tagToPosIdx[t] = idx try: return self.__tagToPosIdx[tagSet] except KeyError: - raise error.PyAsn1Error('Type %s not found' % tagSet) + raise error.PyAsn1Error('Type %s not found' % (tagSet,)) def getNameByPosition(self, idx): try: @@ -79,12 +79,12 @@ class NamedTypes: idx = idx - 1 n = self.__namedTypes[idx].getName() if n in self.__nameToPosIdx: - raise error.PyAsn1Error('Duplicate name %s' % n) + raise error.PyAsn1Error('Duplicate name %s' % (n,)) self.__nameToPosIdx[n] = idx try: return self.__nameToPosIdx[name] except KeyError: - raise error.PyAsn1Error('Name %s not found' % name) + raise error.PyAsn1Error('Name %s not found' % (name,)) def __buildAmbigiousTagMap(self): ambigiousTypes = () diff --git a/libs/pyasn1/type/namedval.py b/libs/pyasn1/type/namedval.py index 815e2d42..d0fea7cc 100644 --- a/libs/pyasn1/type/namedval.py +++ b/libs/pyasn1/type/namedval.py @@ -15,10 +15,10 @@ class NamedValues: name = namedValue val = automaticVal if name in self.nameToValIdx: - raise error.PyAsn1Error('Duplicate name %s' % name) + raise error.PyAsn1Error('Duplicate name %s' % (name,)) self.nameToValIdx[name] = val if val in self.valToNameIdx: - raise error.PyAsn1Error('Duplicate value %s' % name) + raise error.PyAsn1Error('Duplicate value %s=%s' % (name, val)) self.valToNameIdx[val] = name self.namedValues = self.namedValues + ((name, val),) automaticVal = automaticVal + 1 diff --git a/libs/pyasn1/type/tag.py b/libs/pyasn1/type/tag.py index 0cf67ebd..1144907f 100644 --- a/libs/pyasn1/type/tag.py +++ b/libs/pyasn1/type/tag.py @@ -18,7 +18,7 @@ class Tag: def __init__(self, tagClass, tagFormat, tagId): if tagId < 0: raise error.PyAsn1Error( - 'Negative tag ID (%s) not allowed' % tagId + 'Negative tag ID (%s) not allowed' % (tagId,) ) self.__tag = (tagClass, tagFormat, tagId) self.uniq = (tagClass, tagId) diff --git a/libs/pyasn1/type/tagmap.py b/libs/pyasn1/type/tagmap.py index 53e1791a..7cec3a10 100644 --- a/libs/pyasn1/type/tagmap.py +++ b/libs/pyasn1/type/tagmap.py @@ -28,7 +28,7 @@ class TagMap: def clone(self, parentType, tagMap, uniq=False): if self.__defType is not None and tagMap.getDef() is not None: - raise error.PyAsn1Error('Duplicate default value at %s' % self) + raise error.PyAsn1Error('Duplicate default value at %s' % (self,)) if tagMap.getDef() is not None: defType = tagMap.getDef() else: @@ -37,7 +37,7 @@ class TagMap: posMap = self.__posMap.copy() for k in tagMap.getPosMap(): if uniq and k in posMap: - raise error.PyAsn1Error('Duplicate positive key %s' % k) + raise error.PyAsn1Error('Duplicate positive key %s' % (k,)) posMap[k] = parentType negMap = self.__negMap.copy() diff --git a/libs/pyasn1/type/univ.py b/libs/pyasn1/type/univ.py index cb4f49b7..9cd16f8a 100644 --- a/libs/pyasn1/type/univ.py +++ b/libs/pyasn1/type/univ.py @@ -69,13 +69,18 @@ class Integer(base.AbstractSimpleAsn1Item): def prettyIn(self, value): if not isinstance(value, str): - return int(value) + try: + return int(value) + except: + raise error.PyAsn1Error( + 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) + ) r = self.__namedValues.getValue(value) if r is not None: return r try: return int(value) - except ValueError: + except: raise error.PyAsn1Error( 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1]) ) @@ -224,14 +229,14 @@ class BitString(base.AbstractSimpleAsn1Item): return tuple(r) else: raise error.PyAsn1Error( - 'Bad BIT STRING value notation %s' % value + 'Bad BIT STRING value notation %s' % (value,) ) else: for i in value.split(','): j = self.__namedValues.getValue(i) if j is None: raise error.PyAsn1Error( - 'Unknown bit identifier \'%s\'' % i + 'Unknown bit identifier \'%s\'' % (i,) ) if j >= len(r): r.extend([0]*(j-len(r)+1)) @@ -528,7 +533,7 @@ class Real(base.AbstractSimpleAsn1Item): ) if value[1] not in (2, 10): raise error.PyAsn1Error( - 'Prohibited base for Real value: %s' % value[1] + 'Prohibited base for Real value: %s' % (value[1],) ) if value[1] == 10: value = self.__normalizeBase10(value) @@ -648,7 +653,7 @@ class SetOf(base.AbstractConstructedAsn1Item): def _verifyComponent(self, idx, value): if self._componentType is not None and \ not self._componentType.isSuperTypeOf(value): - raise error.PyAsn1Error('Component type error %s' % value) + raise error.PyAsn1Error('Component type error %s' % (value,)) def getComponentByPosition(self, idx): return self._componentValues[idx] def setComponentByPosition(self, idx, value=None, verifyConstraints=True): @@ -924,9 +929,9 @@ class Choice(Set): return self._componentValues[self._currentIdx] >= other return NotImplemented if sys.version_info[0] <= 2: - def __nonzero__(self, other): return bool(self._componentValues) + def __nonzero__(self): return bool(self._componentValues) else: - def __bool__(self, other): return bool(self._componentValues) + def __bool__(self): return bool(self._componentValues) def __len__(self): return self._currentIdx is not None and 1 or 0 diff --git a/libs/pyutil/_version.py b/libs/pyutil/_version.py index 617d2205..376b2b9b 100644 --- a/libs/pyutil/_version.py +++ b/libs/pyutil/_version.py @@ -6,7 +6,7 @@ # pyutil.version_class for a description of what the different fields mean. __pkgname__ = "pyutil" -verstr = "1.9.3" +verstr = "1.9.7" try: from pyutil.version_class import Version as pyutil_Version __version__ = pyutil_Version(verstr) diff --git a/libs/pyutil/benchutil.py b/libs/pyutil/benchutil.py index 3e773a63..6c286346 100644 --- a/libs/pyutil/benchutil.py +++ b/libs/pyutil/benchutil.py @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -21,10 +21,10 @@ the second, e.g.: >>> rep_bench(fib, 25, UNITS_PER_SECOND=1000) best: 1.968e+00, 3th-best: 1.987e+00, mean: 2.118e+00, 3th-worst: 2.175e+00, worst: 2.503e+00 (of 10) -The output is reporting the number of milliseconds that executing the function -took, divided by N, from ten different invocations of fib(). It reports the -best, worst, M-th best, M-th worst, and mean, where "M" is the natural log of -the number of invocations (in this case 10). +The output is reporting the number of milliseconds that executing the +function took, divided by N, from ten different invocations of +fib(). It reports the best, worst, M-th best, M-th worst, and mean, +where "M" is 1/4 of the number of invocations (in this case 10). 2. Now run it with different values of N and look for patterns: @@ -74,10 +74,12 @@ and the main function is to make them be methods of the same object, e.g.: 4. Things to fix: - a. I used to have it hooked up to use the "hotshot" profiler on the code being - measured. I recently tried to change it to use the newer cProfile profiler - instead, but I don't understand the interface to cProfiler so it just gives an - exception if you pass profile=True. Please fix this and send me a patch. + a. I used to have it hooked up to use the "hotshot" profiler on the + code being measured. I recently tried to change it to use the newer + cProfile profiler instead, but I don't understand the interface to + cProfiler so it just gives an exception if you pass + profile=True. Please fix this and send me a patch. xxx change it to + statprof b. Wouldn't it be great if this script emitted results in a json format that was understood by a tool to make pretty interactive explorable graphs? The @@ -122,7 +124,7 @@ def mult(a, b): except TypeError: return to_decimal(a) * to_decimal(b) -def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): +def rep_bench(func, n, runtime=1.0, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME (wall-clock time) has already elapsed (unless MAXTIME is None). @@ -130,33 +132,43 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p @param quiet Don't print anything--just return the results dict. """ assert isinstance(n, int), (n, type(n)) + global worstemptymeasure + emsta = clock() + do_nothing(2**32) + emstop = clock() + empty = emstop - emsta + if empty > worstemptymeasure: + worstemptymeasure = empty + if (worstemptymeasure*2) >= runtime: + raise BadMeasure("Apparently simply invoking an empty Python function can take as long as %0.10f seconds, and we were running iterations for only about %0.10f seconds. So the measurement of the runtime of the code under benchmark is not reliable. Please pass a higher number for the 'runtime' argument to bench_it().") + startwallclocktime = time.time() - tls = [] # elapsed time in seconds + tls = [] # (elapsed time per iter in seconds, iters) bmes = [] while ((len(tls) < MAXREPS) or (MAXREPS is None)) and ((MAXTIME is None) or ((time.time() - startwallclocktime) < MAXTIME)): if initfunc: initfunc(n) try: - tl = bench_it(func, n, profile=profile, profresults=profresults) + tl, iters = bench_it(func, n, runtime=runtime, profile=profile, profresults=profresults) except BadMeasure, bme: bmes.append(bme) else: - tls.append(tl) + tls.append((tl, iters)) if len(tls) == 0: raise Exception("Couldn't get any measurements within time limits or number-of-attempts limits. Maybe something is wrong with your clock? %s" % (bmes,)) - sumtls = reduce(operator.__add__, tls) + sumtls = sum([tl for (tl, iters) in tls]) mean = sumtls / len(tls) tls.sort() - worst = tls[-1] - best = tls[0] - _assert(best > worstemptymeasure*MARGINOFERROR, "%s(n=%s) took %0.10f seconds, but we cannot measure times much less than about %0.10f seconds. Try a more time-consuming variant (such as higher n)." % (func, n, best, worstemptymeasure*MARGINOFERROR,)) + worst = tls[-1][0] + best = tls[0][0] + m = len(tls)/4 if m > 0: - mthbest = tls[m-1] - mthworst = tls[-m] + mthbest = tls[m-1][0] + mthworst = tls[-m][0] else: - mthbest = tls[0] - mthworst = tls[-1] + mthbest = tls[0][0] + mthworst = tls[-1][0] # The +/-0 index is the best/worst, the +/-1 index is the 2nd-best/worst, # etc, so we use mp1 to name it. @@ -196,26 +208,22 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=1.0, profile=False, profresults="pyutil-benchutil.prof"): if profile: - st = clock() - cProfile.run('func(n)', profresults) - sto = clock() + raise NotImplementedException() else: + iters = 0 st = clock() - func(n) + deadline = st + runtime sto = clock() + while sto < deadline: + func(n) + iters += 1 + sto = clock() timeelapsed = sto - st - if timeelapsed <= 0: - raise BadMeasure(timeelapsed) - global worstemptymeasure - emsta = clock() - do_nothing(2**32) - emstop = clock() - empty = emstop - emsta - if empty > worstemptymeasure: - worstemptymeasure = empty - return timeelapsed + if (timeelapsed <= 0) or (iters == 0): + raise BadMeasure((timeelapsed, iters)) + return (timeelapsed / iters, iters) def bench(func, initfunc=None, TOPXP=21, MAXREPS=5, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", outputjson=False, jsonresultsfname="pyutil-benchutil-results.json", UNITS_PER_SECOND=1): BSIZES = [] diff --git a/libs/pyutil/benchutil.py~ b/libs/pyutil/benchutil.py~ index 3ec323eb..a33111e8 100644 --- a/libs/pyutil/benchutil.py~ +++ b/libs/pyutil/benchutil.py~ @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2012 Zooko Wilcox-O'Hearn +# Copyright (c) 2002-2013 Zooko Wilcox-O'Hearn # This file is part of pyutil; see README.rst for licensing terms. """ @@ -104,6 +104,24 @@ def makeg(func): func() return blah +def to_decimal(x): + """ + See if D(x) returns something. If instead it raises TypeError, x must have been a float, so convert it to Decimal by way of string. (In Python >= 2.7, D(x) does this automatically. + """ + try: + return D(x) + except TypeError: + return D("%0.54f" % (x,)) + +def mult(a, b): + """ + If we get TypeError from * (possibly because one is float and the other is Decimal), then promote them both to Decimal. + """ + try: + return a * b + except TypeError: + return to_decimal(a) * to_decimal(b) + def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, profresults="pyutil-benchutil.prof", UNITS_PER_SECOND=1, quiet=False): """ Will run the func up to MAXREPS times, but won't start a new run if MAXTIME @@ -144,12 +162,12 @@ def rep_bench(func, n, initfunc=None, MAXREPS=10, MAXTIME=60.0, profile=False, p # etc, so we use mp1 to name it. mp1 = m+1 res = { - 'worst': (worst*UNITS_PER_SECOND)/n, - 'best': (best*UNITS_PER_SECOND)/n, + 'worst': mult(worst, UNITS_PER_SECOND)/n, + 'best': mult(best, UNITS_PER_SECOND)/n, 'mp1': mp1, - 'mth-best': (mthbest*UNITS_PER_SECOND)/n, - 'mth-worst': (mthworst*UNITS_PER_SECOND)/n, - 'mean': (mean*UNITS_PER_SECOND)/n, + 'mth-best': mult(mthbest, UNITS_PER_SECOND)/n, + 'mth-worst': mult(mthworst, UNITS_PER_SECOND)/n, + 'mean': mult(mean, UNITS_PER_SECOND)/n, 'num': len(tls), } @@ -178,7 +196,10 @@ class BadMeasure(Exception): def do_nothing(n): pass -def bench_it(func, n, profile=False, profresults="pyutil-benchutil.prof"): +def bench_it(func, n, runtime=0.1, profile=False, profresults="pyutil-benchutil.prof"): + """ + runtime is how many seconds to + """ if profile: st = clock() cProfile.run('func(n)', profresults) diff --git a/libs/pyutil/data/wordlist.txt b/libs/pyutil/data/wordlist.txt new file mode 100644 index 00000000..e1048b99 --- /dev/null +++ b/libs/pyutil/data/wordlist.txt @@ -0,0 +1,7248 @@ +fawn +yellow +four +prefix +payoff +scold +outwit +lore +lord +swivel +deli +pigment +foul +fur +disturb +prize +broiler +wooden +satchel +crotch +fritter +charter +tired +miller +bacon +second +tether +ruthless +thunder +fossil +succumb +cull +specialist +hero +avert +herb +splinter +here +herd +china +dogwood +cult +shriek +chink +pancreas +robin +neurologist +climber +diplomat +golden +gridiron +lengthen +summons +remnant +stern +unit +spoke +exhort +statesmanship +music +bedrock +passport +strike +teaspoon +relay +relax +hurt +meteorologist +glass +hurl +hole +hold +unpack +sweeten +blade +locker +locket +plunger +wand +wane +unjust +household +digit +malign +caution +want +rayon +hog +hoe +travel +copious +cutback +revisit +how +hot +hop +cheetah +diagram +possum +modest +antonym +pigtail +revolt +alias +decoy +wing +squint +wine +feedback +misdemeanor +kickoff +foodstuff +butcher +dreamer +fir +bowlder +fix +fib +fig +fin +undercut +enrich +slate +interrupt +sixteen +silver +scholar +thyme +seamstress +debut +arrow +debug +volcano +burial +whim +concord +knockout +garment +allah +spider +crocus +turnip +yiddish +fortnight +allay +whir +whip +diction +smirk +mason +semiconductor +re +adapt +outburst +knit +scruff +silicon +miaow +thumbtack +shopper +wasp +wash +instruct +rhododendron +tango +master +architect +bitter +listen +wisdom +swish +sulphur +crawl +trek +peril +outlay +coward +tree +shower +pneumonia +sheen +acclaim +entail +girder +runner +spectrum +headland +increment +quay +dozen +kidnap +gripe +hum +greenback +tipi +matriarch +stirrup +object +toil +microsecond +mouth +addict +letter +fluster +drought +thriller +expound +singer +upend +grove +professor +camp +detriment +nineteenth +scream +marvel +bomb +reactor +heckler +ulcer +caper +layout +menu +bust +cougar +bush +bliss +rich +mend +rice +plate +pocket +cushion +fetish +relish +jaguar +boarder +pretzel +patch +hasten +respond +fair +heirloom +radium +radius +result +fail +crouch +clef +best +irk +yogurt +ire +wage +extend +vestment +souvenir +extent +wheelbarrow +carbon +debt +roller +accident +trickster +veer +disdain +cup +logic +genus +rehash +gopher +canyon +bewilder +chrome +onomatopoeia +advert +grapefruit +stadium +jackass +counterattack +life +retrospect +spit +worker +wish +lift +toboggan +chile +child +chili +spin +wildcat +dissect +employ +calcium +delicatessen +locksmith +letdown +player +elicit +eighteen +violin +doorman +specter +hone +toaster +honk +rebellion +split +bid +european +typhoid +boiler +ownership +supper +tuna +tune +furlough +noblewoman +unhook +abound +bellow +beset +plight +brandish +previous +ham +hag +hay +prison +falter +east +hat +quirk +birth +shadow +gangplank +remind +pavement +battlefield +attorney +right +old +creek +crowd +creed +crown +glove +billboard +creep +chorus +okra +bottom +circumvent +inhuman +fox +foe +fog +binder +yoke +slither +recollect +despair +rebut +eightieth +sob +sod +overshadow +honeymoon +overgrow +sop +sow +wrap +fabric +panorama +support +tame +avail +width +hothead +call +overhand +overhang +telegraph +offer +thesaurus +beech +squalid +safeguard +otter +duel +misinform +paprika +vanguard +pest +duet +proud +tournament +proven +exist +quintuplet +dealer +leer +floor +glacier +actor +flood +role +entomologist +sunset +smell +leek +intend +glutton +ointment +asterisk +taurus +intent +cleaver +entrust +windscreen +puss +lowdown +time +push +gown +chain +viaduct +skate +chair +midst +millisecond +ballet +uneven +vex +crater +oversight +jerk +ameba +embark +flora +mourn +knapsack +southpaw +exact +epic +judaism +tear +teas +teat +crustacean +subway +team +skewer +prevent +meadow +gremlin +attic +sigh +milligram +heavyweight +crescent +playpen +crackpot +melt +current +boost +abscond +gnaw +splice +address +brilliant +endow +queue +influx +love +radish +prefer +piranha +fake +instal +forefront +sky +homesick +turret +wicker +wicket +scope +prosecutor +wicked +afford +refrain +visual +appendix +behalf +mascot +lumberjack +pretend +descriptor +dispossess +stole +winter +savor +sputter +meddler +slush +spot +textual +date +suck +dove +pulley +stress +conscious +bluster +wheelchair +quadrant +mango +so +skirmish +truce +drunken +archeologist +footstep +yearn +jig +disconnect +thumb +accordion +nearsighted +councillor +hubbub +suspicion +thump +apron +civilian +insomnia +nation +amulet +twilight +ketchup +handiwork +revert +fisherman +quarter +quartet +receipt +fireproof +breakthrough +sponsor +troll +naked +canvas +onrush +trauma +formula +dumbfound +million +envelop +vicious +disrespect +mime +plea +byte +workmanship +punk +wrong +ostrich +punt +footwear +neglect +gunshot +potter +one +reopen +chide +conifer +vote +paleontologist +languish +boulevard +wrath +convent +bite +extortion +shiver +draft +cite +starfish +shawl +artifact +snatch +antic +boyfriend +iceberg +rival +stammer +counselor +janitor +prospect +sac +greyhound +argument +alley +sad +say +borough +saw +handicraft +tulip +general +knead +zoo +note +take +destroy +printer +buffer +squalor +compress +buffet +crochet +knee +byway +lawn +enamel +blockhead +sale +cocoanut +wind +axe +salt +cobra +homespun +lotus +friction +bright +slot +slow +slop +unkind +gourd +transact +cloak +debunk +slog +hockey +slob +robe +clank +dissimilar +psychiatrist +clang +outlet +prime +artist +saliva +borrow +soloist +carrion +handcuff +primp +landlord +tortilla +where +xmas +vision +gout +gangster +cheesecloth +diver +bugler +mutton +plummet +bootleg +teacup +bureau +mope +vender +jumper +spars +screen +dome +supermarket +adept +jovial +spare +spark +quack +oust +fit +madcap +mane +flipper +backpack +twin +boar +supervisor +extinct +twig +boat +companionship +stretch +west +breath +reflex +gist +thousand +photon +cloudburst +turtleneck +former +jute +scarlet +straighten +spotlight +girth +brow +canon +dubious +monk +blab +fame +spunk +breakdown +hideaway +deft +barber +disown +booster +driftwood +veal +pewter +dimension +scholarship +summer +manifold +poach +disconcert +slime +rest +invalid +alarmist +mandolin +instrument +overthrow +stopwatch +haystack +joyful +sportsmanship +rejoin +dart +dark +brazier +snarl +traffic +cranium +vacuum +world +snare +dare +clan +clam +stranger +shutter +glamor +clay +claw +inter +kennel +clap +auditorium +obstruct +grub +potion +lobster +racial +endeavor +tote +tube +moslem +tuba +nook +exit +refer +zest +ration +leadership +standpoint +stone +ace +slender +meal +tumor +neighbor +act +mean +invert +braggart +homeless +wade +hypnotist +racquet +hew +burglar +her +gleam +glean +mindless +harpsichord +italic +hem +hen +defrost +epilog +pull +regimen +darken +wafer +rage +hooray +tripe +ruse +flirt +reprimand +whiz +torso +pulp +rust +ohm +gong +ad +fright +certain +epaulet +catchup +hoodlum +ay +ax +tranquil +jargon +slobber +cream +yoga +collector +abolish +tight +backgammon +congress +annex +slant +midget +brotherhood +slang +rostrum +neuter +thorn +groom +mask +kilogram +mash +mimic +mast +mass +ringworm +waiver +retch +gingham +influenza +consider +neigh +upkeep +taxicab +tinsel +to +tail +smile +norm +debit +baton +candid +salesperson +cobalt +strand +laud +pedant +sand +adjust +small +mammal +peon +ninetieth +plaid +past +burnish +gossip +canvass +healer +hick +offbeat +clock +section +succinct +method +contrast +full +hash +lobbyist +saleswoman +dramatist +backlash +brutal +prior +hamster +skyrocket +social +action +welder +raze +depart +vie +sherbet +regiment +captor +coercion +entrap +select +casket +enliven +petroleum +maltreat +pearl +sitter +morn +ballad +more +teen +teem +door +tester +signpost +nomad +doom +cunning +fatal +malt +chisel +patriarch +knocker +midstream +mall +learn +grope +male +stewardess +prompt +taunt +gallop +scab +accept +autumn +gallon +scar +rustler +condemn +huge +speedboat +fruition +cling +clink +plant +anoint +blotter +variant +unsound +plane +waver +flutter +pucker +wrench +trellis +patio +pant +instep +trade +paper +pang +brim +mislay +hearsay +buttercup +epoch +coarsen +bypass +motley +sucker +gadget +consign +imperil +skipper +harrow +nugget +fount +found +lantern +status +eyelash +clockwork +scribe +penicillin +lipstick +research +highway +bungler +belief +porcelain +bedlam +cockpit +loafer +suntan +acorn +riser +reproach +prefab +drivel +sicken +bumper +testament +clump +major +purport +limerick +number +feeder +slipper +footprint +florist +glitter +guess +guest +jet +swipe +vocalist +saint +gnash +relationship +tightwad +typhoon +mural +consult +grace +frock +getaway +vocal +video +defect +waft +pedestrian +graffito +caress +blond +gasket +sell +ballerina +ragamuffin +tarnish +spaghetti +self +trowel +poplar +brace +bobbin +kneecap +hypochondriac +blackboard +nasal +twine +raucous +virus +plan +wive +foyer +oyster +unequal +arson +covet +cover +barren +barrel +bulletin +chowder +golf +cruiser +affix +session +freight +impact +condor +writer +peculiar +condom +factor +downpour +dandelion +streamer +resent +actress +compass +banner +tumult +sojourn +caramel +enema +weaver +river +outlaw +prospectus +manger +set +creator +overwhelm +jade +sex +see +sea +contour +analog +project +urchin +fission +crossword +pickup +crosswalk +kneel +candor +mildew +hardship +disallow +incident +dividend +pagan +scatterbrain +lass +last +thou +opal +feminist +amoeba +lash +whole +load +loaf +electrician +pendulum +bell +loam +loan +hollow +scallop +church +psychoanalyst +underlay +napalm +airfield +devil +filth +imbed +proprietor +veneer +firm +sweetheart +champion +fire +infect +upstart +fund +deport +hostess +straight +budget +error +outskirt +real +pound +moth +vow +chasm +vanish +chase +starlight +seek +shorten +wasteland +specimen +commune +snail +teeter +cigar +epithet +alert +opinion +stack +recent +expend +clime +person +sixtieth +crayfish +telegram +aroma +belabor +amp +demerit +sandal +goblet +chest +eager +horseradish +homeland +wrongdoer +input +limp +cordon +format +bureaucrat +quest +cataclysm +blackjack +falcon +abduct +flannel +spine +consensus +crescendo +spring +beckon +palm +pall +sight +curious +sprint +battalion +pale +gruel +benefit +religion +be +odor +agreement +carol +by +scepter +coexist +hatchet +sacrament +ambush +biennial +repair +contributor +next +span +sock +submit +custom +spay +suit +spar +spat +blueprint +perplex +poster +lint +slump +pastor +overbear +link +atom +line +up +slander +foist +hornet +insignia +genial +aerial +nationalist +haunt +char +chap +chat +parsec +breather +phantom +paradox +tuft +uranium +scrape +parakeet +swirl +freighter +tart +tedium +scapegoat +trouser +scrub +gardenia +hackney +lane +land +fighter +algorithm +scotch +age +feud +summit +walker +fresh +crowbar +rescuer +hello +essay +code +partial +serviceman +scratch +broaden +totem +soften +leggin +renown +prim +flashback +young +send +moor +tremor +garret +armament +garden +quadruplet +llama +precinct +wipe +magic +harbor +eve +anxious +race +rack +mishap +crook +croon +odd +ode +victor +index +yock +sauerkraut +apparatus +indian +proffer +bird +inspect +leg +punch +acquit +let +fifteen +vinegar +great +casino +screech +scatter +survey +insulin +grandchild +buss +popcorn +mussel +maker +grower +sire +disobey +causal +zip +archbishop +theme +aeon +eleven +doubt +yardstick +midday +pencil +babe +shipwreck +patrol +rubbish +central +hoard +pout +pour +thin +drill +coffin +cherubim +bent +pawn +process +lock +slim +high +slit +bend +slip +pelvis +martyr +trumpet +weaken +rhubarb +delay +blackhead +luster +stow +halter +singular +await +wristwatch +notebook +tier +marrow +hawk +autograph +tomato +counter +robot +element +writ +allot +allow +alloy +thigh +mute +insight +spatula +comma +mutt +warren +perfect +decay +shudder +garnet +derelict +prosper +python +belch +bat +launder +dock +snake +kiss +bar +cage +wrangler +truth +scorch +subset +bump +static +thirteen +mete +jagged +disco +tenth +wander +matrix +bag +fatherland +venom +czar +oblong +lob +shut +perish +tempo +graze +tempt +shun +embarrass +minstrel +chilli +mainland +spill +length +stickler +scare +scarf +manuscript +scene +cobweb +owner +scent +prank +lop +opossum +sergeant +spaceship +painful +stomach +chagrin +vouch +rotunda +haven +steel +wet +bother +aggressor +psalm +disband +unman +steep +torrent +misunderstand +beggar +viewer +partnership +correspond +tonight +receptionist +fourteenth +mischief +depict +soak +bacterium +bassoon +hammer +adjunct +lilt +soap +soar +calculus +manor +raindrop +cipher +vise +segment +fervent +instil +locust +enlist +soprano +fiasco +brew +fact +bring +brine +bedroom +rough +asylum +trivial +brink +redirect +disillusion +planter +jay +jaw +jar +jam +tape +bourbon +flinch +hope +jackpot +move +familiar +scorn +sinus +wring +antagonist +smash +shaver +summon +stuff +rein +withstand +pronoun +packer +frame +packet +bellhop +airmail +dungeon +wire +mien +partisan +unravel +piston +pistol +email +browbeat +fetich +physicist +courtyard +lawsuit +tantrum +drum +quitter +ramp +drug +doorway +puff +roughen +medallion +revamp +migrant +distil +javelin +indict +chromium +lectern +mailman +gondola +quaver +blatant +feather +ballast +sheepish +crisscross +federalist +mannequin +altruism +banish +laser +runway +bathtub +maul +groin +ripe +lush +site +lust +mockingbird +tenor +passbook +ransom +tattoo +inquest +terrorist +buffoon +outbreak +android +balm +ball +balk +dusk +fiesta +bale +bald +dust +broccoli +mosaic +audit +off +shotgun +polyp +command +diphtheria +audio +maggot +compel +glut +glue +rambler +web +generous +clergyman +wee +wed +arrest +crack +scoundrel +government +chancellor +crux +haul +cedar +desk +password +recurs +placenta +crisp +onion +resin +alkali +stagger +imprison +nymph +sprain +overcast +foray +habitat +thief +daylight +flush +wisecrack +ballot +transport +henchman +disbelief +hoarder +avoid +disk +doer +passion +saucepan +stairway +putt +drift +ornithologist +stage +iris +sister +adverb +peal +ingest +union +artefact +parsley +assess +lung +mere +muck +commission +caviar +watchman +stamina +much +function +funnel +cosmopolitan +frisk +shyness +grate +rectum +count +congresswoman +smooth +monument +problem +baptism +cordial +kowtow +sirloin +retina +inn +replica +ink +anesthesia +furl +sexual +saviour +behold +reckless +chum +monday +repeal +veil +vein +ghost +eon +rule +dynamo +torrid +pension +tryout +abhor +buoy +inning +dote +rapid +mansion +defraud +voter +spew +bludgeon +bike +daze +regal +chill +regalia +whack +ale +compassion +blanket +distort +mania +chauvinist +chapel +whisk +daydream +pinch +scalar +handout +roadblock +unblock +math +triumph +chew +paperback +phoney +speck +heliport +sabbath +horn +chef +aristocrat +panda +stardom +lizard +walkout +toll +crunch +dustpan +pursuit +paraffin +sorceress +hairdo +daughter +envoy +adopt +tankard +smoke +loincloth +bunker +anarchist +envious +sanatorium +infield +spigot +thrust +hindsight +total +bra +plot +plow +plop +sweater +gloss +ploy +insult +plod +knoll +beeswax +solicit +award +yard +tariff +overrun +word +err +crest +work +grovel +tinder +era +elbow +spendthrift +quiver +serpent +flunk +impair +liter +chameleon +sever +moron +disappoint +beach +pizza +fever +lad +ladder +lag +lab +lay +law +arch +cosmonaut +retort +greet +greek +green +south +worst +order +greed +salon +gumption +devote +muffin +misconduct +mayor +sheaf +avocado +valor +carton +shear +then +fragment +safe +break +band +bang +coffer +overprint +tzar +bank +bread +crock +gallows +lisp +iguana +schemer +transient +prawn +sled +flock +slew +hostel +burlap +network +diesel +fellowship +amethyst +marigold +barrier +veto +standard +stencil +lollipop +morass +drench +ticket +maniac +raisin +flawless +renew +sprig +regress +vanquish +kin +render +system +hamstring +chopper +disembark +comic +overs +neck +upshot +tourniquet +kiwi +emblem +luncheon +cereal +rebuff +minibus +guild +target +tavern +hike +medley +iota +guilt +iron +minus +pessimist +lull +ponder +strength +realm +widen +silversmith +latter +hamper +transmit +curfew +maiden +boxcar +sue +negro +phase +proverb +grave +deacon +swamp +bracket +aunt +rickshaw +oppress +mitten +crust +boyhood +nephew +toast +geyser +layman +geologist +predecessor +do +ardor +ecologist +roundabout +slack +rebirth +runt +rune +rung +crucifixion +steak +steal +steam +ghoul +reread +misdirect +christian +goulash +pastel +gentleman +cellist +contraband +drawl +accord +unfold +kitchen +cop +cot +cow +brat +excrement +ill +cob +brag +cod +cog +bran +coo +con +emporium +eyesight +tone +spear +royal +trunk +nonconformist +infirm +speak +charisma +scarecrow +warmth +leech +baud +hacksaw +millionth +hoist +spellbind +gracious +physician +inhibit +gnu +launcher +air +aim +ail +abrupt +thrash +aid +stink +have +sticker +sting +throat +brake +cone +hebrew +uplift +stint +descent +perform +descend +decibel +wheel +raid +fuss +nil +swell +hang +evil +hand +fuse +nip +nit +scenario +drip +ragged +client +mamma +kinship +indigo +photo +victim +extol +thyroid +exalt +shout +cognac +board +zillion +righteous +plasma +intercom +fusion +boxer +cape +retreat +cooler +night +flatter +born +rile +flatten +bore +orchid +cede +humor +peek +peel +pose +confer +peer +peep +chafe +foreskin +chaff +diner +coral +visa +banker +horizon +cherish +gingerbread +octopus +croak +faint +dilemma +tetanus +float +profession +bound +curios +sedan +loin +beet +piggyback +wag +bookend +wad +frill +sovereign +fight +gybe +way +wax +burro +war +fizz +peninsula +holdup +boon +true +reset +absent +nursemaid +smidgeon +maximum +crystal +veterinarian +emir +emit +aorta +flat +abstract +molt +flaw +postscript +subsist +prayer +cacao +face +mold +mole +stake +shrine +test +upholster +unwilling +frolic +shrink +heyday +chairperson +hemophilia +faze +affidavit +loyal +longshoreman +igloo +concept +matron +consul +fulcrum +datum +horseback +supplement +toothpick +varnish +grape +zone +mallet +flask +graph +hump +flash +manicurist +glad +rhythm +tusk +terror +idealist +southwest +brown +congest +kitten +blast +brows +ophthalmologist +gun +gum +gut +guy +diarrhoea +upper +brave +regret +bravo +thinker +cost +helpless +tempest +cargo +appear +economist +menthol +medal +havoc +uniform +tarantula +appeal +caveat +genes +gawk +jester +disclaim +goldfish +teacher +buck +merriment +fogey +precursor +plotter +eke +disavow +trial +convertor +pillow +bolt +extra +paragon +keeper +marker +firearm +market +streetcar +prove +subvert +live +matador +club +cluck +clue +logarithm +prepay +graphic +slogan +car +cap +caw +cat +meow +can +cab +heart +hears +chip +sake +bridesmaid +abort +chin +chic +serum +bankrupt +freezer +write +lobe +storeroom +criterion +entrench +afternoon +product +dive +southern +bawl +motorway +pave +drastic +flourish +crepe +grandson +explicit +offend +barnyard +forfeit +haircut +ledger +brain +nitrogen +cold +braid +ethic +willow +theorem +window +artisan +factual +tiara +gizzard +nought +halt +fling +nod +rake +overcrowd +dishearten +hale +half +recap +courtship +taillight +provision +discuss +halo +wont +concerto +servant +drop +domain +supplant +year +operand +wavelength +happen +album +accomplish +space +thirst +rational +thong +carp +cart +virtuoso +quart +rebel +marina +prospector +card +care +fungus +tomahawk +british +honest +nonprofit +profess +blind +madam +blink +rink +rind +ring +drove +tomorrow +size +sheep +sheer +sheet +silent +bookmark +breed +callous +traction +checker +tragic +heartburn +friend +pomp +courier +that +peck +scalpel +rugged +recruit +magnesium +optimist +extinguish +angel +slay +slat +premier +slap +racetrack +slam +anger +breakfast +recover +slab +upbeat +veteran +shore +snout +siesta +begin +prick +halibut +price +foothold +dream +tooth +aerosol +washcloth +fifth +ground +gnat +snack +ratio +stair +proportion +jolt +stain +juror +shrill +pumpkin +cannon +loath +stroll +leather +thermal +husband +druggist +concert +burst +spore +whitewash +unfit +staunch +sport +incisor +concern +crawfish +glaze +complexion +import +clench +pluck +blame +broil +impromptu +whisker +guffaw +pertain +priestess +temper +aura +comet +evict +adroit +dispatch +exploit +semicolon +lioness +harmless +rebuild +toss +sisterhood +textbook +bloodhound +crumb +these +trick +scum +cherub +fool +marksman +zenith +mucus +soil +agnostic +laggard +bias +eras +bestial +beaver +waterway +petunia +helium +develop +media +pester +poetic +document +sweeper +finish +thunderbolt +foal +foam +cymbal +fruit +volley +trawler +smelt +quartz +theater +framework +patchwork +demean +taxi +livestock +fester +battleship +typhus +neon +touch +speed +death +refurbish +treatment +baloney +momentum +lade +ream +hover +frown +spectacular +larva +read +ruler +swig +leapfrog +earnest +detract +stunt +execution +reap +hovel +rear +postcard +incest +roll +engross +oblivion +output +downward +falsehood +laugh +verbal +landslid +squirm +garland +putter +cleanser +deficit +squirt +wheat +sadden +throb +sixth +tuition +strainer +bazaar +throw +comparison +placard +hiatus +chop +fell +wolf +parson +chow +ruff +assassin +processor +heater +outlook +earring +watchdog +your +restless +stare +grater +log +area +start +stealth +low +lot +wigwam +groan +pitcher +rump +posterior +recoil +omelet +lymph +thaw +corduroy +wholes +hire +fraud +default +bucket +draftsman +cornmeal +gibberish +charlatan +scanner +pickax +sheath +mover +antenna +housework +valid +colt +you +houseboat +poor +polar +poop +peat +pear +peas +overreact +podium +peak +pool +fiscal +assert +moonlight +repay +forswear +mourner +skeleton +breadth +groundwork +angler +month +unrest +bequeath +carpet +corps +gymnast +foster +spearhead +fervor +fountain +washroom +horror +verb +minded +heaven +ceaseless +saboteur +tendril +blackout +smock +homonym +resound +exodus +casual +bomber +protractor +lurch +milk +turmoil +vet +excess +strong +arena +divisor +noticeboard +outgrowth +vegetarian +whine +soldier +amount +base +trainer +put +haemoglobin +seventeenth +taker +helper +pup +titbit +assessor +chestnut +yuck +suction +grill +nine +parasol +transcend +pusher +boycott +archipelago +tract +phrase +magenta +frostbit +sheikh +warhead +spreadsheet +lyre +snippet +reject +gash +circuit +rude +sneak +denial +gasp +reelect +undergo +figment +overnight +ego +dread +egg +lynch +earthworm +help +reservoir +slouch +auburn +reclaim +preempt +soot +helm +hell +clarinet +limelight +prowler +lateral +heron +astonish +forbear +dim +food +musket +terrain +vomit +foot +stopper +holiday +payer +twelfth +bless +radial +trailer +pamper +thirteenth +talisman +event +magnet +vertigo +wedlock +teak +publish +eardrum +sustain +shrivel +outrun +ass +pun +drink +bass +dirt +dung +dune +cornstarch +reason +heroin +ask +ash +turnout +bask +bash +pus +launch +curtsey +round +bridegroom +caption +liaison +heartbeat +blush +assign +arsenal +demagog +elder +effortless +mist +miss +blossom +minion +station +expand +fingernail +scheme +banana +merciless +slosh +behind +bowel +trapper +boredom +sign +leotard +tunic +lament +ouster +hyaena +bride +bobsled +currant +toward +weapon +phobia +chipmunk +yowl +null +sensual +lid +lie +koala +cave +lip +useless +honeycomb +popular +quota +plunder +mace +watt +smoulder +clear +cleat +succor +clean +skein +blend +humid +phenomenon +cowgirl +flicker +sheik +crayon +copyright +paranoid +scooter +less +ikon +ampersand +waterproof +custodian +outlast +strut +strum +basement +chimney +monogram +fluff +chasten +geld +courteous +cramp +backtrack +grey +close +despatch +bandwagon +haddock +aqueduct +wow +grocer +won +woodchuck +wok +woe +stalk +bettor +wreath +philanthropist +spray +distinguish +zipper +garrison +delimit +eggplant +buzz +vault +protector +mausoleum +onward +oversleep +liken +proton +header +badminton +vessel +catwalk +stamp +damp +nape +damn +threaten +dame +alto +liven +exempt +deter +liver +hobnob +furrow +pact +loom +utmost +look +socialist +governor +rope +pace +while +smart +fleet +loot +loop +pack +petal +hoax +grant +belong +makeshift +discredit +grand +conflict +sham +hallway +optic +dime +bonus +banter +overweight +user +boa +grind +auditor +five +ambassador +chore +abstain +bearer +morsel +tick +botch +pier +carat +march +albino +game +jibe +banjo +optician +signal +manifest +eel +sleigh +sketch +creation +undress +yolk +urgent +impoverish +mustang +clothespin +fundamentalist +gild +simmer +slash +slapstick +run +rum +rub +booklet +benefactor +rug +stem +step +stew +taboo +subtract +rut +discus +shine +faith +pigpen +letup +portico +reappear +aye +congressman +block +foreswear +misbehavior +dude +within +gilt +pentagon +connector +syllabus +palomino +harem +frost +reed +womankind +reef +reek +reel +dull +skulk +swagger +chiropractor +ringlet +foresight +similar +psychopath +kidney +straitjacket +nab +sullen +nag +objection +obelisk +nap +department +nay +draw +resign +drag +tundra +drab +formal +horseplay +outing +orbit +depth +bribe +pinion +underbrush +cheerful +go +emboss +dunk +ammonia +compact +aquarium +baron +aria +stave +ameer +shack +geranium +warpath +epitaph +velour +schooner +virtuous +fond +wave +trough +cellular +tenet +nausea +stiff +asparagus +gender +button +hive +verdict +cloister +pilfer +picket +blitz +jump +booth +ardent +languor +cartel +click +poke +wallet +colonel +valet +cell +rotten +experiment +stanch +brooch +fifteenth +quell +weirdo +convert +chant +gent +repel +behead +ricksha +wig +daybreak +danger +win +clout +wit +ligament +infest +gimmick +wiz +cloud +metaphor +snoop +copperhead +crag +crab +cram +expressway +compatriot +mismatch +starter +salad +consort +ride +donut +archer +meet +control +wharf +halloween +glade +skirt +bandana +filament +circular +fare +farm +thunderstorm +canker +foment +corral +scoop +encyclopedia +scoot +agenda +american +cadet +sperm +gunman +hock +brood +broom +brook +walnut +youngster +skater +frond +auto +dike +relentless +snorkel +placid +stout +hands +front +refuel +muff +perfectionist +mode +upward +commonwealth +unwind +chunk +mollusc +seesaw +apartheid +mollusk +special +gallant +armor +confess +fathom +remiss +activist +playground +wick +obsess +umbrella +hopscotch +watermark +jilt +undo +advisor +sneer +princess +shrew +timer +keep +counterpart +elector +keel +mad +blogger +seventeen +bitch +drinker +equinox +dump +wrapper +chintz +attach +attack +jellyfish +final +beard +introvert +punish +feint +noun +plough +piecework +waist +photograph +spurn +cartoonist +beg +bed +bee +discolor +swindler +firework +spurt +bet +are +exhibit +fume +tabu +torment +sundown +portrait +need +border +rotor +bastard +sprinkler +gunner +jaunt +tactic +truck +detector +visor +brand +african +camper +rigor +awe +plumber +eject +spleen +urn +upset +snapshot +businessman +constrain +skunk +affair +indoor +crate +molest +cavort +sheriff +fiberglass +winner +wreak +rash +earner +rasp +gradual +fuel +sulfur +joint +fallout +buyer +endless +gray +tobacco +gust +ordain +topaz +nutmeg +she +contain +recoup +grab +conduct +widow +hardwood +shake +orphan +portend +computer +driveway +equip +portent +unearth +southward +tend +state +lug +tens +antler +tent +bleed +castoff +blinker +keg +bemoan +key +overhaul +thank +sniff +career +admit +spatter +plankton +jersey +christen +tuesday +poem +sari +tread +shaikh +yap +cent +quiz +yam +treat +yak +whisper +poet +fibber +spaniel +nuptial +brunch +debtor +novel +ripen +pandemonium +harden +neuron +hearten +steer +generic +balloon +speaker +northwest +blight +fireman +flesh +absorb +powwow +inbreed +spree +magician +rift +weld +surfboard +lunch +glower +well +drone +welt +underdog +discord +mistaken +dose +distant +laurel +skill +cinder +jackal +dais +ovum +snicker +stratum +possess +warrant +homework +canter +rafter +fate +burden +loss +clown +tablespoons +lose +divest +satirist +rote +page +backlog +shed +glare +twitter +hush +redress +home +peter +competitor +pinpoint +overlay +broad +overlap +hinder +individualist +journal +offset +instinct +smidgen +refuge +freedom +cleans +nightclub +rodeo +dominion +wallop +buzzard +cocoa +pointless +gerbil +snowplow +mastermind +museum +poinsettia +drumstick +mohair +jinx +backhand +cricket +north +gait +admonish +neutral +ho +technician +overflow +ear +eat +he +leper +limit +cello +display +wringer +twist +entreat +contest +meteor +finch +chemist +fodder +star +stay +stag +foil +stab +phosphorus +appoint +sphinx +shunt +broncho +atheist +atheism +portion +pardon +mackerel +demand +unfurl +protest +asian +captain +gamut +swab +swan +sinew +swat +swap +anticlimax +sway +loon +appal +void +vase +smack +govern +affect +hitch +vast +pilgrim +naturalist +vector +washout +whirlwind +quilt +crave +yack +cactus +quill +pander +wreck +orchestra +bikini +spokesman +haze +new +net +maverick +seventh +mew +onus +cardboard +interpret +taper +credit +harass +jamb +permit +prolog +menial +hunch +campaign +bayonet +moral +handlebar +overhear +ore +overheat +calk +overhead +calm +intrust +recommend +type +tell +calf +demon +wart +warp +warn +dogma +warm +pecan +adult +qualm +flotilla +ward +blindfold +confound +rook +room +candlestick +worth +bungalow +headway +root +defer +vodka +give +climax +assent +honey +surveyor +quail +freshen +polio +rib +stockyard +answer +abdomen +plank +coup +fracas +passageway +waterfront +lesbian +guerilla +attempt +third +maintain +capitalist +fetus +deck +keyboard +windshield +furlong +harmonica +crew +better +persist +pass +workout +microfilm +caterpillar +grammar +meat +mistrust +roast +side +bone +luck +caustic +aids +dawn +extract +jell +contend +velvet +gradient +open +crucial +content +reader +linear +whiff +bestow +mistress +needlework +steward +fleck +loud +skinflint +playwright +grade +hoop +hoot +buttock +hook +ditch +hoof +hood +hydrant +acquaint +spinach +historian +enthral +woodwind +brainwash +dwell +inferno +twister +gym +somewhat +gambler +symptom +preacher +affront +keyword +matter +loiter +mink +seep +quench +modern +mind +mine +ginger +seed +seem +churn +mint +unfasten +alibi +desist +chess +sleeper +quarterback +phonograph +chatterbox +regular +condominium +blacklist +don +alarm +impostor +dog +doe +solemn +digress +constrict +consumer +dot +hunger +visitor +probe +syntax +chord +sundial +northeast +explain +jailer +sugar +folder +inventor +edict +patter +smut +stop +coast +pincushion +watermelon +smug +earl +earn +peacock +bay +reload +bad +troop +cower +ban +stinger +linguist +enchant +attest +subject +snuff +scrap +sail +causeway +scram +baboon +warrior +triplet +vitamin +lotion +orthodontist +beautician +cousin +motto +sprinter +pate +typist +height +gusher +aftermath +arsenic +ether +accustom +tint +recur +three +erect +ting +chrysanthemum +trigger +interest +basic +basil +basin +idol +chug +mushroom +suppress +dismiss +deepen +encyclopaedia +unpopular +tank +affirm +tang +near +moratorium +neat +motorist +anchor +spawn +seven +cane +diaphragm +it +shame +jest +in +id +disappear +if +abscess +growl +sap +parish +make +kit +delight +squat +garlic +warden +unicorn +jealous +overt +bequest +kid +butter +romp +smoker +inherit +bedspread +diphthong +left +protocol +just +yen +unfair +psych +human +yes +cretin +yew +legion +character +wretch +save +opt +discreet +background +shoulder +nude +manual +pillar +dean +squander +deal +deaf +maxim +dead +revel +intern +dear +strife +sprawl +pail +collect +normal +councilor +flounder +bullfight +tartar +bold +statistician +burn +blackmail +cottontail +sift +protagonist +burp +burr +tartan +super +innuendo +crucifix +craftsman +commit +marshal +unsay +paunch +chimp +down +lieu +chime +initial +lampoon +editor +fraction +unseat +fork +undergrowth +form +fore +ford +diaper +overburden +analyst +fort +pavilion +whiskey +boomerang +cosmos +propaganda +shin +disciplinarian +classic +covert +sidestep +drive +gland +scrawl +fatherhood +ship +graft +vista +marxist +smidgin +excel +handed +venison +congeal +marxism +sling +faction +handicap +slink +felt +diet +parenthood +journey +reign +stoke +weekend +derail +billion +potato +jacket +gorilla +almanac +teeth +meander +befriend +proletariat +woodwork +skip +skit +invent +adjourn +mild +mile +skim +skin +mill +primer +proletarian +skid +surplus +seasick +misread +depend +swoon +father +countdown +deject +swoop +regatta +unburden +string +yeast +pathologist +merit +join +jettison +stiffen +hoorah +din +stapler +nectar +die +dig +democrat +noiseless +item +dip +blur +shave +thresher +villa +worm +slake +sunup +talker +fillet +suspect +drunkard +shoo +dwarf +dweller +wail +guardian +clerk +makeup +stallion +waif +detest +tangent +deceit +rue +wait +box +boy +cuckoo +shift +bow +dither +boo +raccoon +cyst +bob +nylon +bog +elect +plumb +kayak +surmount +transplant +saki +wealth +perk +visit +vineyard +somersault +sharpen +yoghurt +aspirin +labyrinth +curriculum +downtown +tandem +rigid +savior +effort +gnome +demolish +pageant +moccasin +melodrama +flu +soul +impel +soup +sour +claim +plaza +reflector +predict +agent +drawer +council +craze +pink +purr +arbor +tilt +clever +parch +pine +till +sunday +sword +tile +pathway +pint +map +mar +mat +may +gelatin +membership +mankind +tablecloth +grow +man +relinquish +aimless +hemlock +omen +tale +switch +jail +deposit +talc +unleash +basket +longhand +talk +shield +rabbi +moralist +lyric +pitch +solder +pointer +group +monitor +bedbug +maid +drummer +maim +mail +main +tonic +killer +shatter +minuet +safari +teller +rattler +outweigh +feverish +peasant +careless +rock +hijack +eyelid +latin +bookshop +poker +gavel +unlock +manifesto +girl +stitch +monolog +priest +dutch +blubber +sensor +correct +monster +zombi +vinyl +jaywalk +cough +underwear +waiter +buzzer +thing +registrar +blacksmith +think +frequent +first +lone +crib +long +extrovert +thoughtless +lap +autocrat +escort +daunt +mermaid +anus +yoghourt +memo +broadcast +butt +proofread +tractor +coconut +lick +piccolo +marijuana +dash +comedian +sulk +nazi +sherbert +stopgap +daredevil +acumen +squad +interior +channel +pain +trace +roster +track +acrid +zigzag +whizz +assault +billow +pair +synonym +napkin +typeset +scowl +voodoo +toucan +amir +shop +lexicon +shot +show +cornea +veranda +therapist +shoe +threshold +corner +label +cornet +enthusiast +fend +objector +flapjack +dice +plume +enough +syphon +black +consent +enthusiasm +fiendish +plump +get +straggler +stomp +midriff +slyness +gee +gibber +neckerchief +gem +disinherit +beseech +skull +businesswoman +yield +stupid +nostril +tallow +kernel +sear +eighteenth +seat +seam +seal +stigma +calendar +wonder +puma +parent +limber +ornament +forecast +gage +pump +august +foreword +slingshot +tacit +wednesday +gauntlet +childbirth +tug +tuck +trader +tour +tout +delirium +stretcher +cancer +spank +cancel +tub +mare +underworld +imp +undershirt +mark +mart +workshop +rancher +fiftieth +chalet +graveyard +squash +wake +sound +litterbug +epidermis +slumber +cock +strait +strain +sudden +protein +par +pat +harsh +paw +pay +woodland +same +heartbreak +pad +cotton +pal +pan +exhaust +oil +chloroform +munch +companion +foghorn +polygon +drain +vertebra +soundproof +outdoor +suitor +money +imprint +leeway +aspect +flavor +asthma +godchild +comradeship +forgo +pile +pill +grip +grit +mop +mow +moo +mom +mob +railroad +grim +grin +oxygen +server +chamber +nose +hallelujah +fulfil +sneaker +afflict +witchcraft +ascend +dole +ascent +spasm +gross +confirm +pioneer +inject +gladden +highbrow +linoleum +intravenous +knife +raincoat +broker +squall +bravado +racoon +opium +contagion +roar +island +insect +mixer +thrive +partizan +road +checkup +dagger +coupon +splint +empress +whiten +strip +uptown +skillet +paraphernalia +jigsaw +totalitarian +madden +tycoon +tripod +striker +shroud +hiccup +gore +spice +ember +magnolia +grouch +conqueror +embed +deadlock +affection +deer +deep +fellow +planetarium +deem +file +girlfriend +deed +hound +film +fill +tedious +selfish +personnel +hybrid +repent +drouth +field +prism +astronaut +fruitless +lapel +shelter +gander +unload +burrow +god +gangway +oral +motel +represent +forger +pheasant +forget +founder +suburban +dollar +rebind +zinc +implement +crimson +hideous +premium +parcel +straightforward +scout +scour +fall +bottleneck +pueblo +hinterland +dampen +dictatorship +flyover +neighborhood +clinch +gnarl +burger +zero +cottonwood +lawyer +further +misrepresent +ribbon +dial +skeptic +stool +trinket +stoop +plush +movement +girlhood +malaria +intrench +twang +mule +ranger +beacon +bigamist +capacitor +search +stupor +margin +airport +chipper +chieftain +narrow +fatten +quotient +wizard +caravan +transit +sadist +sadism +establish +dachshund +hobgoblin +eye +score +distinct +two +splash +libel +furor +wiper +diamond +brisk +opportunist +particular +disfavor +nineteen +town +hour +cluster +fast +dew +remain +paragraph +den +abandon +stubborn +shark +buttress +onslaught +share +sphere +minimum +rainstorm +attain +junket +sharp +botanist +siren +awkward +comfort +rapport +stir +bleat +whopper +blacken +blood +bloom +chute +coax +orchard +coat +doctor +spiteful +electron +blunder +mislead +coal +sect +infant +setback +radar +dough +lava +suffer +hundredth +sodium +bosom +late +speech +clamor +lath +lookout +goof +good +goon +detour +frigid +compound +detach +complain +bombard +headroom +countersign +token +monsoon +clamp +harm +hark +mental +hare +hard +beret +banquet +connect +fist +callus +hart +orient +harp +flower +creditor +trooper +pigeon +seaport +granola +print +foreground +assist +cockroach +pleasant +gig +faucet +prophet +omit +wither +pure +corkscrew +copper +perturb +barbarian +shoal +cups +jabber +razor +construct +paint +leash +statement +mama +hummingbird +catapult +pare +park +selector +glycerin +dentist +part +pars +youth +totter +plead +hangout +cistern +blanch +mountain +cardigan +couch +onset +build +zucchini +flute +salmon +chart +most +charm +moss +eskimo +organist +humanitarian +mammoth +pennant +squelch +weigh +standoff +sector +sparrow +fine +find +giant +merger +nervous +ruin +fiend +boulder +prowess +paperweight +cholera +express +ferret +cheapen +batter +breast +theft +silk +pellet +restart +silo +huff +common +archaeologist +printout +vine +lion +overeat +tender +expert +burner +myriad +stowaway +subscript +hypochondria +premiss +egotist +complement +figurehead +mailbox +pagoda +aircraft +sultan +archway +annual +foreign +point +smother +newborn +pamphlet +dancer +esophagus +platinum +pocketbook +secret +amnesia +reformat +finalist +ram +gay +gas +gap +holler +gal +understand +gag +chatter +gab +bile +politician +metro +solid +bill +holocaust +crutch +fun +lingo +manner +mystic +astound +rancor +eczema +ingrain +anaesthesia +sociologist +dishonor +ewe +seminar +corridor +neutron +itch +leopard +yesterday +moment +stripe +unveil +timid +task +werewolf +withdraw +landmark +grid +recant +spend +howl +darn +shape +snot +timber +rundown +impetus +cut +cur +pollster +snag +forbid +cue +punter +cub +snap +bridal +easter +brainstorm +bin +squawk +rebound +bib +judgement +redeem +bit +knock +disrepair +blemish +flue +fagot +flux +bamboo +foolish +walrus +sequin +transgress +often +back +impeach +extremist +mirror +lightning +scald +scale +culprit +pet +pelt +pew +pep +pen +scalp +lard +lark +peg +pea +patient +fed +megaton +constraint +oatmeal +drama +catnip +pediatrician +offshoot +obstetrician +gambit +maelstrom +tiff +clack +lesson +jockey +few +doll +errand +camera +handbook +forward +nougat +sideshow +showman +switchboard +calico +lifeguard +planet +jumbo +azalea +constant +flow +possessor +lye +curd +cure +curb +curl +prevail +stagecoach +leaflet +crypt +underweight +cellar +lend +tablespoon +papa +lens +cater +desert +statesman +mantel +notion +uterus +anguish +caribou +stroller +seaman +golfer +strew +parrakeet +peanut +welter +mower +rudder +compost +blaze +atlas +gravel +queen +dessert +rhyme +claustrophobia +surgeon +molar +verandah +knight +shock +crow +queer +crop +append +power +junior +anthem +access +clipboard +bachelor +intercept +sink +sing +roof +bode +implicit +remark +talent +conceit +resurrect +weekday +climb +honor +blizzard +liqueur +talon +oval +scandal +gateway +sermon +lime +patron +asteroid +butler +charcoal +trait +kiosk +thatch +trail +train +armadillo +harvest +fan +account +tunnel +carrot +obvious +smear +parkway +unread +fetch +employe +truism +sanitarium +teamster +boney +spruce +serial +contempt +hangar +lamb +lame +lamp +forest +goner +stock +roam +leukemia +bluff +terrier +fray +drape +bind +guru +liner +linen +chief +poacher +furious +furnish +disarm +meter +bunch +marshmallow +decorum +labor +kindergarten +heroism +willing +marsh +dad +junction +dab +dam +spell +swordfish +mention +courtroom +sonata +day +strive +flail +snowdrift +thrill +slacken +cider +memoir +sawdust +disregard +flair +thwart +jailor +jugular +pivot +cupboard +lentil +salesman +hippopotamus +matt +defend +rev +repress +stub +mate +barley +stud +smog +stun +red +franc +frank +hanker +fourteen +salami +likelihood +afterward +squadron +indent +mortar +skateboard +yarn +mortal +workbook +retain +retail +waitress +suffix +overshoot +ethnic +sack +brute +whoop +puppet +guidebook +vandal +pauper +ancient +monkey +bologna +laps +vulgar +vagina +hexagon +scant +liquor +cabin +sixteenth +gear +eavesdrop +bulldog +smolder +forethought +springboard +nun +bodyguard +prune +shrapnel +shampoo +linchpin +lover +anthropologist +tide +cavern +pedlar +countryman +waken +optimum +mix +parka +spinster +meek +dryness +hazel +eight +clamber +handbag +hoodwink +transcript +payment +gullet +gulley +gather +request +absurd +rendezvous +occasion +thicken +recess +kite +text +hamlet +traitor +industrialist +sidetrack +portfolio +floodlight +thicket +staff +communism +scorpion +madman +prolong +resubmit +satan +oppressor +communist +inferior +equilibrium +gumdrop +starch +beat +rush +bear +beam +bean +october +beak +bead +organ +ashtray +nutriment +eyebrow +motherhood +mascara +conform +showdown +infidel +racket +interview +reform +pattern +nebula +brunt +hammock +progress +tailspin +sorrow +stratagem +deliver +blackbird +boloney +exclaim +instant +joke +equal +kosher +swim +swallow +highland +guerrilla +glorious +wear +comment +vent +denim +overcoat +commend +vend +harpoon +manhood +citizenship +copier +newscast +gaze +teamwork +gulch +curtain +curtail +hyacinth +juggler +censor +goddess +bulk +bull +bulb +skew +carburetor +cypher +plain +homey +bray +kinfolk +bicker +dissent +squid +blimp +creak +prose +partner +inspector +lynchpin +portray +whirl +grinder +matchbook +defiant +anorak +tumbler +infer +whirr +tighten +pockmark +sauna +ion +grandstand +sunburn +judgment +retard +center +builder +pickpocket +thought +starboard +usual +coaster +humdrum +fingerprint +storey +clinic +interim +surpass +tough +earshot +flashlight +tong +flee +lupin +lake +bench +add +citizen +ado +crossroad +ravel +match +raven +cantaloup +punctual +newsstand +dryer +insert +flamingo +like +success +sofa +journalist +heed +arraign +chick +soft +heel +outfield +propel +fuze +hail +hair +convey +proper +paddock +novelist +shrug +shrub +slide +tureen +regain +pepper +hose +slight +host +panel +beaker +actual +socket +flake +preen +toadstool +pickaback +discard +tomb +tome +snitch +chronic +guard +esteem +custard +underpass +glimmer +gene +maze +globe +buy +bus +coke +sequel +but +bun +bum +bug +bud +embargo +woodsman +wise +ecosystem +debrief +flip +wisp +wist +trapezoid +condiment +plutonium +pin +garter +domino +circus +pie +pig +pit +campus +gush +oaf +cashew +oak +detail +virtual +detain +sewer +oar +redden +dresser +wallow +nutrient +godsend +yelp +baker +jab +hiker +pupil +yell +cookbook +vermin +sleek +sleet +sleep +liar +hate +trolley +sallow +tweet +glider +under +tweed +pride +merchant +lure +risk +rise +lurk +jack +confetti +anemia +school +parrot +enjoy +overdo +cracker +almond +direct +nail +street +monorail +ransack +blue +hide +worsen +poison +beater +supplier +dashboard +wink +even +pontoon +studio +path +crossbow +enrol +connoisseur +forum +ravish +auction +settler +mentor +midway +blowtorch +stray +straw +strap +cassino +would +phlegm +bayou +asset +spike +preview +musk +mush +saber +muse +grief +phone +muss +pouch +must +shoot +hutch +ma +ms +mr +machinist +fortress +quarrel +loosen +joyous +hemoglobin +dolphin +mayhem +attract +end +trill +keen +bunk +vagrant +rhinoceros +shred +toxin +gate +ancestor +dialect +moisten +kilowatt +mess +lump +mesh +sparkler +parallel +stronghold +splendid +spout +patent +enter +vapor +hedgehog +fetter +deform +clapper +sprout +over +bleach +mallard +oven +caster +digest +forehead +theologian +womanhood +comprehend +tramp +drawback +fade +croquet +tourist +plaster +roost +knighthood +monarch +rental +gloom +chuck +choir +prohibit +hanger +unscrew +gymnasium +poncho +truant +saturday +depress +goo +lair +dinosaur +nitwit +psychic +tonsil +gob +emphysema +nite +washer +resistor +carcass +rail +free +rain +acrobat +fret +harpist +ritual +filter +hopeless +soda +rang +accent +puck +rank +restrict +rant +sober +toy +their +sarcasm +top +tow +tot +fiction +ton +duress +toe +urban +murder +overdraw +tool +hearth +embellish +solicitor +toot +incur +western +nonpartisan +lather +prong +flame +mirth +countess +rag +donkey +fashion +handkerchief +ruckus +taint +raw +rat +rap +protract +spade +ray +snow +thorough +contact +hatch +snob +cleft +extravert +quicken +rider +evangelist +shallow +milkman +coil +coin +glow +interject +flop +metal +freeway +policewoman +flog +yank +chariot +bait +endear +saga +alight +random +sage +dupe +radio +rector +earth +bail +shellfish +spite +stanza +disgust +axiom +waltz +gees +watch +fluid +ultimatum +report +reconstruct +noon +spokesperson +egoism +public +erupt +pacifist +pummel +habit +wrest +nut +resist +corrupt +hourglass +mull +mud +mug +finger +mum +approach +wean +weak +contort +boss +toothbrush +southeast +larynx +devour +devout +censorship +newt +protect +irregular +fault +papyrus +facet +elf +smuggler +trust +bingo +bathroom +beef +legend +beer +spread +communion +loft +bladder +uncommon +craft +spearmint +catch +snipe +teapot +misfit +lessen +thousandth +referendum +pyramid +handrail +broth +lollypop +exterior +suggest +wound +overstep +utilitarian +complex +papaya +screw +pick +deflect +suburb +portal +postmark +tassel +ocean +mother +bough +bugger +rodent +shorthand +enlighten +elk +elm +moonbeam +flutist +kelp +misprint +teetotal +upturn +ramrod +dismount +quicksand +spanner +authorship +roach +befit +rumor +apart +ditto +gift +zeal +contradict +hunt +dishonest +zoom +mongrel +hunk +mosquito +hunchback +sanction +excerpt +curio +accost +usher +indirect +intellect +doorstep +nobleman +cooper +combat +letterhead +ice +rhino +newsprint +skylight +convict +christmas +splendor +cord +core +khaki +brawl +corn +brawn +cork +discount +shuck +plug +census +cowboy +plum +choke +surround +caulk +dinner +plus +alga +duke +abet +civic +civil +bath +engulf +cafeteria +art +scamper +transform +sunlight +forbad +virgin +gin +head +medium +amateur +heal +stereo +heat +hear +heap +raft +counsel +muster +bargain +bide +latch +adorn +trim +trio +forearm +cobbler +trip +no +tit +when +junta +tin +whet +tie +implant +depot +pseudonym +evergreen +cleric +toad +geneticist +bullet +navel +yacht +withhold +fasten +backward +coach +impression +rob +rod +focus +livelihood +snip +yokel +rot +discern +environment +aplomb +melon +prop +coop +impend +plantain +cook +cool +looney +level +brother +encroach +quick +lever +pork +drier +trend +bullfrog +pore +inland +voucher +takeoff +bake +port +colic +hymn +choral +postman +spire +theist +thresh +tormentor +humorist +water +fluke +entertain +witch +twentieth +tire +boast +catnap +blotch +cinnamon +prude +weird +tweak +brighten +touchdown +post +panacea +concoct +scan +handler +prey +today +chapter +conductor +altar +cashier +drown +dismal +inhabit +judo +conceal +flagship +hullabaloo +fauna +laughter +streak +overpass +sandbag +trump +stream +despot +stroke +cube +hydrogen +bigot +dress +vital +fourth +dope +ballroom +fascist +clone +scoff +fascism +birthday +apprenticeship +eighth +repeat +classroom +twitch +liquid +inform +reaper +lagoon +superscript +refund +rye +midnight +blare +worship +thermostat +apex +platform +farmer +meridian +cutter +underneath +conquer +fern +rescind +wagon +term +name +realist +opera +bunion +bullion +realism +ailment +torch +zebra +distributor +hysteria +hacker +concur +profit +middleman +gram +clover +hull +hulk +flyer +tuner +flare +highjack +motion +turn +place +swine +swing +turf +preach +childhood +origin +pelican +feign +suspend +insist +scollop +bobcat +array +peddler +given +afterthought +district +opus +trillion +plastic +assort +white +hue +hug +hub +cope +season +hut +enigma +naught +grunt +holder +wide +bewitch +spokeswoman +oath +powder +rend +froth +pro +ani +ant +rent +dragon +stolid +marathon +ideal +blunt +surf +sure +aspen +equestrian +tornado +freshman +librarian +bluebird +icon +latex +tendon +annul +seafood +later +koran +readjust +senior +slope +perch +convoy +cheat +cheap +trespass +hack +broach +hustler +trot +woolen +gulf +genius +gull +shimmer +crime +gulp +woof +wood +deign +wool +entrant +viewpoint +lighten +jazz +festoon +tailor +lighter +dye +homestead +reveal +aluminum +workman +joker +dumfound +bison +picnic +pane +vizor +prowl +optometrist +detect +crooked +review +spoons +hiss +smokestack +caucus +fearless +guitar +coma +comb +come +zodiac +isthmus +reaction +superstar +region +quiet +contract +railway +penal +adjoin +color +armchair +pot +period +pop +pole +colon +polo +pod +poll +runaway +turkey +hobo +schoolboy +tiger +padlock +hernia +careful +spirit +robber +pilot +case +shaft +amend +mount +cash +cask +cast +mound +ventriloquist +vest +exult +clutter +helmet +projector +author +alphabet +fender +bowl +check +macaroni +catfish +bellboy +hermit +week +sang +applaud +nest +driver +weed +director +petticoat +lute +puke +vowel +muffler +weep +cartoon +ranch +relief +model +reward +sinner +clod +clog +tip +kilt +ninth +clot +lavish +violent +kill +kiln +kilo +polish +satin +blow +blot +hint +rose +regent +except +blog +bloc +blob +hind +scrapbook +disrupt +impound +kingdom +blowout +sandman +mugger +towel +bracelet +snort +friar +tower +node +deduct +wombat +interlock +canteen +slice +mood +slick +legal +moon +moos +teardrop +moot +heir +porter +metropolis +quit +unmask +slaughter +quip +ok +oh +of +jeer +shrimp +pistachio +karat +stand +ox +doze +accredit +amber +tribe +vicar +polka +garb +spinal +forewarn +feudal +whaler +there +racism +strict +racist +valley +fish +gibe +relic +jug +regard +cabinet +castaway +strenuous +jut +terminus +feeler +grasp +grass +toilet +ruffian +cinema +frighten +lichen +encompass +bishop +incorrect +abyss +fiddler +heather +idiot +diarrhea +rubber +idiom +heathen +trash +stalwart +championship +symbol +cove +nucleus +serious +brass +wife +invest +derrick +treason +apparel +platter +all +lace +duplex +lack +spacecraft +disc +dish +follow +settlement +titter +wanton +thursday +program +neglig +woman +song +fat +roe +psychologist +retract +ultraviolet +awful +dapper +fad +induct +stimulus +list +trench +align +flick +ten +tea +tee +rate +design +chalk +what +sub +sun +sum +whimper +rascal +brief +overload +crush +version +pulpit +intersect +row +womb +lacquer +pumpernickel +backer +goodby +thrift +misinterpret +heifer +jogger +cataract +haemophilia +murmur +snug +snub +herring +proceed +tarpaulin +wield +hurray +rustic +quash +inlay +garnish +hurrah +minor +ladybug +wretched +flap +mire +protestor +stutter +flit +flag +stick +mellow +chaplain +berth +wrestler +plagiarist +searchlight +sunscreen +pond +court +goal +goad +goat +sandwich +okay +algebra +headrest +embalm +reflect +catalog +numb +short +ricochet +tsar +shade +waylay +mission +scientist +flaunt +reconnect +pretext +stride +islam +thirtieth +style +glide +pray +inward +wilder +abbey +mattress +resort +airstrip +bout +soccer +might +alter +return +hunter +underground +abacus +mathematician +liniment +policeman +refresh +tactless +friendship +weight +needless +duchess +falsetto +expect +inflict +wager +alcohol +disquiet +hilt +dugout +loll +health +hill +shipment +fiber +solvent +friday +differ +effect +disinfect +octagon +physic +teach +sidewalk +jew +blister +thread +threat +bushel +feed +dine +feel +sailor +revolution +least +blank +cigaret +idea +moan +script +gourmet +interact +grime +stork +swarm +storm +moat +syrup +store +mainstay +imperfect +option +hotel +fidget +king +kind +vial +kink +stall +cuff +foreleg +stale +restful +amass +cleaner +exert +strengthen +shrewd +bookworm +gale +gala +gall +remodel +smallpox +toughen +bacteria +chairman +donor +pianist +buff +gill +foreman +rapist +reckon +english +reach +react +nothing +quorum +hyena +amphibian +saloon +notch +scaffold +asphalt +memorandum +felon +font +anvil +firewood +betray +hip +shepherd +hit +deaden +reprint +him +adulthood +snowstorm +forego +stump +martyrdom +arc +bare +bard +bark +ark +arm +barn +blurt +parchment +various +plywood +nincompoop +solo +muslim +sole +outfit +succeed +inertia +orangutang +blazer +bandit +context +bond +cynic +sloth +flier +distress +chaperon +sweet +wastebasket +sweep +weasel +rave +shaykh +bolster +dub +overlook +dud +due +buttermilk +pa +watchword +brick +pi +flight +quintet +dropout +marihuana +cinch +temperament +instructor +heighten +toga +shove +batch +pitchfork +kick +behavior +incognito +lodger +bluegrass +sirup +rip +shamrock +rim +frantic +rig +rid +reprogram +chauffeur +shirt +kimono +viola +shirk +sliver +straightjacket +restraint +painless +throwback +cement +birch +robust +knack +lower +earmark +cheek +cheep +cheer +pollen +facial +vigilant +cabaret +continent +tablet +contractor +plateau +tuxedo +complaint +vendor +foreshadow +awaken +confront +uproar +distrust +breeder +hallmark +play +global +litter +wonderland +butterscotch +saucer +prow +seller +prom +prod +sag +perpendicular +tinker +raider +vivid +cautious +undertow +yawn +ordeal +militia +dialog +tomboy +conquest +momma +piteous +holster +vagabond +stench +canal +pundit +question +swill +parsnip +etch +filet +potassium +glamour +cloth +crank +usurp +delta +upright +crane +outpost +penguin +patriot +consist +apricot +caller +peppermint +husk +cartwheel +highlight +dill +freak +dismay +sublet +sagebrush +rainbow +lemon +riot +peach +grouper +saffron +nick +parlor +ferment +bandstand +mock +nice +mustard +chirp +meaning +vigil +vice +ocular +remit +pyre +buffalo +scroll +pervert +lean +alien +dispel +gang +theorist +gold +uphold +floss +breach +sniper +materialist +toenail +spool +spoon +spook +spoof +harlot +outdo +pleas +pleat +trawl +procession +fold +reunion +acid +folk +sandstorm +outsmart +acronym +relent +kangaroo +gloat +miser +cyclist +barb +survivor +guarantor +orangutan +armpit +shovel +duct +ensign +apt +volt +motor +duck +thick +redo +ape +use +fee +fen +frog +germ +modicum +fez +sort +parliament +porch +musician +impress +sore +rabbit +recount +penis +sculptor +annoy +topic +augment +critic +lumber +executor +proof +bittersweet +tap +tar +tax +villain +tag +condescend +tab +spa +silt +tan +rape +counterfeit +sir +sip +scuff +sit +tamper +six +outclass +occur +sic +carrier +goldsmith +toddler +panic +sin +defeat +tension +lesion +attend +tact +hazard +discomfort +tack +wrist +taco +footpath +aftereffect +light +arduous +schoolchild +sailboat +stamped +minnow +damsel +accompanist +hemp +tyrant +badger +glen +superior +inlet +sill +glee +nostalgia +flank +restrain +glisten +turban +redhead +bye +flex +crash +citrus +flour +flout +emerald +flea +republican +investor +successor +easel +footstool +profound +edit +feast +fuzz +trap +blacktop +cocoon +tray +lilac +mincemeat +interplay +our +proclaim +out +semen +tabloid +cocktail +sentiment +frontier +vehement +disarray +clatter +impart +plural +proviso +planner +utensil +tenement +pendant +gospel +tenant +greenhorn +tanker +zoologist +rivet +uproot +embryo +sew +bouquet +echo +bonnet +eleventh +synagog +salient +droop +unknown +galley +snore +anaemia +drool +boil +tidbit +shell +shelf +transistor +woo +diminish +persecutor +goblin +institution +kickback +frugal +brazen +yodel +laughingstock +clip +fowl +splatter +flunkey +blip +footwork +outstrip +disjoint +pallor +catholic +clove +rout +outward +bagel +lope +divert +trivia +pharmacist +divers +clash +petrol +siphon +filch +fortieth +class +clasp +fang +dens +dent +pipe +vernacular +gain +son +stove +sonnet +utter +chicken +feat +winch +dandruff +rioter +herald +piano +local +counsellor +vigor +sued +skimp +plaintiff +spud +watercolor +barter +bronco +spur +rite +ghetto +bisect +compliment +ascertain +sediment +view +unison +workbench +ebb +expel +hymnal +distract +violet +still +closet +superb +favor +viper +crude +torpedo +avow +jot +exam +amen +joy +foetus +job +spoil +jog +swift +memento +lifeboat +april +grain +commando +wall +hyphen +walk +respect +unclean +decent +trademark +tutor +reindeer +mike +nickel +cypress +penmanship +dearth +overturn +present +kerchief +corset +wilt +vanilla +priesthood +will +fingertip +wild +whirlpool +layer +mutant +motif +apprehend +rooster +lightweight +thug +thud +whore +headlight +cross +member +pediatrist +inch +grandeur +slave +diploma +outcast +beast +student +pedal +whale +collar +gutter +masochist +overwork +scissor +twirl +flint +outgrow +bandanna +rocker +cameo +rocket +camel +boot +wren +obtain +replenish +biologist +daub +distend +smite +now +panther +drunk +smith +hall +book +ski +enact +knob +sick +myth +know +knot +press +redesign +doughnut +loser +cutlet +vortex +clutch +exceed +setter +flagrant +birthmark +demeanor +growth +export +leaf +lead +leak +miner +leap +belt +leader +trout +obey +slur +mitt +slut +slum +pasta +mite +slug +throne +pike +throng +rare +linger +column +biscuit +fear +swear +sweat +udder +emperor +owl +outset +own +owe +weather +champ +brush +billfold +gape +rowboat +van +platoon +transfer +spiral +grotto +cliff +vat +nourish +catsup +unwrap +saunter +mutter +brassier +assail +tomcat +daffodil +nightgown +record +cake +faggot +maroon +boardwalk +abbot +counteract +limb +squirrel +mutual +glint +boor +percent +other +boom +branch +cutthroat +junk +mulch +june +squeak +squeal +extort +jewel +gynecologist +vane +sash diff --git a/libs/pyutil/fileutil.py~ b/libs/pyutil/fileutil.py~ deleted file mode 100644 index e37eb792..00000000 --- a/libs/pyutil/fileutil.py~ +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -""" -Futz with files like a pro. -""" - -import errno, exceptions, os, stat, tempfile - -try: - import bsddb -except ImportError: - DBNoSuchFileError = None -else: - DBNoSuchFileError = bsddb.db.DBNoSuchFileError - -# read_file() and write_file() copied from Mark Seaborn's blog post. Please -# read it for complete rationale: -# http://lackingrhoticity.blogspot.com/2009/12/readfile-and-writefile-in-python.html - -def read_file(filename, mode='rb'): - """ Read the contents of the file named filename and return it in - a string. This function closes the file handle before it returns - (even if the underlying Python implementation's garbage collector - doesn't). """ - fh = open(filename, mode) - try: - return fh.read() - finally: - fh.close() - -def write_file(filename, data, mode='wb'): - """ Write the string data into a file named filename. This - function closes the file handle (ensuring that the written data is - flushed from the perspective of the Python implementation) before - it returns (even if the underlying Python implementation's garbage - collector doesn't).""" - fh = open(filename, mode) - try: - fh.write(data) - finally: - fh.close() - -# For backwards-compatibility in case someone is using these names. We used to -# have a superkludge in fileutil.py under these names. -def rename(src, dst, tries=4, basedelay=0.1): - return os.rename(src, dst) - -def remove(f, tries=4, basedelay=0.1): - return os.remove(f) - -def rmdir(f, tries=4, basedelay=0.1): - return os.rmdir(f) - -class _Dir(object): - """ - Hold a set of files and subdirs and clean them all up when asked to. - """ - def __init__(self, name, cleanup=True): - self.name = name - self.cleanup = cleanup - self.files = [] - self.subdirs = set() - - def file(self, fname, mode=None): - """ - Create a file in the tempdir and remember it so as to close() it - before attempting to cleanup the temp dir. - - @rtype: file - """ - ffn = os.path.join(self.name, fname) - if mode is not None: - fo = open(ffn, mode) - else: - fo = open(ffn) - self.register_file(fo) - return fo - - def subdir(self, dirname): - """ - Create a subdirectory in the tempdir and remember it so as to call - shutdown() on it before attempting to clean up. - - @rtype: _Dir instance - """ - ffn = os.path.join(self.name, dirname) - sd = _Dir(ffn, self.cleanup) - self.register_subdir(sd) - make_dirs(sd.name) - return sd - - def register_file(self, fileobj): - """ - Remember the file object and call close() on it before attempting to - clean up. - """ - self.files.append(fileobj) - - def register_subdir(self, dirobj): - """ - Remember the _Dir object and call shutdown() on it before attempting - to clean up. - """ - self.subdirs.add(dirobj) - - def shutdown(self): - if self.cleanup: - for subdir in hasattr(self, 'subdirs') and self.subdirs or []: - subdir.shutdown() - for fileobj in hasattr(self, 'files') and self.files or []: - if DBNoSuchFileError is None: - fileobj.close() # "close()" is idempotent so we don't need to catch exceptions here - else: - try: - fileobj.close() - except DBNoSuchFileError: - # Ah, except that the bsddb module's file-like object (a DB object) has a non-idempotent close... - pass - - if hasattr(self, 'name'): - rm_dir(self.name) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - try: - self.shutdown() - except: - import traceback - traceback.print_exc() - -class NamedTemporaryDirectory(_Dir): - """ - Call tempfile.mkdtemp(), store the name of the dir in self.name, and - rm_dir() when it gets garbage collected or "shutdown()". - - Also keep track of file objects for files within the tempdir and call - close() on them before rm_dir(). This is a convenient way to open temp - files within the directory, and it is very helpful on Windows because you - can't delete a directory which contains a file which is currently open. - """ - - def __init__(self, cleanup=True, *args, **kwargs): - """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """ - name = tempfile.mkdtemp(*args, **kwargs) - _Dir.__init__(self, name, cleanup) - -class ReopenableNamedTemporaryFile: - """ - This uses tempfile.mkstemp() to generate a secure temp file. It then closes - the file, leaving a zero-length file as a placeholder. You can get the - filename with ReopenableNamedTemporaryFile.name. When the - ReopenableNamedTemporaryFile instance is garbage collected or its shutdown() - method is called, it deletes the file. - """ - def __init__(self, *args, **kwargs): - fd, self.name = tempfile.mkstemp(*args, **kwargs) - os.close(fd) - - def __repr__(self): - return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name) - - def __str__(self): - return self.__repr__() - - def __del__(self): - self.shutdown() - - def shutdown(self): - remove(self.name) - -def make_dirs(dirname, mode=0777): - """ - An idempotent version of os.makedirs(). If the dir already exists, do - nothing and return without raising an exception. If this call creates the - dir, return without raising an exception. If there is an error that - prevents creation or if the directory gets deleted after make_dirs() creates - it and before make_dirs() checks that it exists, raise an exception. - """ - tx = None - try: - os.makedirs(dirname, mode) - except OSError, x: - tx = x - - if not os.path.isdir(dirname): - if tx: - raise tx - raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning... - -def rmtree(dirname): - """ - A threadsafe and idempotent version of shutil.rmtree(). If the dir is - already gone, do nothing and return without raising an exception. If this - call removes the dir, return without raising an exception. If there is an - error that prevents deletion or if the directory gets created again after - rm_dir() deletes it and before rm_dir() checks that it is gone, raise an - exception. - """ - excs = [] - try: - os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) - for f in os.listdir(dirname): - fullname = os.path.join(dirname, f) - if os.path.isdir(fullname): - rm_dir(fullname) - else: - remove(fullname) - os.rmdir(dirname) - except EnvironmentError, le: - # Ignore "No such file or directory", collect any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - excs.append(le) - except Exception, le: - excs.append(le) - - # Okay, now we've recursively removed everything, ignoring any "No - # such file or directory" errors, and collecting any other errors. - - if os.path.exists(dirname): - if len(excs) == 1: - raise excs[0] - if len(excs) == 0: - raise OSError, "Failed to remove dir for unknown reason." - raise OSError, excs - -def rm_dir(dirname): - # Renamed to be like shutil.rmtree and unlike rmdir. - return rmtree(dirname) - -def remove_if_possible(f): - try: - remove(f) - except EnvironmentError: - pass - -def remove_if_present(f): - try: - remove(f) - except EnvironmentError, le: - # Ignore "No such file or directory", re-raise any other exception. - if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT): - raise - -def rmdir_if_possible(f): - try: - rmdir(f) - except EnvironmentError: - pass - -def open_or_create(fname, binarymode=True): - try: - f = open(fname, binarymode and "r+b" or "r+") - except EnvironmentError: - f = open(fname, binarymode and "w+b" or "w+") - return f - -def du(basedir): - size = 0 - - for root, dirs, files in os.walk(basedir): - for f in files: - fn = os.path.join(root, f) - size += os.path.getsize(fn) - - return size diff --git a/libs/pyutil/iputil.py b/libs/pyutil/iputil.py index b8b96362..cb3e7c02 100644 --- a/libs/pyutil/iputil.py +++ b/libs/pyutil/iputil.py @@ -1,22 +1,12 @@ -# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) -# portions adapted from nattraverso.ipdiscover -# portions authored by Brian Warner, working for Allmydata -# most recent version authored by Zooko O'Whielacronx, working for Allmydata - # from the Python Standard Library -import os, re, socket, sys +import os, re, socket, sys, subprocess # from Twisted -from twisted.internet import defer, reactor -from twisted.python import failure +from twisted.internet import defer, threads, reactor from twisted.internet.protocol import DatagramProtocol -from twisted.internet.utils import getProcessOutput from twisted.python.procutils import which from twisted.python import log -# from pyutil -import observer - try: import resource def increase_rlimits(): @@ -77,6 +67,7 @@ except ImportError: # since one might be shadowing the other. This hack appeases pyflakes. increase_rlimits = _increase_rlimits + def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET """ Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad @@ -121,14 +112,16 @@ def get_local_ip_for(target): except socket.gaierror: # DNS isn't running, or somehow we encountered an error - # note: if an interface is configured and up, but nothing is connected to it, - # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror - # . This is synchronous and occurs for each node being started, so users of certain unit - # tests will see something like 120s of delay, which may be enough to hit the default - # trial timeouts. For that reason, get_local_addresses_async() was changed to default to - # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also - # makes node startup a tad faster. - + # note: if an interface is configured and up, but nothing is + # connected to it, gethostbyname("A.ROOT-SERVERS.NET") will take 20 + # seconds to raise socket.gaierror . This is synchronous and occurs + # for each node being started, so users of + # test.common.SystemTestMixin (like test_system) will see something + # like 120s of delay, which may be enough to hit the default trial + # timeouts. For that reason, get_local_addresses_async() was changed + # to default to the numerical ip address for A.ROOT-SERVERS.NET, to + # avoid this DNS lookup. This also makes node startup fractionally + # faster. return None udpprot = DatagramProtocol() port = reactor.listenUDP(0, udpprot) @@ -146,16 +139,29 @@ _platform_map = { "linux-i386": "linux", # redhat "linux-ppc": "linux", # redhat "linux2": "linux", # debian + "linux3": "linux", # debian "win32": "win32", "irix6-n32": "irix", "irix6-n64": "irix", "irix6": "irix", "openbsd2": "bsd", + "openbsd3": "bsd", + "openbsd4": "bsd", + "openbsd5": "bsd", "darwin": "bsd", # Mac OS X "freebsd4": "bsd", "freebsd5": "bsd", "freebsd6": "bsd", + "freebsd7": "bsd", + "freebsd8": "bsd", + "freebsd9": "bsd", "netbsd1": "bsd", + "netbsd2": "bsd", + "netbsd3": "bsd", + "netbsd4": "bsd", + "netbsd5": "bsd", + "netbsd6": "bsd", + "dragonfly2": "bsd", "sunos5": "sunos", "cygwin": "cygwin", } @@ -173,12 +179,12 @@ _win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\ # These work in Redhat 6.x and Debian 2.2 potato _linux_path = '/sbin/ifconfig' -_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +_linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) -# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD -_bsd_path = '/sbin/ifconfig' -_bsd_args = ('-a',) -_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) +# NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X +_netbsd_path = '/sbin/ifconfig' +_netbsd_args = ('-a',) +_netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) # Irix 6.5 _irix_path = '/usr/etc/ifconfig' @@ -186,39 +192,6 @@ _irix_path = '/usr/etc/ifconfig' # Solaris 2.x _sunos_path = '/usr/sbin/ifconfig' -class SequentialTrier(object): - """ I hold a list of executables to try and try each one in turn - until one gives me a list of IP addresses.""" - - def __init__(self, exebasename, args, regex): - assert not os.path.isabs(exebasename) - self.exes_left_to_try = which(exebasename) - self.exes_left_to_try.reverse() - self.args = args - self.regex = regex - self.o = observer.OneShotObserverList() - self._try_next() - - def _try_next(self): - if not self.exes_left_to_try: - self.o.fire(None) - else: - exe = self.exes_left_to_try.pop() - d2 = _query(exe, self.args, self.regex) - - def cb(res): - if res: - self.o.fire(res) - else: - self._try_next() - - def eb(why): - self._try_next() - - d2.addCallbacks(cb, eb) - - def when_tried(self): - return self.o.when_fired() # k: platform string as provided in the value of _platform_map # v: tuple of (path_to_tool, args, regex,) @@ -226,19 +199,22 @@ _tool_map = { "linux": (_linux_path, (), _linux_re,), "win32": (_win32_path, _win32_args, _win32_re,), "cygwin": (_win32_path, _win32_args, _win32_re,), - "bsd": (_bsd_path, _bsd_args, _bsd_re,), - "irix": (_irix_path, _bsd_args, _bsd_re,), - "sunos": (_sunos_path, _bsd_args, _bsd_re,), + "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,), + "irix": (_irix_path, _netbsd_args, _netbsd_re,), + "sunos": (_sunos_path, _netbsd_args, _netbsd_re,), } + def _find_addresses_via_config(): - # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + return threads.deferToThread(_synchronously_find_addresses_via_config) + +def _synchronously_find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian's API platform = _platform_map.get(sys.platform) - (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + if not platform: + raise UnsupportedPlatformError(sys.platform) - # If the platform isn't known then we attempt BSD-style ifconfig. If it - # turns out that we don't get anything resembling a dotted quad IPv4 address - # out of it, then we'll raise UnsupportedPlatformError. + (pathtotool, args, regex,) = _tool_map[platform] # If pathtotool is a fully qualified path then we just try that. # If it is merely an executable name then we use Twisted's @@ -246,34 +222,33 @@ def _find_addresses_via_config(): # gives us something that resembles a dotted-quad IPv4 address. if os.path.isabs(pathtotool): - d = _query(pathtotool, args, regex) + return _query(pathtotool, args, regex) else: - d = SequentialTrier(pathtotool, args, regex).when_tried() - - d.addCallback(_check_result) - return d - -def _check_result(result): - if not result and not _platform_map.has_key(sys.platform): - return failure.Failure(UnsupportedPlatformError(sys.platform)) - else: - return result + exes_to_try = which(pathtotool) + for exe in exes_to_try: + try: + addresses = _query(exe, args, regex) + except Exception: + addresses = [] + if addresses: + return addresses + return [] def _query(path, args, regex): - d = getProcessOutput(path, args) - def _parse(output): - addresses = [] - outputsplit = output.split('\n') - for outline in outputsplit: - m = regex.match(outline) - if m: - addr = m.groupdict()['address'] - if addr not in addresses: - addresses.append(addr) + env = {'LANG': 'en_US.UTF-8'} + p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + (output, err) = p.communicate() - return addresses - d.addCallback(_parse) - return d + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses def _cygwin_hack_find_addresses(target): addresses = [] diff --git a/libs/pyutil/iputil.py~ b/libs/pyutil/iputil.py~ new file mode 100644 index 00000000..b8b96362 --- /dev/null +++ b/libs/pyutil/iputil.py~ @@ -0,0 +1,288 @@ +# portions extracted from ipaddresslib by Autonomous Zone Industries, LGPL (author: Greg Smith) +# portions adapted from nattraverso.ipdiscover +# portions authored by Brian Warner, working for Allmydata +# most recent version authored by Zooko O'Whielacronx, working for Allmydata + +# from the Python Standard Library +import os, re, socket, sys + +# from Twisted +from twisted.internet import defer, reactor +from twisted.python import failure +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.utils import getProcessOutput +from twisted.python.procutils import which +from twisted.python import log + +# from pyutil +import observer + +try: + import resource + def increase_rlimits(): + # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain + # systems (OS-X, probably solaris) start with a relatively low limit + # (256), and some unit tests want to open up more sockets than this. + # Most linux systems start with both hard and soft limits at 1024, + # which is plenty. + + # unfortunately the values to pass to setrlimit() vary widely from + # one system to another. OS-X reports (256, HUGE), but the real hard + # limit is 10240, and accepts (-1,-1) to mean raise it to the + # maximum. Cygwin reports (256, -1), then ignores a request of + # (-1,-1): instead you have to guess at the hard limit (it appears to + # be 3200), so using (3200,-1) seems to work. Linux reports a + # sensible (1024,1024), then rejects (-1,-1) as trying to raise the + # maximum limit, so you could set it to (1024,1024) but you might as + # well leave it alone. + + try: + current = resource.getrlimit(resource.RLIMIT_NOFILE) + except AttributeError: + # we're probably missing RLIMIT_NOFILE + return + + if current[0] >= 1024: + # good enough, leave it alone + return + + try: + if current[1] > 0 and current[1] < 1000000: + # solaris reports (256, 65536) + resource.setrlimit(resource.RLIMIT_NOFILE, + (current[1], current[1])) + else: + # this one works on OS-X (bsd), and gives us 10240, but + # it doesn't work on linux (on which both the hard and + # soft limits are set to 1024 by default). + resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1)) + new = resource.getrlimit(resource.RLIMIT_NOFILE) + if new[0] == current[0]: + # probably cygwin, which ignores -1. Use a real value. + resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1)) + + except ValueError: + log.msg("unable to set RLIMIT_NOFILE: current value %s" + % (resource.getrlimit(resource.RLIMIT_NOFILE),)) + except: + # who knows what. It isn't very important, so log it and continue + log.err() +except ImportError: + def _increase_rlimits(): + # TODO: implement this for Windows. Although I suspect the + # solution might be "be running under the iocp reactor and + # make this function be a no-op". + pass + # pyflakes complains about two 'def FOO' statements in the same time, + # since one might be shadowing the other. This hack appeases pyflakes. + increase_rlimits = _increase_rlimits + +def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET + """ + Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad + strings) that are currently configured on this host, sorted in descending + order of how likely we think they are to work. + + @param target: we want to learn an IP address they could try using to + connect to us; The default value is fine, but it might help if you + pass the address of a host that you are actually trying to be + reachable to. + """ + addresses = [] + local_ip = get_local_ip_for(target) + if local_ip: + addresses.append(local_ip) + + if sys.platform == "cygwin": + d = _cygwin_hack_find_addresses(target) + else: + d = _find_addresses_via_config() + + def _collect(res): + for addr in res: + if addr != "0.0.0.0" and not addr in addresses: + addresses.append(addr) + return addresses + d.addCallback(_collect) + + return d + +def get_local_ip_for(target): + """Find out what our IP address is for use by a given target. + + @return: the IP address as a dotted-quad string which could be used by + to connect to us. It might work for them, it might not. If + there is no suitable address (perhaps we don't currently have an + externally-visible interface), this will return None. + """ + + try: + target_ipaddr = socket.gethostbyname(target) + except socket.gaierror: + # DNS isn't running, or somehow we encountered an error + + # note: if an interface is configured and up, but nothing is connected to it, + # gethostbyname("A.ROOT-SERVERS.NET") will take 20 seconds to raise socket.gaierror + # . This is synchronous and occurs for each node being started, so users of certain unit + # tests will see something like 120s of delay, which may be enough to hit the default + # trial timeouts. For that reason, get_local_addresses_async() was changed to default to + # the numerical ip address for A.ROOT-SERVERS.NET, to avoid this DNS lookup. This also + # makes node startup a tad faster. + + return None + udpprot = DatagramProtocol() + port = reactor.listenUDP(0, udpprot) + try: + udpprot.transport.connect(target_ipaddr, 7) + localip = udpprot.transport.getHost().host + except socket.error: + # no route to that host + localip = None + port.stopListening() # note, this returns a Deferred + return localip + +# k: result of sys.platform, v: which kind of IP configuration reader we use +_platform_map = { + "linux-i386": "linux", # redhat + "linux-ppc": "linux", # redhat + "linux2": "linux", # debian + "win32": "win32", + "irix6-n32": "irix", + "irix6-n64": "irix", + "irix6": "irix", + "openbsd2": "bsd", + "darwin": "bsd", # Mac OS X + "freebsd4": "bsd", + "freebsd5": "bsd", + "freebsd6": "bsd", + "netbsd1": "bsd", + "sunos5": "sunos", + "cygwin": "cygwin", + } + +class UnsupportedPlatformError(Exception): + pass + +# Wow, I'm really amazed at home much mileage we've gotten out of calling +# the external route.exe program on windows... It appears to work on all +# versions so far. Still, the real system calls would much be preferred... +# ... thus wrote Greg Smith in time immemorial... +_win32_path = 'route.exe' +_win32_args = ('print',) +_win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P\d+\.\d+\.\d+\.\d+)\s+(?P\d+)\s*$', flags=re.M|re.I|re.S) + +# These work in Redhat 6.x and Debian 2.2 potato +_linux_path = '/sbin/ifconfig' +_linux_re = re.compile('^\s*inet addr:(?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# originally NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X, FreeBSD, OpenBSD +_bsd_path = '/sbin/ifconfig' +_bsd_args = ('-a',) +_bsd_re = re.compile('^\s+inet (?P\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S) + +# Irix 6.5 +_irix_path = '/usr/etc/ifconfig' + +# Solaris 2.x +_sunos_path = '/usr/sbin/ifconfig' + +class SequentialTrier(object): + """ I hold a list of executables to try and try each one in turn + until one gives me a list of IP addresses.""" + + def __init__(self, exebasename, args, regex): + assert not os.path.isabs(exebasename) + self.exes_left_to_try = which(exebasename) + self.exes_left_to_try.reverse() + self.args = args + self.regex = regex + self.o = observer.OneShotObserverList() + self._try_next() + + def _try_next(self): + if not self.exes_left_to_try: + self.o.fire(None) + else: + exe = self.exes_left_to_try.pop() + d2 = _query(exe, self.args, self.regex) + + def cb(res): + if res: + self.o.fire(res) + else: + self._try_next() + + def eb(why): + self._try_next() + + d2.addCallbacks(cb, eb) + + def when_tried(self): + return self.o.when_fired() + +# k: platform string as provided in the value of _platform_map +# v: tuple of (path_to_tool, args, regex,) +_tool_map = { + "linux": (_linux_path, (), _linux_re,), + "win32": (_win32_path, _win32_args, _win32_re,), + "cygwin": (_win32_path, _win32_args, _win32_re,), + "bsd": (_bsd_path, _bsd_args, _bsd_re,), + "irix": (_irix_path, _bsd_args, _bsd_re,), + "sunos": (_sunos_path, _bsd_args, _bsd_re,), + } +def _find_addresses_via_config(): + # originally by Greg Smith, hacked by Zooko to conform to Brian Warner's API. + + platform = _platform_map.get(sys.platform) + (pathtotool, args, regex,) = _tool_map.get(platform, ('ifconfig', _bsd_args, _bsd_re,)) + + # If the platform isn't known then we attempt BSD-style ifconfig. If it + # turns out that we don't get anything resembling a dotted quad IPv4 address + # out of it, then we'll raise UnsupportedPlatformError. + + # If pathtotool is a fully qualified path then we just try that. + # If it is merely an executable name then we use Twisted's + # "which()" utility and try each executable in turn until one + # gives us something that resembles a dotted-quad IPv4 address. + + if os.path.isabs(pathtotool): + d = _query(pathtotool, args, regex) + else: + d = SequentialTrier(pathtotool, args, regex).when_tried() + + d.addCallback(_check_result) + return d + +def _check_result(result): + if not result and not _platform_map.has_key(sys.platform): + return failure.Failure(UnsupportedPlatformError(sys.platform)) + else: + return result + +def _query(path, args, regex): + d = getProcessOutput(path, args) + def _parse(output): + addresses = [] + outputsplit = output.split('\n') + for outline in outputsplit: + m = regex.match(outline) + if m: + addr = m.groupdict()['address'] + if addr not in addresses: + addresses.append(addr) + + return addresses + d.addCallback(_parse) + return d + +def _cygwin_hack_find_addresses(target): + addresses = [] + for h in [target, "localhost", "127.0.0.1",]: + try: + addr = get_local_ip_for(h) + if addr not in addresses: + addresses.append(addr) + except socket.gaierror: + pass + + return defer.succeed(addresses) diff --git a/libs/pyutil/mathutil.py b/libs/pyutil/mathutil.py index 46781b0c..9c169801 100644 --- a/libs/pyutil/mathutil.py +++ b/libs/pyutil/mathutil.py @@ -11,7 +11,7 @@ def div_ceil(n, d): """ The smallest integer k such that k*d >= n. """ - return (n/d) + (n%d != 0) + return int((n//d) + (n%d != 0)) def next_multiple(n, k): """ diff --git a/libs/pyutil/mathutil.py~ b/libs/pyutil/mathutil.py~ new file mode 100644 index 00000000..46781b0c --- /dev/null +++ b/libs/pyutil/mathutil.py~ @@ -0,0 +1,106 @@ +# Copyright (c) 2005-2010 Zooko Wilcox-O'Hearn +# This file is part of pyutil; see README.rst for licensing terms. + +""" +A few commonly needed functions. +""" + +import math + +def div_ceil(n, d): + """ + The smallest integer k such that k*d >= n. + """ + return (n/d) + (n%d != 0) + +def next_multiple(n, k): + """ + The smallest multiple of k which is >= n. Note that if n is 0 then the + answer is 0. + """ + return div_ceil(n, k) * k + +def pad_size(n, k): + """ + The smallest number that has to be added to n to equal a multiple of k. + """ + if n%k: + return k - n%k + else: + return 0 + +def is_power_of_k(n, k): + return k**int(math.log(n, k) + 0.5) == n + +def next_power_of_k(n, k): + p = 1 + while p < n: + p *= k + return p + +def ave(l): + return sum(l) / len(l) + +def log_ceil(n, b): + """ + The smallest integer k such that b^k >= n. + + log_ceil(n, 2) is the number of bits needed to store any of n values, e.g. + the number of bits needed to store any of 128 possible values is 7. + """ + p = 1 + k = 0 + while p < n: + p *= b + k += 1 + return k + +def log_floor(n, b): + """ + The largest integer k such that b^k <= n. + """ + p = 1 + k = 0 + while p <= n: + p *= b + k += 1 + return k - 1 + +def linear_fit_slope(ps): + """ + Single-independent-variable linear regression -- least squares method. + + At least, I *think* this function computes that answer. I no longer + remember where I learned this trick and at the moment I can't prove to + myself that this is correct. + + @param ps a sequence of tuples of (x, y) + """ + avex = ave([x for (x, y) in ps]) + avey = ave([y for (x, y) in ps]) + sxy = sum([ (x - avex) * (y - avey) for (x, y) in ps ]) + sxx = sum([ (x - avex) ** 2 for (x, y) in ps ]) + if sxx == 0: + return None + return sxy / sxx + +def permute(l): + """ + Return all possible permutations of l. + + @type l: sequence + @rtype a set of sequences + """ + if len(l) == 1: + return [l,] + + res = [] + for i in range(len(l)): + l2 = list(l[:]) + x = l2.pop(i) + for l3 in permute(l2): + l3.append(x) + res.append(l3) + + return res + diff --git a/libs/pyutil/odict.py~ b/libs/pyutil/odict.py~ deleted file mode 100644 index 0ed5ce7b..00000000 --- a/libs/pyutil/odict.py~ +++ /dev/null @@ -1,552 +0,0 @@ -# Copyright (c) 2002-2009 Zooko "Zooko" Wilcox-O'Hearn - -""" -This module offers a Ordered Dict, which is a dict that preserves -insertion order. See PEP 372 for description of the problem. This -implementation uses a linked-list to get good O(1) asymptotic -performance. (Actually it is O(hashtable-update-cost), but whatever.) - -Warning: if -O optimizations are not turned on then OrderedDict performs -extensive self-analysis in every function call, which can take minutes -and minutes for a large cache. Turn on -O, or comment out assert -self._assert_invariants() -""" - -import operator - -from assertutil import _assert, precondition -from humanreadable import hr - -class OrderedDict: - """ - An efficient ordered dict. - - Adding an item that is already in the dict *does not* make it the - most- recently-added item although it may change the state of the - dict itself (if the value is different than the previous value). - - See also SmallOrderedDict (below), which is faster in some cases. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return (k, v,) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - k = self.i - precondition(self.c.d.has_key(k), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", k, self.c) - (v, p, n,) = self.c.d[k] - self.i = p - return k - - class ValIterator: - def __init__(self, c): - self.c = c - self.i = c.d[c.ts][1] - def __iter__(self): - return self - def next(self): - if self.i is self.c.hs: - raise StopIteration - precondition(self.c.d.has_key(self.i), "The iterated OrderedDict doesn't have the next key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - (v, p, n,) = self.c.d[self.i] - self.i = p - return v - - class Sentinel: - def __init__(self, msg): - self.msg = msg - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.msg,) - - def __init__(self, initialdata={}): - self.d = {} # k: k, v: [v, prev, next,] # the dict - self.hs = OrderedDict.Sentinel("hs") - self.ts = OrderedDict.Sentinel("ts") - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.update(initialdata) - - assert self._assert_invariants() - - def __repr_n__(self, n=None): - s = ["{",] - try: - iter = self.iteritems() - x = iter.next() - s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - i = 1 - while (n is None) or (i < n): - x = iter.next() - s.append(", "); s.append(str(x[0])); s.append(": "); s.append(str(x[1])) - except StopIteration: - pass - s.append("}") - return ''.join(s) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(),) - - def __str__(self): - return "<%s %s>" % (self.__class__.__name__, self.__repr_n__(16),) - - def _assert_invariants(self): - _assert((len(self.d) > 2) == (self.d[self.hs][2] is not self.ts) == (self.d[self.ts][1] is not self.hs), "Head and tail point to something other than each other if and only if there is at least one element in the dictionary.", self.hs, self.ts, len(self.d)) - foundprevsentinel = 0 - foundnextsentinel = 0 - for (k, (v, p, n,)) in self.d.iteritems(): - _assert(v not in (self.hs, self.ts,)) - _assert(p is not self.ts, "A reference to the tail sentinel may not appear in prev.", k, v, p, n) - _assert(n is not self.hs, "A reference to the head sentinel may not appear in next.", k, v, p, n) - _assert(p in self.d, "Each prev is required to appear as a key in the dict.", k, v, p, n) - _assert(n in self.d, "Each next is required to appear as a key in the dict.", k, v, p, n) - if p is self.hs: - foundprevsentinel += 1 - _assert(foundprevsentinel <= 2, "No more than two references to the head sentinel may appear as a prev.", k, v, p, n) - if n is self.ts: - foundnextsentinel += 1 - _assert(foundnextsentinel <= 2, "No more than one reference to the tail sentinel may appear as a next.", k, v, p, n) - _assert(foundprevsentinel == 2, "A reference to the head sentinel is required appear as a prev (plus a self-referential reference).") - _assert(foundnextsentinel == 2, "A reference to the tail sentinel is required appear as a next (plus a self-referential reference).") - - count = 0 - for (k, v,) in self.iteritems(): - _assert(k not in (self.hs, self.ts,), k, self.hs, self.ts) - count += 1 - _assert(count == len(self.d)-2, count, len(self.d)) # -2 for the sentinels - - return True - - def move_to_most_recent(self, k, strictkey=False): - assert self._assert_invariants() - - if not self.d.has_key(k): - if strictkey: - raise KeyError, k - return - - node = self.d[k] - - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - - # move to front - hnode = self.d[self.hs] - - node[1] = self.hs - node[2] = hnode[2] - hnode[2] = k - self.d[node[2]][1] = k - - assert self._assert_invariants() - - def iteritems(self): - return OrderedDict.ItemIterator(self) - - def itervalues(self): - return OrderedDict.ValIterator(self) - - def iterkeys(self): - return self.__iter__() - - def __iter__(self): - return OrderedDict.KeyIterator(self) - - def __getitem__(self, key, default=None, strictkey=True): - node = self.d.get(key) - if not node: - if strictkey: - raise KeyError, key - return default - return node[0] - - def __setitem__(self, k, v=None): - assert self._assert_invariants() - - node = self.d.get(k) - if node: - node[0] = v - return - - hnode = self.d[self.hs] - n = hnode[2] - self.d[k] = [v, self.hs, n,] - hnode[2] = k - self.d[n][1] = k - - assert self._assert_invariants() - return v - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the value removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if self.d.has_key(key): - node = self.d[key] - # relink - self.d[node[1]][2] = node[2] - self.d[node[2]][1] = node[1] - del self.d[key] - assert self._assert_invariants() - return node[0] - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def has_key(self, key): - assert self._assert_invariants() - if self.d.has_key(key): - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def clear(self): - assert self._assert_invariants() - self.d.clear() - self.d[self.hs] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - self.d[self.ts] = [None, self.hs, self.ts,] # This allows us to use sentinels as normal nodes. - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - - for (k, v,) in otherdict.iteritems(): - assert self._assert_invariants() - self[k] = v - assert self._assert_invariants() - - def pop(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - self.remove(k) - assert self._assert_invariants() - return k - - def popitem(self): - assert self._assert_invariants() - if len(self.d) < 2: # the +2 is for the sentinels - raise KeyError, 'popitem(): dictionary is empty' - k = self.d[self.hs][2] - val = self.remove(k) - assert self._assert_invariants() - return (k, val,) - - def keys_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return t.keys() - - def keys(self): - res = [None] * len(self) - i = 0 - for k in self.iterkeys(): - res[i] = k - i += 1 - return res - - def values_unsorted(self): - assert self._assert_invariants() - t = self.d.copy() - del t[self.hs] - del t[self.ts] - assert self._assert_invariants() - return map(operator.__getitem__, t.values(), [0]*len(t)) - - def values(self): - res = [None] * len(self) - i = 0 - for v in self.itervalues(): - res[i] = v - i += 1 - return res - - def items(self): - res = [None] * len(self) - i = 0 - for it in self.iteritems(): - res[i] = it - i += 1 - return res - - def __len__(self): - return len(self.d) - 2 - - def insert(self, key, val=None): - assert self._assert_invariants() - result = self.__setitem__(key, val) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def get(self, key, default=None): - return self.__getitem__(key, default, strictkey=False) - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - -class SmallOrderedDict(dict): - """ - SmallOrderedDict is faster than OrderedDict for small sets. How small? That - depends on your machine and which operations you use most often. Use - performance profiling to determine whether the cache class that you are - using makes any difference to the performance of your program, and if it - does, then run "quick_bench()" in test/test_cache.py to see which cache - implementation is faster for the size of your datasets. - - A simple least-recently-used cache. It keeps an LRU queue, and - when the number of items in the cache reaches maxsize, it removes - the least recently used item. - - "Looking" at an item or a key such as with "has_key()" makes that - item become the most recently used item. - - You can also use "refresh()" to explicitly make an item become the most - recently used item. - - Adding an item that is already in the dict *does* make it the - most- recently-used item although it does not change the state of - the dict itself. - """ - class ItemIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return (k, dict.__getitem__(self.c, k),) - - class KeyIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return k - - class ValueIterator: - def __init__(self, c): - self.c = c - self.i = 0 - def __iter__(self): - return self - def next(self): - precondition(self.i <= len(self.c._lru), "The iterated SmallOrderedDict doesn't have this many elements. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c) - precondition(dict.has_key(self.c, self.c._lru[self.i]), "The iterated SmallOrderedDict doesn't have this key. Most likely this is because someone altered the contents of the OrderedDict while the iteration was in progress.", self.i, self.c._lru[self.i], self.c) - if self.i == len(self.c._lru): - raise StopIteration - k = self.i - self.i += 1 - return dict.__getitem__(self.c, k) - - def __init__(self, initialdata={}, maxsize=128): - dict.__init__(self, initialdata) - self._lru = initialdata.keys() # contains keys - self._maxsize = maxsize - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - assert self._assert_invariants() - - def _assert_invariants(self): - _assert(len(self._lru) <= self._maxsize, "Size is required to be <= maxsize.") - _assert(len(filter(lambda x: dict.has_key(self, x), self._lru)) == len(self._lru), "Each key in self._lru is required to be in dict.", filter(lambda x: not dict.has_key(self, x), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(filter(lambda x: x in self._lru, self.keys())) == len(self), "Each key in dict is required to be in self._lru.", filter(lambda x: x not in self._lru, self.keys()), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) == len(self), "internal consistency", filter(lambda x: x not in self.keys(), self._lru), len(self._lru), self._lru, len(self), self) - _assert(len(self._lru) <= self._maxsize, "internal consistency", len(self._lru), self._lru, self._maxsize) - return True - - def insert(self, key, item=None): - assert self._assert_invariants() - result = self.__setitem__(key, item) - assert self._assert_invariants() - return result - - def setdefault(self, key, default=None): - assert self._assert_invariants() - if not self.has_key(key): - self[key] = default - assert self._assert_invariants() - return self[key] - - def __setitem__(self, key, item=None): - assert self._assert_invariants() - if dict.has_key(self, key): - self._lru.remove(key) - else: - if len(self._lru) == self._maxsize: - # If this insert is going to increase the size of the cache to bigger than maxsize: - killkey = self._lru.pop(0) - dict.__delitem__(self, killkey) - dict.__setitem__(self, key, item) - self._lru.append(key) - assert self._assert_invariants() - return item - - def remove(self, key, default=None, strictkey=True): - assert self._assert_invariants() - result = self.__delitem__(key, default, strictkey) - assert self._assert_invariants() - return result - - def __delitem__(self, key, default=None, strictkey=True): - """ - @param strictkey: True if you want a KeyError in the case that - key is not there, False if you want a reference to default - in the case that key is not there - @param default: the object to return if key is not there; This - is ignored if strictkey. - - @return: the object removed or default if there is not item by - that key and strictkey is False - """ - assert self._assert_invariants() - if dict.has_key(self, key): - val = dict.__getitem__(self, key) - dict.__delitem__(self, key) - self._lru.remove(key) - assert self._assert_invariants() - return val - elif strictkey: - assert self._assert_invariants() - raise KeyError, key - else: - assert self._assert_invariants() - return default - - def clear(self): - assert self._assert_invariants() - dict.clear(self) - self._lru = [] - assert self._assert_invariants() - - def update(self, otherdict): - """ - @return: self - """ - assert self._assert_invariants() - if len(otherdict) > self._maxsize: - # Handling this special case here makes it possible to implement the - # other more common cases faster below. - dict.clear(self) - self._lru = [] - if self._maxsize > (len(otherdict) - self._maxsize): - dict.update(self, otherdict) - while len(self) > self._maxsize: - dict.popitem(self) - else: - for k, v, in otherdict.iteritems(): - if len(self) == self._maxsize: - break - dict.__setitem__(self, k, v) - self._lru = dict.keys(self) - assert self._assert_invariants() - return self - - for k in otherdict.iterkeys(): - if dict.has_key(self, k): - self._lru.remove(k) - self._lru.extend(otherdict.keys()) - dict.update(self, otherdict) - - over = len(self) - self._maxsize - if over > 0: - map(dict.__delitem__, [self]*over, self._lru[:over]) - del self._lru[:over] - - assert self._assert_invariants() - return self - - def has_key(self, key): - assert self._assert_invariants() - if dict.has_key(self, key): - assert key in self._lru, "key: %s, self._lru: %s" % tuple(map(hr, (key, self._lru,))) - self._lru.remove(key) - self._lru.append(key) - assert self._assert_invariants() - return True - else: - assert self._assert_invariants() - return False - - def refresh(self, key, strictkey=True): - """ - @param strictkey: raise a KeyError exception if key isn't present - """ - assert self._assert_invariants() - if not dict.has_key(self, key): - if strictkey: - raise KeyError, key - return - self._lru.remove(key) - self._lru.append(key) - - def popitem(self): - if not self._lru: - raise KeyError, 'popitem(): dictionary is empty' - k = self._lru[-1] - obj = self.remove(k) - return (k, obj,) diff --git a/libs/pyutil/randutil.py b/libs/pyutil/randutil.py index a3efb74f..82eb3e18 100644 --- a/libs/pyutil/randutil.py +++ b/libs/pyutil/randutil.py @@ -80,6 +80,5 @@ seed = randobj.seed def randstr(n): return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) -import random as insecurerandom def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) + return os.urandom(n) diff --git a/libs/pyutil/randutil.py~ b/libs/pyutil/randutil.py~ deleted file mode 100644 index b0f1c4f9..00000000 --- a/libs/pyutil/randutil.py~ +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2002-2010 Zooko Wilcox-O'Hearn -# This file is part of pyutil; see README.rst for licensing terms. - -import warnings -import os, random - -try: - import hashexpand - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=None, deterministic=True): - warnings.warn("deprecated", DeprecationWarning) - if not deterministic: - raise NotImplementedError, "SHA256Expander is always deterministic. For non-deterministic, try urandomRandom." - - hashexpand.SHA256Expander.__init__(self) - random.Random.__init__(self, seed) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - import increasing_timer - seed = repr(increasing_timer.time()) - hashexpand.SHA256Expander.seed(self, seed) - - - class SHA256Random(hashexpand.SHA256Expander, random.Random): - def __init__(self, seed=""): - warnings.warn("deprecated", DeprecationWarning) - hashexpand.SHA256Expander.__init__(self) - self.seed(seed) - - def seed(self, seed=None): - if seed is None: - seed = os.urandom(32) - hashexpand.SHA256Expander.seed(self, seed) -except ImportError, le: - class InsecureSHA256Random: - def __init__(self, seed=None): - raise ImportError, le - class SHA256Random: - def __init__(self, seed=""): - raise ImportError, le - -class devrandomRandom(random.Random): - """ The problem with using this one, of course, is that it blocks. This - is, of course, a security flaw. (On Linux and probably on other - systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def __init__(self): - warnings.warn("deprecated", DeprecationWarning) - self.dr = open("/dev/random", "r") - - def get(self, bytes): - return self.dr.read(bytes) - - -class devurandomRandom(random.Random): - """ The problem with using this one is that it gives answers even when it - has never been properly seeded, e.g. when you are booting from CD and have - just started up and haven't yet gathered enough entropy to actually be - unguessable. (On Linux and probably on other systems.) --Zooko 2005-03-04 - - Not repeatable. - """ - def get(self, bytes): - warnings.warn("deprecated", DeprecationWarning) - return os.urandom(bytes) - - -randobj = devurandomRandom() -get = randobj.get -random = randobj.random -randrange = randobj.randrange -shuffle = randobj.shuffle -choice = randobj.choice -seed = randobj.seed - -def randstr(n): - return ''.join(map(chr, map(randrange, [0]*n, [256]*n))) - -import random as insecurerandom -def insecurerandstr(n): - return ''.join(map(chr, map(insecurerandom.randrange, [0]*n, [256]*n))) diff --git a/libs/pyutil/scripts/passphrase.py b/libs/pyutil/scripts/passphrase.py new file mode 100644 index 00000000..bed79c13 --- /dev/null +++ b/libs/pyutil/scripts/passphrase.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse, math, random + +from pyutil.mathutil import div_ceil + +from pkg_resources import resource_stream + +def recursive_subset_sum(entropy_needed, wordlists): + # Pick a minimalish set of numbers which sum to at least + # entropy_needed. + + # Okay now what's the smallest number of words which will give us + # at least this much entropy? + entropy_of_biggest_wordlist = wordlists[-1][0] + assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1] + needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist) + # How much entropy do we need from each word? + needed_entropy_per_word = entropy_needed / needed_words + # What's the smallest wordlist that offers at least this much + # entropy per word? + for (wlentropy, wl) in wordlists: + if wlentropy >= needed_entropy_per_word: + break + assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word) + + result = [(wlentropy, wl)] + # If we need more, recurse... + if wlentropy < entropy_needed: + rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists) + result.extend(rest) + return result + +def gen_passphrase(entropy, allwords): + maxlenwords = [] + i = 2 # The smallest set is words of length 1 or 2. + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + while len(maxlenwords[-1][1]) < len(allwords): + i += 1 + words = [x for x in allwords if len(x) <= i] + maxlenwords.append((math.log(len(words), 2), words)) + + sr = random.SystemRandom() + passphrase = [] + + wordlists_to_use = recursive_subset_sum(entropy, maxlenwords) + + passphraseentropy = 0.0 + for (wle, wl) in wordlists_to_use: + passphrase.append(sr.choice(wl)) + passphraseentropy += wle + + return (u".".join(passphrase), passphraseentropy) + +def main(): + parser = argparse.ArgumentParser(prog="chbs", description="Create a random passphrase by picking a few random words.") + + parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use chbs's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT") + parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS") + args = parser.parse_args() + + dicti = args.dictionary + if not dicti: + dicti = resource_stream('pyutil', 'data/wordlist.txt') + allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()]) + + passphrase, bits = gen_passphrase(args.bits, allwords) + + print u"Your new password is: '%s'. It is worth about %s bits." % (passphrase, bits) diff --git a/libs/pyutil/scripts/time_comparisons.py b/libs/pyutil/scripts/time_comparisons.py new file mode 100644 index 00000000..15a38852 --- /dev/null +++ b/libs/pyutil/scripts/time_comparisons.py @@ -0,0 +1,209 @@ +# If you run this file, it will make up a random secret and then crack it +# using timing information from a string comparison function. Maybe--if it +# gets lucky. It takes a long, long time to work. + +# So, the thing I need help with is statistics. The way this thing works is +# extremely stupid. Suppose you want to know which function invocation takes +# longer: comparison(secret, guess1) or comparison(secret, guess2)? + +# If you can correctly determine that one of them takes longer than the +# other, then (a) you can use that to crack the secret, and (b) this is a +# unit test demonstrating that comparison() is not timing-safe. + +# So how does this script do it? Extremely stupidly. First of all, you can't +# reliably measure tiny times, so to measure the time that a function takes, +# we run that function 10,000 times in a row, measure how long that took, and +# divide by 10,000 to estimate how long any one run would have taken. + +# Then, we do that 100 times in a row, and take the fastest of 100 runs. (I +# also experimented with taking the mean of 100 runs instead of the fastest.) + +# Then, we just say whichever comparison took longer (for its fastest run of +# 100 runs of 10,000 executions per run) is the one we think is a closer +# guess to the secret. + +# Now I would *like* to think that there is some kind of statistical analysis +# more sophisticated than "take the slowest of the fastest of 100 runs of +# 10,000 executions". Such improved statistical analysis would hopefully be +# able to answer these two questions: + +# 1. Are these two function calls -- comparison(secret, guess1) and +# comparison(secret, guess2) -- drawing from the same distribution or +# different? If you can answer that question, then you've answered the +# question of whether "comparison" is timing-safe or not. + +# And, this would also allow the cracker to recover from a false step. If it +# incorrectly decides the the prefix of the secret is ABCX, when the real +# secret is ABCD, then after that every next step it takes will be the +# "drawing from the same distribution" kind -- any difference between ABCXQ +# and ABCXR will be just due to noise, since both are equally far from the +# correct answer, which startsw with ABCD. If it could realize that there is +# no real difference between the distributions, then it could back-track and +# recover. + +# 2. Giving the ability to measure, noisily, the time taken by comparison(), +# how can you most efficiently figure out which guess takes the longest? If +# you can do that more efficiently, you can crack secrets more efficiently. + +# The script takes two arguments. The first is how many symbols in the +# secret, and the second is how big the alphabet from which the symbols are +# drawn. To prove that this script can *ever* work, try passing length 5 and +# alphabet size 2. Also try editing the code to let is use sillycomp. That'll +# definitely make it work. If you can improve this script (as per the thing +# above about "needing better statistics") to the degree that it can crack a +# secret with length 32 and alphabet size 256, then that would be awesome. + +# See the result of this commandline: + +# $ python -c 'import time_comparisons ; time_comparisons.print_measurements()' + + +from pyutil import benchutil + +import hashlib, random, os + +from decimal import Decimal +D=Decimal + +p1 = 'a'*32 +p1a = 'a'*32 +p2 = 'a'*31+'b' # close, but no cigar +p3 = 'b'*32 # different in the first byte + +def randstr(n, alphabetsize): + alphabet = [ chr(x) for x in range(alphabetsize) ] + return ''.join([random.choice(alphabet) for i in range(n)]) + +def compare(n, f, a, b): + for i in xrange(n): + f(a, b) + +def eqeqcomp(a, b): + return a == b + +def sillycomp(a, b): + # This exposes a lot of information in its timing about how many leading bytes match. + for i in range(len(a)): + if a[i] != b[i]: + return False + for i in xrange(2**9): + pass + if len(a) == len(b): + return True + else: + return False + +def hashcomp(a, b): + # Brian Warner invented this for Tahoe-LAFS. It seems like it should be very safe agaist timing leakage of any kind, because of the inclusion of a new random randkey every time. Note that exposing the value of the hash (i.e. the output of md5(randkey+secret)) is *not* a security problem. You can post that on your web site and let all attackers have it, no problem. (Provided that the value of "randkey" remains secret.) + + randkey = os.urandom(32) + return hashlib.md5(randkey+ a).digest() == hashlib.md5(randkey+b).digest() + +def xorcomp(a, b): + # This appears to be the most popular timing-insensitive string comparison function. I'm not completely sure it is fully timing-insensitive. (There are all sorts of funny things inside Python, such as caching of integer objects < 100...) + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + +def print_measurements(): + N=10**4 + REPS=10**2 + + print "all times are in nanoseconds per comparison (in scientific notation)" + print + + for comparator in [eqeqcomp, hashcomp, xorcomp, sillycomp]: + print "using comparator ", comparator + + # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: + trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] + random.shuffle(trials) + for (a, b, desc) in trials: + print "comparing two strings that are %s to each other" % (desc,) + + def f(n): + compare(n, comparator, a, b) + + benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) + + print + +def try_to_crack_secret(cracker, comparator, secretlen, alphabetsize): + secret = randstr(secretlen, alphabetsize) + + def test_guess(x): + return comparator(secret, x) + + print "Giving cracker %s a chance to figure out the secret. Don't tell him, but the secret is %s. Whenever he makes a guess, we'll use comparator %s to decide if his guess is right ..." % (cracker, secret.encode('hex'), comparator,) + + guess = cracker(test_guess, secretlen, alphabetsize) + + print "Cracker %s guessed %r" % (cracker, guess,) + if guess == secret: + print "HE FIGURED IT OUT!? HOW DID HE DO THAT." + else: + print "HAHA. Our secret is safe." + +def byte_at_a_time_cracker(test_guess, secretlen, alphabetsize): + # If we were cleverer, we'd add some backtracking behaviour where, if we can't find any x such that ABCx stands out from the crowd as taking longer than all the other ABCy's, then we start to think that we've taken a wrong step and we go back to trying ABy's. Make sense? But we're not that clever. Once we take a step, we don't backtrack. + + print + + guess=[] + + while len(guess) < secretlen: + best_next_byte = None + best_next_byte_time = None + + # For each possible byte... + for next_byte in range(alphabetsize): + c = chr(next_byte) + + # Construct a guess with our best candidate so far... + candidate_guess = guess[:] + + # Plus that byte... + candidate_guess.append(c) + s = ''.join(candidate_guess) + + # Plus random bytes... + s += os.urandom(32 - len(s)) + + # And see how long it takes the test_guess to consider it... + def f(n): + for i in xrange(n): + test_guess(s) + + times = benchutil.rep_bench(f, 10**7, MAXREPS=10**3, quiet=True) + + fastesttime = times['mean'] + + print "%s..."%(c.encode('hex'),), + if best_next_byte is None or fastesttime > best_next_byte_time: + print "new candidate for slowest next-char: %s, took: %s" % (c.encode('hex'), fastesttime,), + + best_next_byte_time = fastesttime + best_next_byte = c + + # Okay we've tried all possible next bytes. Our guess is this one (the one that took longest to be tested by test_guess): + guess.append(best_next_byte) + print "SLOWEST next-char %s! Current guess at secret: %s" % (best_next_byte.encode('hex'), ''.join(guess).encode('hex'),) + + guess = ''.join(guess) + print "Our guess for the secret: %r" % (guess,) + return guess + +if __name__ == '__main__': + import sys + secretlen = int(sys.argv[1]) + alphabetsize = int(sys.argv[2]) + if alphabetsize > 256: + raise Exception("We assume we can fit one element of the alphabet into a byte.") + + print "secretlen: %d, alphabetsize: %d" % (secretlen, alphabetsize,) + + # try_to_crack_secret(byte_at_a_time_cracker, sillycomp, secretlen, alphabetsize) + try_to_crack_secret(byte_at_a_time_cracker, eqeqcomp, secretlen, alphabetsize) diff --git a/libs/pyutil/test/current/test_mathutil.py b/libs/pyutil/test/current/test_mathutil.py index 7c189dfb..da788758 100644 --- a/libs/pyutil/test/current/test_mathutil.py +++ b/libs/pyutil/test/current/test_mathutil.py @@ -42,6 +42,13 @@ class MathUtilTestCase(unittest.TestCase): self.failUnlessEqual(f(5, 3), 2) self.failUnlessEqual(f(6, 3), 2) self.failUnlessEqual(f(7, 3), 3) + self.failUnless(isinstance(f(0.0, 1), int)) + self.failUnlessEqual(f(7.0, 3.0), 3) + self.failUnlessEqual(f(7, 3.0), 3) + self.failUnlessEqual(f(7.0, 3), 3) + self.failUnlessEqual(f(6.0, 3.0), 2) + self.failUnlessEqual(f(6.0, 3), 2) + self.failUnlessEqual(f(6, 3.0), 2) def test_next_multiple(self): f = mathutil.next_multiple diff --git a/libs/pyutil/test/current/test_mathutil.py~ b/libs/pyutil/test/current/test_mathutil.py~ new file mode 100644 index 00000000..7c189dfb --- /dev/null +++ b/libs/pyutil/test/current/test_mathutil.py~ @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import unittest + +from pyutil import mathutil +from pyutil.assertutil import _assert + +class MathUtilTestCase(unittest.TestCase): + def _help_test_is_power_of_k(self, k): + for i in range(2, 40): + _assert(mathutil.is_power_of_k(k**i, k), k, i) + + def test_is_power_of_k(self): + for i in range(2, 5): + self._help_test_is_power_of_k(i) + + def test_log_ceil(self): + f = mathutil.log_ceil + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 2), 2) + + def test_log_floor(self): + f = mathutil.log_floor + self.failUnlessEqual(f(1, 2), 0) + self.failUnlessEqual(f(1, 3), 0) + self.failUnlessEqual(f(2, 2), 1) + self.failUnlessEqual(f(2, 3), 0) + self.failUnlessEqual(f(3, 2), 1) + + def test_div_ceil(self): + f = mathutil.div_ceil + self.failUnlessEqual(f(0, 1), 0) + self.failUnlessEqual(f(0, 2), 0) + self.failUnlessEqual(f(0, 3), 0) + self.failUnlessEqual(f(1, 3), 1) + self.failUnlessEqual(f(2, 3), 1) + self.failUnlessEqual(f(3, 3), 1) + self.failUnlessEqual(f(4, 3), 2) + self.failUnlessEqual(f(5, 3), 2) + self.failUnlessEqual(f(6, 3), 2) + self.failUnlessEqual(f(7, 3), 3) + + def test_next_multiple(self): + f = mathutil.next_multiple + self.failUnlessEqual(f(5, 1), 5) + self.failUnlessEqual(f(5, 2), 6) + self.failUnlessEqual(f(5, 3), 6) + self.failUnlessEqual(f(5, 4), 8) + self.failUnlessEqual(f(5, 5), 5) + self.failUnlessEqual(f(5, 6), 6) + self.failUnlessEqual(f(32, 1), 32) + self.failUnlessEqual(f(32, 2), 32) + self.failUnlessEqual(f(32, 3), 33) + self.failUnlessEqual(f(32, 4), 32) + self.failUnlessEqual(f(32, 5), 35) + self.failUnlessEqual(f(32, 6), 36) + self.failUnlessEqual(f(32, 7), 35) + self.failUnlessEqual(f(32, 8), 32) + self.failUnlessEqual(f(32, 9), 36) + self.failUnlessEqual(f(32, 10), 40) + self.failUnlessEqual(f(32, 11), 33) + self.failUnlessEqual(f(32, 12), 36) + self.failUnlessEqual(f(32, 13), 39) + self.failUnlessEqual(f(32, 14), 42) + self.failUnlessEqual(f(32, 15), 45) + self.failUnlessEqual(f(32, 16), 32) + self.failUnlessEqual(f(32, 17), 34) + self.failUnlessEqual(f(32, 18), 36) + self.failUnlessEqual(f(32, 589), 589) + + def test_pad_size(self): + f = mathutil.pad_size + self.failUnlessEqual(f(0, 4), 0) + self.failUnlessEqual(f(1, 4), 3) + self.failUnlessEqual(f(2, 4), 2) + self.failUnlessEqual(f(3, 4), 1) + self.failUnlessEqual(f(4, 4), 0) + self.failUnlessEqual(f(5, 4), 3) + + def test_is_power_of_k_part_2(self): + f = mathutil.is_power_of_k + for i in range(1, 100): + if i in (1, 2, 4, 8, 16, 32, 64): + self.failUnless(f(i, 2), "but %d *is* a power of 2" % i) + else: + self.failIf(f(i, 2), "but %d is *not* a power of 2" % i) + for i in range(1, 100): + if i in (1, 3, 9, 27, 81): + self.failUnless(f(i, 3), "but %d *is* a power of 3" % i) + else: + self.failIf(f(i, 3), "but %d is *not* a power of 3" % i) + + def test_next_power_of_k(self): + f = mathutil.next_power_of_k + self.failUnlessEqual(f(0,2), 1) + self.failUnlessEqual(f(1,2), 1) + self.failUnlessEqual(f(2,2), 2) + self.failUnlessEqual(f(3,2), 4) + self.failUnlessEqual(f(4,2), 4) + for i in range(5, 8): self.failUnlessEqual(f(i,2), 8, "%d" % i) + for i in range(9, 16): self.failUnlessEqual(f(i,2), 16, "%d" % i) + for i in range(17, 32): self.failUnlessEqual(f(i,2), 32, "%d" % i) + for i in range(33, 64): self.failUnlessEqual(f(i,2), 64, "%d" % i) + for i in range(65, 100): self.failUnlessEqual(f(i,2), 128, "%d" % i) + + self.failUnlessEqual(f(0,3), 1) + self.failUnlessEqual(f(1,3), 1) + self.failUnlessEqual(f(2,3), 3) + self.failUnlessEqual(f(3,3), 3) + for i in range(4, 9): self.failUnlessEqual(f(i,3), 9, "%d" % i) + for i in range(10, 27): self.failUnlessEqual(f(i,3), 27, "%d" % i) + for i in range(28, 81): self.failUnlessEqual(f(i,3), 81, "%d" % i) + for i in range(82, 200): self.failUnlessEqual(f(i,3), 243, "%d" % i) + + def test_ave(self): + f = mathutil.ave + self.failUnlessEqual(f([1,2,3]), 2) + self.failUnlessEqual(f([0,0,0,4]), 1) + self.failUnlessAlmostEqual(f([0.0, 1.0, 1.0]), .666666666666) + + def failUnlessEqualContents(self, a, b): + self.failUnlessEqual(sorted(a), sorted(b)) + + def test_permute(self): + f = mathutil.permute + self.failUnlessEqualContents(f([]), []) + self.failUnlessEqualContents(f([1]), [[1]]) + self.failUnlessEqualContents(f([1,2]), [[1,2], [2,1]]) + self.failUnlessEqualContents(f([1,2,3]), + [[1,2,3], [1,3,2], + [2,1,3], [2,3,1], + [3,1,2], [3,2,1]]) diff --git a/libs/pyutil/time_comparisons.py b/libs/pyutil/time_comparisons.py deleted file mode 100644 index ee1bcfa0..00000000 --- a/libs/pyutil/time_comparisons.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyutil import benchutil - -import hashlib, random, os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeqcomp(a, b): - return a == b - -def hashcomp(a, b): - salt = os.urandom(32) - return hashlib.md5(salt+ a).digest() == hashlib.md5(salt+b).digest() - -N=10**4 -REPS=10**2 - -print "all times are in nanoseconds per comparison (scientific notation)" -print - -for comparator in [eqeqcomp, hashcomp]: - print "using comparator ", comparator - - # for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]: - trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')] - random.shuffle(trials) - for (a, b, desc) in trials: - print "comparing two strings that are %s to each other" % (desc,) - - def f(n): - compare(n, comparator, a, b) - - benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - - print diff --git a/libs/pyutil/time_comparisons.py~ b/libs/pyutil/time_comparisons.py~ deleted file mode 100644 index abf151ad..00000000 --- a/libs/pyutil/time_comparisons.py~ +++ /dev/null @@ -1,72 +0,0 @@ -from pyutil import benchutil - -import hashlib -import os - -from decimal import Decimal -D=Decimal - -p1 = 'a'*32 -p1a = 'a'*32 -p2 = 'a'*31+'b' # close, but no cigar -p3 = 'b'*32 # different in the first byte - -def compare(n, f, a, b): - for i in xrange(n): - f(a, b) - -def eqeq(a, b): - return a == b - -def equalsequals_s(n): - # return compare(n, eqeq, - for i in xrange(n): - p1 == p1a - -def equalsequals_c(n): - for i in xrange(n): - p1 == p2 - -def equalsequals_f(n): - for i in xrange(n): - p1 == p3 - -def hash_s(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p1a).digest() - -def hash_c(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p2).digest() - -def hash_f(n): - for i in xrange(n): - salt = os.urandom(32) - hashlib.md5(salt+ p1).digest() == hashlib.md5(salt+p3).digest() - -N=10**4 -REPS=10**2 - -print "using '=='" - -print "same" -benchutil.rep_bench(equalsequals_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(equalsequals_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(equalsequals_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "using hash" - -print "same" -benchutil.rep_bench(hash_s, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "far" -benchutil.rep_bench(hash_f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) - -print "close" -benchutil.rep_bench(hash_c, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 0cbfa7c0..a2cef356 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -549,7 +549,7 @@ class OAuth2Mixin(object): @return_future def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None, - callback=None): + callback=None, scope=None, response_type="code"): """Redirects the user to obtain OAuth authorization for this service. Some providers require that you register a redirect URL with @@ -566,10 +566,13 @@ class OAuth2Mixin(object): """ args = { "redirect_uri": redirect_uri, - "client_id": client_id + "client_id": client_id, + "response_type": response_type } if extra_params: args.update(extra_params) + if scope: + args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) callback() @@ -945,6 +948,67 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): return OpenIdMixin.get_authenticated_user(self) +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2.""" + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = 'google_oauth' + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Google user, returning a user object. + + Example usage:: + + class GoogleOAuth2LoginHandler(LoginHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.asynchronous + @tornado.gen.coroutine + def get(self): + if self.get_argument("code", False): + user = yield self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + yield self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings["google_consumer_key"], + scope=['openid', 'email'], + response_type='code', + extra_params={"approval_prompt": "auto"}) + """ + http = self.get_auth_http_client() + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, + "code": code, + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], + "grant_type": "authorization_code", + }) + + http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + self.async_callback(self._on_access_token, callback), + method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) + + def _on_access_token(self, future, response): + """Callback function for the exchange to the access token.""" + if response.error: + future.set_exception(AuthError('Google auth error: %s' % str(response))) + return + + args = escape.json_decode(response.body) + future.set_result(args) + + def get_auth_http_client(self): + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + class FacebookMixin(object): """Facebook Connect authentication. diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 05754299..79cccb49 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -16,11 +16,15 @@ """xAutomatically restart the server when a source file is modified. -Most applications should not access this module directly. Instead, pass the -keyword argument ``debug=True`` to the `tornado.web.Application` constructor. -This will enable autoreload mode as well as checking for changes to templates -and static resources. Note that restarting is a destructive operation -and any requests in progress will be aborted when the process restarts. +Most applications should not access this module directly. Instead, +pass the keyword argument ``autoreload=True`` to the +`tornado.web.Application` constructor (or ``debug=True``, which +enables this setting and several others). This will enable autoreload +mode as well as checking for changes to templates and static +resources. Note that restarting is a destructive operation and any +requests in progress will be aborted when the process restarts. (If +you want to disable autoreload while using other debug-mode features, +pass both ``debug=True`` and ``autoreload=False``). This module can also be used as a command-line wrapper around scripts such as unit test runners. See the `main` method for details. @@ -38,6 +42,7 @@ Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) because it re-executes Python using ``sys.executable`` and ``sys.argv``. Additionally, modifying these variables will cause reloading to behave incorrectly. + """ from __future__ import absolute_import, division, print_function, with_statement diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index e0900569..cb97710a 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -360,6 +360,7 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.PROXYUSERPWD, credentials) else: curl.setopt(pycurl.PROXY, '') + curl.unsetopt(pycurl.PROXYUSERPWD) if request.validate_cert: curl.setopt(pycurl.SSL_VERIFYPEER, 1) curl.setopt(pycurl.SSL_VERIFYHOST, 2) @@ -382,6 +383,8 @@ def _curl_setup_request(curl, request, buffer, headers): # that we can't reach, so allow ipv6 unless the user asks to disable. # (but see version check in _process_queue above) curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + else: + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER) # Set the request method through curl's irritating interface which makes # up names for almost every single method @@ -404,6 +407,11 @@ def _curl_setup_request(curl, request, buffer, headers): # Handle curl's cryptic options for every individual HTTP method if request.method in ("POST", "PUT"): + if request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % request.method) + request_buffer = BytesIO(utf8(request.body)) curl.setopt(pycurl.READFUNCTION, request_buffer.read) if request.method == "POST": @@ -414,6 +422,9 @@ def _curl_setup_request(curl, request, buffer, headers): curl.setopt(pycurl.POSTFIELDSIZE, len(request.body)) else: curl.setopt(pycurl.INFILESIZE, len(request.body)) + elif request.method == "GET": + if request.body is not None: + raise AssertionError('Body must be empty for GET request') if request.auth_username is not None: userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index 92b7458e..217ebdf5 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -38,8 +38,8 @@ since it is both shorter and provides better exception handling):: def get(self): yield gen.Task(AsyncHTTPClient().fetch, "http://example.com") -You can also yield a list of ``Futures`` and/or ``Tasks``, which will be -started at the same time and run in parallel; a list of results will +You can also yield a list or dict of ``Futures`` and/or ``Tasks``, which will be +started at the same time and run in parallel; a list or dict of results will be returned when they are all finished:: @gen.coroutine @@ -47,6 +47,13 @@ be returned when they are all finished:: http_client = AsyncHTTPClient() response1, response2 = yield [http_client.fetch(url1), http_client.fetch(url2)] + response_dict = yield dict(response3=http_client.fetch(url3), + response4=http_client.fetch(url4)) + response3 = response_dict['response3'] + response4 = response_dict['response4'] + +.. versionchanged:: 3.2 + Dict support added. For more complicated interfaces, `Task` can be split into two parts: `Callback` and `Wait`:: @@ -404,6 +411,10 @@ class Multi(YieldPoint): a list of ``YieldPoints``. """ def __init__(self, children): + self.keys = None + if isinstance(children, dict): + self.keys = list(children.keys()) + children = children.values() self.children = [] for i in children: if isinstance(i, Future): @@ -423,7 +434,11 @@ class Multi(YieldPoint): return not self.unfinished_children def get_result(self): - return [i.get_result() for i in self.children] + result = (i.get_result() for i in self.children) + if self.keys is not None: + return dict(zip(self.keys, result)) + else: + return list(result) class _NullYieldPoint(YieldPoint): @@ -523,7 +538,7 @@ class Runner(object): self.finished = True self.yield_point = _null_yield_point raise - if isinstance(yielded, list): + if isinstance(yielded, (list, dict)): yielded = Multi(yielded) elif isinstance(yielded, Future): yielded = YieldFuture(yielded) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 67675894..b58a8348 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -282,7 +282,8 @@ class HTTPRequest(object): :arg int max_redirects: Limit for ``follow_redirects`` :arg string user_agent: String to send as ``User-Agent`` header :arg bool use_gzip: Request gzip encoding from the server - :arg string network_interface: Network interface to use for request + :arg string network_interface: Network interface to use for request. + ``curl_httpclient`` only; see note below. :arg callable streaming_callback: If set, ``streaming_callback`` will be run with each chunk of data as it is received, and ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in @@ -310,14 +311,26 @@ class HTTPRequest(object): :arg bool validate_cert: For HTTPS requests, validate the server's certificate? :arg string ca_certs: filename of CA certificates in PEM format, - or None to use defaults. Note that in ``curl_httpclient``, if - any request uses a custom ``ca_certs`` file, they all must (they - don't have to all use the same ``ca_certs``, but it's not possible - to mix requests with ``ca_certs`` and requests that use the defaults. + or None to use defaults. See note below when used with + ``curl_httpclient``. :arg bool allow_ipv6: Use IPv6 when available? Default is false in ``simple_httpclient`` and true in ``curl_httpclient`` - :arg string client_key: Filename for client SSL key, if any - :arg string client_cert: Filename for client SSL certificate, if any + :arg string client_key: Filename for client SSL key, if any. See + note below when used with ``curl_httpclient``. + :arg string client_cert: Filename for client SSL certificate, if any. + See note below when used with ``curl_httpclient``. + + .. note:: + + When using ``curl_httpclient`` certain options may be + inherited by subsequent fetches because ``pycurl`` does + not allow them to be cleanly reset. This applies to the + ``ca_certs``, ``client_key``, ``client_cert``, and + ``network_interface`` arguments. If you use these + options, you should pass them on every request (you don't + have to always use the same values, but it's not possible + to mix requests that specify these options with ones that + use the defaults). .. versionadded:: 3.1 The ``auth_mode`` argument. @@ -372,6 +385,9 @@ class HTTPResponse(object): * headers: `tornado.httputil.HTTPHeaders` object + * effective_url: final location of the resource after following any + redirects + * buffer: ``cStringIO`` object for response body * body: response body as string (created on demand from ``self.buffer``) diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index d005545e..34e7b768 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -29,6 +29,7 @@ from __future__ import absolute_import, division, print_function, with_statement import socket import ssl import time +import copy from tornado.escape import native_str, parse_qs_bytes from tornado import httputil @@ -326,8 +327,8 @@ class HTTPConnection(object): self.request_callback(self._request) except _BadRequestException as e: - gen_log.info("Malformed HTTP request from %s: %s", - self.address[0], e) + gen_log.info("Malformed HTTP request from %r: %s", + self.address, e) self.close() return @@ -336,7 +337,10 @@ class HTTPConnection(object): if self._request.method in ("POST", "PATCH", "PUT"): httputil.parse_body_arguments( self._request.headers.get("Content-Type", ""), data, - self._request.arguments, self._request.files) + self._request.body_arguments, self._request.files) + + for k, v in self._request.body_arguments.items(): + self._request.arguments.setdefault(k, []).extend(v) self.request_callback(self._request) @@ -403,6 +407,20 @@ class HTTPRequest(object): `.RequestHandler.get_argument`, which returns argument values as unicode strings. + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + .. attribute:: files File uploads are available in the files property, which maps file @@ -457,6 +475,8 @@ class HTTPRequest(object): self.path, sep, self.query = uri.partition('?') self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 3e7337d9..2575bc56 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -320,7 +320,11 @@ def parse_body_arguments(content_type, body, arguments, files): with the parsed contents. """ if content_type.startswith("application/x-www-form-urlencoded"): - uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + try: + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + except Exception as e: + gen_log.warning('Invalid x-www-form-urlencoded body: %s', e) + uri_arguments = {} for name, values in uri_arguments.items(): if values: arguments.setdefault(name, []).extend(values) diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 91ee2c5b..a36ab7a5 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -676,8 +676,7 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - if self._handlers.has_key(fd): - self._handlers[fd](fd, events) + self._handlers[fd](fd, events) except (OSError, IOError) as e: if e.args[0] == errno.EPIPE: # Happens when the client closes the connection diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 6bdc6397..08430cea 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -774,7 +774,7 @@ class IOStream(BaseIOStream): # Sometimes setsockopt will fail if the socket is closed # at the wrong time. This can happen with HTTPServer # resetting the value to false between requests. - if e.errno != errno.EINVAL: + if e.errno not in (errno.EINVAL, errno.ECONNRESET): raise diff --git a/libs/tornado/log.py b/libs/tornado/log.py index fa11f379..648db5c6 100755 --- a/libs/tornado/log.py +++ b/libs/tornado/log.py @@ -51,7 +51,7 @@ gen_log = logging.getLogger("tornado.general") def _stderr_supports_color(): color = False - if curses and sys.stderr.isatty(): + if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 9dc8506e..21db4755 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -20,7 +20,6 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os -import re import socket import ssl import stat @@ -30,6 +29,13 @@ from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec from tornado.util import Configurable +if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ + ssl_match_hostname = ssl.match_hostname + SSLCertificateError = ssl.CertificateError +else: + import backports.ssl_match_hostname + ssl_match_hostname = backports.ssl_match_hostname.match_hostname + SSLCertificateError = backports.ssl_match_hostname.CertificateError def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. @@ -391,73 +397,3 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): return context.wrap_socket(socket, **kwargs) else: return ssl.wrap_socket(socket, **dict(context, **kwargs)) - -if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ - ssl_match_hostname = ssl.match_hostname - SSLCertificateError = ssl.CertificateError -else: - # match_hostname was added to the standard library ssl module in python 3.2. - # The following code was backported for older releases and copied from - # https://bitbucket.org/brandon/backports.ssl_match_hostname - class SSLCertificateError(ValueError): - pass - - def _dnsname_to_pat(dn, max_wildcards=1): - pats = [] - for frag in dn.split(r'.'): - if frag.count('*') > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survery of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise SSLCertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - if frag == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - else: - # Otherwise, '*' matches any dotless fragment. - frag = re.escape(frag) - pats.append(frag.replace(r'\*', '[^.]*')) - return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - - def ssl_match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules - are mostly followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate") - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_to_pat(value).match(hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise SSLCertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise SSLCertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise SSLCertificateError("no appropriate commonName or " - "subjectAltName fields were found") diff --git a/libs/tornado/platform/asyncio.py b/libs/tornado/platform/asyncio.py new file mode 100644 index 00000000..a8f5bad4 --- /dev/null +++ b/libs/tornado/platform/asyncio.py @@ -0,0 +1,134 @@ +"""Bridges between the `asyncio` module and Tornado IOLoop. + +This is a work in progress and interfaces are subject to change. + +To test: +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOLoop +python3.4 -m tornado.test.runtests --ioloop=tornado.platform.asyncio.AsyncIOMainLoop +(the tests log a few warnings with AsyncIOMainLoop because they leave some +unfinished callbacks on the event loop that fail when it resumes) +""" +import asyncio +import datetime +import functools +import os + +from tornado.ioloop import IOLoop +from tornado import stack_context + +class BaseAsyncIOLoop(IOLoop): + def initialize(self, asyncio_loop, close_loop=False): + self.asyncio_loop = asyncio_loop + self.close_loop = close_loop + self.asyncio_loop.call_soon(self.make_current) + # Maps fd to handler function (as in IOLoop.add_handler) + self.handlers = {} + # Set of fds listening for reads/writes + self.readers = set() + self.writers = set() + self.closing = False + + def close(self, all_fds=False): + self.closing = True + for fd in list(self.handlers): + self.remove_handler(fd) + if all_fds: + os.close(fd) + if self.close_loop: + self.asyncio_loop.close() + + def add_handler(self, fd, handler, events): + if fd in self.handlers: + raise ValueError("fd %d added twice" % fd) + self.handlers[fd] = stack_context.wrap(handler) + if events & IOLoop.READ: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + if events & IOLoop.WRITE: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + + def update_handler(self, fd, events): + if events & IOLoop.READ: + if fd not in self.readers: + self.asyncio_loop.add_reader( + fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + else: + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if events & IOLoop.WRITE: + if fd not in self.writers: + self.asyncio_loop.add_writer( + fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + else: + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + + def remove_handler(self, fd): + if fd not in self.handlers: + return + if fd in self.readers: + self.asyncio_loop.remove_reader(fd) + self.readers.remove(fd) + if fd in self.writers: + self.asyncio_loop.remove_writer(fd) + self.writers.remove(fd) + del self.handlers[fd] + + def _handle_events(self, fd, events): + self.handlers[fd](fd, events) + + def start(self): + self.asyncio_loop.run_forever() + + def stop(self): + self.asyncio_loop.stop() + + def _run_callback(self, callback, *args, **kwargs): + try: + callback(*args, **kwargs) + except Exception: + self.handle_callback_exception(callback) + + def add_timeout(self, deadline, callback): + if isinstance(deadline, (int, float)): + delay = max(deadline - self.time(), 0) + elif isinstance(deadline, datetime.timedelta): + delay = deadline.total_seconds() + else: + raise TypeError("Unsupported deadline %r", deadline) + return self.asyncio_loop.call_later(delay, self._run_callback, + stack_context.wrap(callback)) + + def remove_timeout(self, timeout): + timeout.cancel() + + def add_callback(self, callback, *args, **kwargs): + if self.closing: + raise RuntimeError("IOLoop is closing") + if kwargs: + self.asyncio_loop.call_soon_threadsafe(functools.partial( + self._run_callback, stack_context.wrap(callback), + *args, **kwargs)) + else: + self.asyncio_loop.call_soon_threadsafe( + self._run_callback, stack_context.wrap(callback), *args) + + add_callback_from_signal = add_callback + + +class AsyncIOMainLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(), + close_loop=False) + +class AsyncIOLoop(BaseAsyncIOLoop): + def initialize(self): + super(AsyncIOLoop, self).initialize(asyncio.new_event_loop(), + close_loop=True) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index ffd2d29d..942c5c3f 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -92,7 +92,8 @@ def fork_processes(num_processes, max_restarts=100): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the debug=True option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``fork_processes``. diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index d8dbb271..2558ada8 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -72,6 +72,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.max_clients = max_clients self.queue = collections.deque() self.active = {} + self.waiting = {} self.max_buffer_size = max_buffer_size if resolver: self.resolver = resolver @@ -89,7 +90,16 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.resolver.close() def fetch_impl(self, request, callback): - self.queue.append((request, callback)) + key = object() + self.queue.append((key, request, callback)) + if not len(self.active) < self.max_clients: + timeout_handle = self.io_loop.add_timeout( + self.io_loop.time() + min(request.connect_timeout, + request.request_timeout), + functools.partial(self._on_timeout, key)) + else: + timeout_handle = None + self.waiting[key] = (request, callback, timeout_handle) self._process_queue() if self.queue: gen_log.debug("max_clients limit reached, request queued. " @@ -99,8 +109,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _process_queue(self): with stack_context.NullContext(): while self.queue and len(self.active) < self.max_clients: - request, callback = self.queue.popleft() - key = object() + key, request, callback = self.queue.popleft() + if key not in self.waiting: + continue + self._remove_timeout(key) self.active[key] = (request, callback) release_callback = functools.partial(self._release_fetch, key) self._handle_request(request, release_callback, callback) @@ -113,6 +125,22 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): del self.active[key] self._process_queue() + def _remove_timeout(self, key): + if key in self.waiting: + request, callback, timeout_handle = self.waiting[key] + if timeout_handle is not None: + self.io_loop.remove_timeout(timeout_handle) + del self.waiting[key] + + def _on_timeout(self, key): + request, callback, timeout_handle = self.waiting[key] + self.queue.remove((key, request, callback)) + timeout_response = HTTPResponse( + request, 599, error=HTTPError(599, "Timeout"), + request_time=self.io_loop.time() - request.start_time) + self.io_loop.add_callback(callback, timeout_response) + del self.waiting[key] + class _HTTPConnection(object): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) @@ -162,15 +190,18 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) self.resolver.resolve(host, port, af, callback=self._on_resolve) def _on_resolve(self, addrinfo): + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout + return self.stream = self._create_stream(addrinfo) - timeout = min(self.request.connect_timeout, self.request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) self.stream.set_close_callback(self._on_close) # ipv6 addresses are broken (in self.parsed.hostname) until # 2.7, here is correctly parsed value calculated in __init__ @@ -199,10 +230,10 @@ class _HTTPConnection(object): # the SSL_OP_NO_SSLv2, but that wasn't exposed to python # until 3.2. Python 2.7 adds the ciphers argument, which # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to SSLv3. This is + # on python 2.6, we set ssl_version to TLSv1. This is # more narrow than we'd like since it also breaks - # compatibility with servers configured for TLSv1 only, - # but nearly all servers support SSLv3: + # compatibility with servers configured for SSLv3 only, + # but nearly all servers support both SSLv3 and TLSv1: # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html if sys.version_info >= (2, 7): ssl_options["ciphers"] = "DEFAULT:!SSLv2" @@ -210,7 +241,7 @@ class _HTTPConnection(object): # This is really only necessary for pre-1.0 versions # of openssl, but python 2.6 doesn't expose version # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 return SSLIOStream(socket.socket(af), io_loop=self.io_loop, @@ -233,6 +264,8 @@ class _HTTPConnection(object): def _on_connect(self): self._remove_timeout() + if self.final_callback is None: + return if self.request.request_timeout: self._timeout = self.io_loop.add_timeout( self.start_time + self.request.request_timeout, @@ -269,9 +302,15 @@ class _HTTPConnection(object): self.request.headers["User-Agent"] = self.request.user_agent if not self.request.allow_nonstandard_methods: if self.request.method in ("POST", "PATCH", "PUT"): - assert self.request.body is not None + if self.request.body is None: + raise AssertionError( + 'Body must not be empty for "%s" request' + % self.request.method) else: - assert self.request.body is None + if self.request.body is not None: + raise AssertionError( + 'Body must be empty for "%s" request' + % self.request.method) if self.request.body is not None: self.request.headers["Content-Length"] = str(len( self.request.body)) diff --git a/libs/tornado/speedups.c b/libs/tornado/speedups.c new file mode 100644 index 00000000..8a316c58 --- /dev/null +++ b/libs/tornado/speedups.c @@ -0,0 +1,49 @@ +#include + +static PyObject* websocket_mask(PyObject* self, PyObject* args) { + const char* mask; + int mask_len; + const char* data; + int data_len; + int i; + + if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { + return NULL; + } + + PyObject* result = PyBytes_FromStringAndSize(NULL, data_len); + if (!result) { + return NULL; + } + char* buf = PyBytes_AsString(result); + for (i = 0; i < data_len; i++) { + buf[i] = data[i] ^ mask[i % 4]; + } + + return result; +} + +static PyMethodDef methods[] = { + {"websocket_mask", websocket_mask, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef speedupsmodule = { + PyModuleDef_HEAD_INIT, + "speedups", + NULL, + -1, + methods +}; + +PyMODINIT_FUNC +PyInit_speedups() { + return PyModule_Create(&speedupsmodule); +} +#else // Python 2.x +PyMODINIT_FUNC +initspeedups() { + Py_InitModule("tornado.speedups", methods); +} +#endif diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index 8473a21a..c0773732 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -180,7 +180,8 @@ class TCPServer(object): between any server code. Note that multiple processes are not compatible with the autoreload - module (or the ``debug=True`` option to `tornado.web.Application`). + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). When using multiple processes, no IOLoops can be created or referenced until after the call to ``TCPServer.start(n)``. """ diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 5f8d6091..b6d7e97e 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -250,7 +250,7 @@ class RequestHandler(object): not self.request.connection.no_keep_alive): conn_header = self.request.headers.get("Connection") if conn_header and (conn_header.lower() == "keep-alive"): - self.set_header("Connection", "Keep-Alive") + self._headers["Connection"] = "Keep-Alive" self._write_buffer = [] self._status_code = 200 self._reason = httputil.responses[200] @@ -348,12 +348,7 @@ class RequestHandler(object): The returned value is always unicode. """ - args = self.get_arguments(name, strip=strip) - if not args: - if default is self._ARG_DEFAULT: - raise MissingArgumentError(name) - return default - return args[-1] + return self._get_argument(name, default, self.request.arguments, strip) def get_arguments(self, name, strip=True): """Returns a list of the arguments with the given name. @@ -362,9 +357,73 @@ class RequestHandler(object): The returned values are always unicode. """ + return self._get_arguments(name, self.request.arguments, strip) + def get_body_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request body. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.body_arguments, strip) + + def get_body_arguments(self, name, strip=True): + """Returns a list of the body arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.body_arguments, strip) + + def get_query_argument(self, name, default=_ARG_DEFAULT, strip=True): + """Returns the value of the argument with the given name + from the request query string. + + If default is not provided, the argument is considered to be + required, and we raise a `MissingArgumentError` if it is missing. + + If the argument appears in the url more than once, we return the + last value. + + The returned value is always unicode. + + .. versionadded:: 3.2 + """ + return self._get_argument(name, default, self.request.query_arguments, strip) + + def get_query_arguments(self, name, strip=True): + """Returns a list of the query arguments with the given name. + + If the argument is not present, returns an empty list. + + The returned values are always unicode. + + .. versionadded:: 3.2 + """ + return self._get_arguments(name, self.request.query_arguments, strip) + + def _get_argument(self, name, default, source, strip=True): + args = self._get_arguments(name, source, strip=strip) + if not args: + if default is self._ARG_DEFAULT: + raise MissingArgumentError(name) + return default + return args[-1] + + def _get_arguments(self, name, source, strip=True): values = [] - for v in self.request.arguments.get(name, []): + for v in source.get(name, []): v = self.decode_argument(v, name=name) if isinstance(v, unicode_type): # Get rid of any weird control chars (unless decoding gave @@ -838,7 +897,7 @@ class RequestHandler(object): else: self.finish(self.get_error_html(status_code, **kwargs)) return - if self.settings.get("debug") and "exc_info" in kwargs: + if self.settings.get("serve_traceback") and "exc_info" in kwargs: # in debug mode, try to send a traceback self.set_header('Content-Type', 'text/plain') for line in traceback.format_exception(*kwargs["exc_info"]): @@ -1318,6 +1377,12 @@ def asynchronous(method): if not self._finished: self.finish() IOLoop.current().add_future(result, future_complete) + # Once we have done this, hide the Future from our + # caller (i.e. RequestHandler._when_complete), which + # would otherwise set up its own callback and + # exception handler (resulting in exceptions being + # logged twice). + return None return result return wrapper @@ -1383,10 +1448,16 @@ class Application(object): or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. + The request class can be specified as either a class object or a + (fully-qualified) name. - Each tuple can contain an optional third element, which should be - a dictionary if it is present. That dictionary is passed as - keyword arguments to the contructor of the handler. This pattern + Each tuple can contain additional elements, which correspond to the + arguments to the `URLSpec` constructor. (Prior to Tornado 3.2, this + only tuples of two or three elements were allowed). + + A dictionary may be passed as the third element of the tuple, + which will be used as keyword arguments to the handler's + constructor and `~RequestHandler.initialize` method. This pattern is used for the `StaticFileHandler` in this example (note that a `StaticFileHandler` can be installed automatically with the static_path setting described below):: @@ -1409,6 +1480,7 @@ class Application(object): and ``/robots.txt`` from the same directory. A custom subclass of `StaticFileHandler` can be specified with the ``static_handler_class`` setting. + """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1447,8 +1519,14 @@ class Application(object): if handlers: self.add_handlers(".*$", handlers) + if self.settings.get('debug'): + self.settings.setdefault('autoreload', True) + self.settings.setdefault('compiled_template_cache', False) + self.settings.setdefault('static_hash_cache', False) + self.settings.setdefault('serve_traceback', True) + # Automatically reload modified modules - if self.settings.get("debug") and not wsgi: + if self.settings.get('autoreload') and not wsgi: from tornado import autoreload autoreload.start() @@ -1493,20 +1571,8 @@ class Application(object): for spec in host_handlers: if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3) - pattern = spec[0] - handler = spec[1] - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - if len(spec) == 3: - kwargs = spec[2] - else: - kwargs = {} - spec = URLSpec(pattern, handler, kwargs) + assert len(spec) in (2, 3, 4) + spec = URLSpec(*spec) handlers.append(spec) if spec.name: if spec.name in self.named_handlers: @@ -1597,14 +1663,23 @@ class Application(object): args = [unquote(s) for s in match.groups()] break if not handler: - handler = ErrorHandler(self, request, status_code=404) + if self.settings.get('default_handler_class'): + handler_class = self.settings['default_handler_class'] + handler_args = self.settings.get( + 'default_handler_args', {}) + else: + handler_class = ErrorHandler + handler_args = dict(status_code=404) + handler = handler_class(self, request, **handler_args) - # In debug mode, re-compile templates and reload static files on every + # If template cache is disabled (usually in the debug mode), + # re-compile templates and reload static files on every # request so you don't need to restart to see changes - if self.settings.get("debug"): + if not self.settings.get("compiled_template_cache", True): with RequestHandler._template_loader_lock: for loader in RequestHandler._template_loaders.values(): loader.reset() + if not self.settings.get('static_hash_cache', True): StaticFileHandler.reset() handler._execute(transforms, *args, **kwargs) @@ -2454,7 +2529,7 @@ class _UIModuleNamespace(object): class URLSpec(object): """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler_class, kwargs=None, name=None): + def __init__(self, pattern, handler, kwargs=None, name=None): """Parameters: * ``pattern``: Regular expression to be matched. Any groups @@ -2475,7 +2550,13 @@ class URLSpec(object): assert len(self.regex.groupindex) in (0, self.regex.groups), \ ("groups in url regexes must either be all named or all " "positional: %r" % self.regex.pattern) - self.handler_class = handler_class + + if isinstance(handler, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + handler = import_object(handler) + + self.handler_class = handler self.kwargs = kwargs or {} self.name = name self._path, self._group_count = self._find_groups() diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 676d21bf..8c2f5a64 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -33,7 +33,7 @@ import tornado.web from tornado.concurrent import TracebackFuture from tornado.escape import utf8, native_str -from tornado import httpclient +from tornado import httpclient, httputil from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log @@ -52,6 +52,10 @@ class WebSocketError(Exception): class WebSocketClosedError(WebSocketError): + """Raised by operations on a closed connection. + + .. versionadded:: 3.2 + """ pass @@ -163,6 +167,12 @@ class WebSocketHandler(tornado.web.RequestHandler): encoded as json). If the ``binary`` argument is false, the message will be sent as utf8; in binary mode any byte string is allowed. + + If the connection is already closed, raises `WebSocketClosedError`. + + .. versionchanged:: 3.2 + `WebSocketClosedError` was added (previously a closed connection + would raise an `AttributeError`) """ if self.ws_connection is None: raise WebSocketClosedError() @@ -586,7 +596,7 @@ class WebSocketProtocol13(WebSocketProtocol): frame += struct.pack("!BQ", 127 | mask_bit, l) if self.mask_outgoing: mask = os.urandom(4) - data = mask + self._apply_mask(mask, data) + data = mask + _websocket_mask(mask, data) frame += data self.stream.write(frame) @@ -671,21 +681,8 @@ class WebSocketProtocol13(WebSocketProtocol): except StreamClosedError: self._abort() - def _apply_mask(self, mask, data): - mask = array.array("B", mask) - unmasked = array.array("B", data) - for i in xrange(len(data)): - unmasked[i] = unmasked[i] ^ mask[i % 4] - if hasattr(unmasked, 'tobytes'): - # tostring was deprecated in py32. It hasn't been removed, - # but since we turn on deprecation warnings in our tests - # we need to use the right one. - return unmasked.tobytes() - else: - return unmasked.tostring() - def _on_masked_frame_data(self, data): - self._on_frame_data(self._apply_mask(self._frame_mask, data)) + self._on_frame_data(_websocket_mask(self._frame_mask, data)) def _on_frame_data(self, data): if self._frame_opcode_is_control: @@ -771,7 +768,11 @@ class WebSocketProtocol13(WebSocketProtocol): class WebSocketClientConnection(simple_httpclient._HTTPConnection): - """WebSocket client connection.""" + """WebSocket client connection. + + This class should not be instantiated directly; use the + `websocket_connect` function instead. + """ def __init__(self, io_loop, request): self.connect_future = TracebackFuture() self.read_future = None @@ -793,9 +794,19 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): io_loop, None, request, lambda: None, self._on_http_response, 104857600, self.resolver) + def close(self): + """Closes the websocket connection. + + .. versionadded:: 3.2 + """ + if self.protocol is not None: + self.protocol.close() + self.protocol = None + def _on_close(self): self.on_message(None) self.resolver.close() + super(WebSocketClientConnection, self)._on_close() def _on_http_response(self, response): if not self.connect_future.done(): @@ -859,13 +870,54 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): Takes a url and returns a Future whose result is a `WebSocketClientConnection`. + + .. versionchanged:: 3.2 + Also accepts ``HTTPRequest`` objects in place of urls. """ if io_loop is None: io_loop = IOLoop.current() - request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) + if isinstance(url, httpclient.HTTPRequest): + assert connect_timeout is None + request = url + # Copy and convert the headers dict/object (see comments in + # AsyncHTTPClient.fetch) + request.headers = httputil.HTTPHeaders(request.headers) + else: + request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) request = httpclient._RequestProxy( request, httpclient.HTTPRequest._DEFAULTS) conn = WebSocketClientConnection(io_loop, request) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future + +def _websocket_mask_python(mask, data): + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. + Returns a `bytes` object of the same length as `data` with the mask applied + as specified in section 5.3 of RFC 6455. + + This pure-python implementation may be replaced by an optimized version when available. + """ + mask = array.array("B", mask) + unmasked = array.array("B", data) + for i in xrange(len(data)): + unmasked[i] = unmasked[i] ^ mask[i % 4] + if hasattr(unmasked, 'tobytes'): + # tostring was deprecated in py32. It hasn't been removed, + # but since we turn on deprecation warnings in our tests + # we need to use the right one. + return unmasked.tobytes() + else: + return unmasked.tostring() + +if os.environ.get('TORNADO_NO_EXTENSION'): + # This environment variable exists to make it easier to do performance comparisons; + # it's not guaranteed to remain supported in the future. + _websocket_mask = _websocket_mask_python +else: + try: + from tornado.speedups import websocket_mask as _websocket_mask + except ImportError: + _websocket_mask = _websocket_mask_python diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 5e25a564..8e5ddedb 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -33,6 +33,7 @@ from __future__ import absolute_import, division, print_function, with_statement import sys import time +import copy import tornado from tornado import escape @@ -142,11 +143,14 @@ class HTTPRequest(object): self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) self.uri = self.path self.arguments = {} + self.query_arguments = {} + self.body_arguments = {} self.query = environ.get("QUERY_STRING", "") if self.query: self.uri += "?" + self.query self.arguments = parse_qs_bytes(native_str(self.query), keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) self.version = "HTTP/1.1" self.headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): @@ -171,7 +175,10 @@ class HTTPRequest(object): # Parse request body self.files = {} httputil.parse_body_arguments(self.headers.get("Content-Type", ""), - self.body, self.arguments, self.files) + self.body, self.body_arguments, self.files) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) self._start_time = time.time() self._finish_time = None
" - for child in self.childNodes: - result += child.hilite() - return result.encode(encoding) + "
<!DOCTYPE %s>
%s
"%s"
<!--%s-->