#!/usr/bin/python # Copyright 2015 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Module for parsing TCG TPM2 library specification in HTML format. This module processes parts 2 and 3 of the specification, extracting information related to tables defined in the documents, feeding the information into the Table object for further processing and creating the appropriate TPM2 objects. """ from __future__ import print_function import HTMLParser import os import re import sys import tpm_table table_name = re.compile(r'^\s*Table\s+[0-9]+') class SpecParser(HTMLParser.HTMLParser): """A class for parsing TCG specifications in html format.""" # The state machine of the parser could be in one of the following states. ANCHOR = 0 # Look for table title anchor TABLE_NAME = 1 # Look for table title in the data stream TABLE_BODY = 2 # Scraping the actual table body MAYBE_DONE = 3 # Could be over, unless a single spec table is split in # multiple HTML tables (to continue on the next page) SKIP_HEADER = 4 # Ignore the header of the split tables def __init__(self): """Initialize a parser object to default state.""" HTMLParser.HTMLParser.__init__(self) self._state = self.ANCHOR self._title = '' self._table = tpm_table.Table() self._previous_table_number = 0 # Used to check if there are skipped tables def _Normalize(self, data): """Normalize HTML data. HTML files generated from TCG specifications sometimes include utf8 characters (like long dashes), which appear only in comments/table titles and can be safely ignored. Args: data: a string representing portion of data from the HTML being parsed. Returns: a string, the input data with characters above ASCII printable range excluded. """ return ' ' + ''.join(x for x in self.unescape(data) if ord(x) < 128) def GetTable(self): """Return the Table object containing all information parsed so far.""" return self._table def _SetState(self, new_state): if self._state != new_state: self._state = new_state if new_state == self.TABLE_NAME: self._title = '' def handle_starttag(self, tag, attrs): """Invoked each time a new HTML tag is opened. This method drives changes in the parser FSM states, its heuristics are derived from the format of the HTML files the TCG specs get converted to. Each specification table is preceded with a tittle. The title is wrapped in an anchor tag with a property 'name' set to 'bookmark#xxx. The title text starts with ' Table [0-9]+ '. Once the table title is detected, the state machine switches to looking for the actual HTML table, i.e. tags 'table', 'tr' and 'td' (the generated specs do not use the 'th' tags). Large specification tables can be split into multiple HTML tables (so that they fit in a page). This is why the presence of the closing 'table' tag is not enough to close the parsing of the current specification table. In some cases the next table is defined in the spec immediately after the current one - this is when the new anchor tag is used as a signal that the previous table has been completely consumed. Args: tag: a string, the HTML tag attrs: a tuple of zero or more two-string tuples, the first element - the HTML tag's attribute, the second element - the attribute value. """ if tag == 'a': if [x for x in attrs if x[0] == 'name' and x[1].startswith('bookmark')]: if self._state == self.ANCHOR: self._SetState(self.TABLE_NAME) elif self._state == self.MAYBE_DONE: # Done indeed self._table.ProcessTable() self._table.Init() self._SetState(self.TABLE_NAME) elif self._state == self.TABLE_NAME: self._title = '' elif tag == 'p' and self._state == self.TABLE_NAME and not self._title: # This was not a valid table start, back to looking for the right anchor. self._SetState(self.ANCHOR) elif self._state == self.TABLE_NAME and tag == 'table': if not table_name.search(self._title): # Table title does not match the expected format - back to square one. self._SetState(self.ANCHOR) return # will have to start over table_number = int(self._title.split()[1]) self._previous_table_number += 1 if table_number > self._previous_table_number: print('Table(s) %s missing' % ' '.join( '%d' % x for x in range(self._previous_table_number, table_number)), file=sys.stderr) self._previous_table_number = table_number self._table.Init(self._title) self._SetState(self.TABLE_BODY) elif self._state == self.MAYBE_DONE and tag == 'tr': self._SetState(self.SKIP_HEADER) elif self._state == self.SKIP_HEADER and tag == 'tr': self._SetState(self.TABLE_BODY) self._table.NewRow() elif self._state == self.TABLE_BODY: if tag == 'tr': self._table.NewRow() elif tag == 'td': self._table.NewCell() def handle_endtag(self, tag): """Invoked each time an HTML tag is closed.""" if tag == 'table' and self._table.InProgress(): self._SetState(self.MAYBE_DONE) def handle_data(self, data): """Process data outside HTML tags.""" if self._state == self.TABLE_NAME: self._title += ' %s' % self._Normalize(data) elif self._state == self.TABLE_BODY: self._table.AddData(self._Normalize(data)) elif self._state == self.MAYBE_DONE: # Done indeed self._table.ProcessTable() self._table.Init() self._SetState(self.ANCHOR) def close(self): """Finish processing of the HTML buffer.""" if self._state in (self.TABLE_BODY, self.MAYBE_DONE): self._table.ProcessTable() self._state = self.ANCHOR def handle_entityref(self, name): """Process HTML escape sequence.""" entmap = { 'amp': '&', 'gt': '>', 'lt': '<', 'quot': '"', } if name in entmap: if self._state == self.TABLE_BODY: self._table.AddData(entmap[name]) elif self._state == self.TABLE_NAME: self._title += entmap[name] def main(structs_html_file_name): """When invoked standalone - dump .h file on the console.""" parser = SpecParser() with open(structs_html_file_name) as input_file: html_content = input_file.read() parser.feed(html_content) parser.close() print(parser.GetTable().GetHFile()) if __name__ == '__main__': if len(sys.argv) != 2: print('%s: One parameter is required, the name of the html file ' 'which is the TPM2 library Part 2 specification' % os.path.basename(sys.argv[0]), file=sys.stderr) sys.exit(1) main(sys.argv[1])