Update automated documentation tool.

Add the ability to include notes, and add a note to ChaCha20
specifying the exact ChaCha variant that's included.

Prefer the existing name over the name coming from the device,
allowing us to customize names by updating them in the JSON file.  Use
AES/GCM/NoPadding over AES/GCM/NOPADDING to match other ciphers.

Wrap a number of fields with white-space:nowrap, so that algorithm
names and other identifiers aren't awkwardly broken in the middle,
like in OAEPwithSHA-1andMGF1Padding.

Add the ability to only output a single category, to more easily allow
updating Javadoc for a single class.

Bug: 70936054
Test: update_crypto_support_test.py
Change-Id: I4b2bb59adca7b003e357776783ee723bdf82302c
diff --git a/tools/docs/crypto/data/crypto_support.json b/tools/docs/crypto/data/crypto_support.json
index 8730405..ce5f793 100644
--- a/tools/docs/crypto/data/crypto_support.json
+++ b/tools/docs/crypto/data/crypto_support.json
@@ -227,7 +227,7 @@
           "supported_api_levels": "1+"
         },
         {
-          "name": "AES/GCM/NOPADDING",
+          "name": "AES/GCM/NoPadding",
           "supported_api_levels": "10+"
         },
         {
@@ -364,10 +364,12 @@
         },
         {
           "name": "ChaCha20/NONE/NoPadding",
+          "note": "ChaCha with 20 rounds, 96-bit nonce, and 32-bit counter as described in RFC 7539.",
           "supported_api_levels": "28+"
         },
         {
           "name": "ChaCha20/Poly1305/NoPadding",
+          "note": "ChaCha with 20 rounds, 96-bit nonce, and 32-bit counter as described in RFC 7539.",
           "supported_api_levels": "28+"
         },
         {
@@ -2741,5 +2743,5 @@
       "name": "TrustManagerFactory"
     }
   ],
-  "last_updated": "2018-01-03 14:21:23 UTC"
+  "last_updated": "2018-02-02 13:58:08 UTC"
 }
\ No newline at end of file
diff --git a/tools/docs/crypto/format_supported_algorithm_table.py b/tools/docs/crypto/format_supported_algorithm_table.py
index 5a6dd15..1fc96065 100755
--- a/tools/docs/crypto/format_supported_algorithm_table.py
+++ b/tools/docs/crypto/format_supported_algorithm_table.py
@@ -33,11 +33,28 @@
     return sorted(seq, key=lambda x: x['name'])
 
 
+def has_notes(category):
+    for algorithm in category['algorithms']:
+        if 'note' in algorithm:
+            return True
+    return False
+
+
+# Prevents the given value from being word-wrapped.  This is mainly to ensure that
+# long identifiers with hyphens, like OAEPwithSHA-1andMGF1Padding, don't get word-wrapped
+# at the hyphen.
+def nowrap(value):
+    return '<span style="white-space: nowrap">%s</span>' % value
+
+
 def main():
     parser = argparse.ArgumentParser(description='Output algorithm support HTML tables')
     parser.add_argument('--for_javadoc',
                         action='store_true',
                         help='If specified, format for inclusion in class documentation')
+    parser.add_argument('--category',
+                        action='append',
+                        help='The category to display, may be specified multiple times')
     parser.add_argument('file',
                         help='The JSON file to use for data')
     args = parser.parse_args()
@@ -54,6 +71,9 @@
                    '<code>{name}</code></a></li>'.format(**category))
     output.append('</ul>')
     for category in categories:
+        if args.category and category['name'] not in args.category:
+            continue
+        show_notes = has_notes(category)
         if category['name'].endswith('.Enabled'):
             # These are handled in the "Supported" section below
             continue
@@ -74,7 +94,8 @@
                     [mode],
                     [padding],
                     algorithm['supported_api_levels'],
-                    'deprecated' in algorithm and algorithm['deprecated']))
+                    'deprecated' in algorithm and algorithm['deprecated'],
+                    algorithm.get('note', '')))
             # Sort the tuples by all items except padding, then collapse
             # items with all non-padding values the same (which will always be
             # neighboring items) into a single item.
@@ -84,7 +105,8 @@
                 if (tuples[i][0] == tuples[i+1][0]
                     and tuples[i][1] == tuples[i+1][1]
                     and tuples[i][3] == tuples[i+1][3]
-                    and tuples[i][4] == tuples[i+1][4]):
+                    and tuples[i][4] == tuples[i+1][4]
+                    and tuples[i][5] == tuples[i+1][5]):
                     tuples[i][2].extend(tuples[i+1][2])
                     del tuples[i+1]
                 else:
@@ -96,7 +118,8 @@
                 if (tuples[i][0] == tuples[i+1][0]
                     and tuples[i][2] == tuples[i+1][2]
                     and tuples[i][3] == tuples[i+1][3]
-                    and tuples[i][4] == tuples[i+1][4]):
+                    and tuples[i][4] == tuples[i+1][4]
+                    and tuples[i][5] == tuples[i+1][5]):
                     tuples[i][1].extend(tuples[i+1][1])
                     del tuples[i+1]
                 else:
@@ -111,6 +134,8 @@
             output.append('      <th>Modes</th>')
             output.append('      <th>Paddings</th>')
             output.append('      <th>Supported API Levels</th>')
+            if show_notes:
+                output.append('      <th>Notes</th>')
             output.append('    </tr>')
             output.append('  </thead>')
             output.append('  <tbody>')
@@ -123,6 +148,7 @@
                 row = tuples[i]
                 if row[4] != cur_deprecated:
                     cur_deprecated = row[4]
+                    cur_note = row[5]
                     cur_algorithm = None
                     cur_mode = None
                 if cur_deprecated:
@@ -135,29 +161,33 @@
                     j = i + 1
                     while (j < len(tuples)
                            and tuples[j][4] == cur_deprecated
+                           and tuples[j][5] == cur_note
                            and tuples[j][0] == cur_algorithm):
                         j += 1
                     rowspan = j - i
                     if rowspan > 1:
-                        output.append('      <td rowspan="%d">%s</td>' % (rowspan, cur_algorithm))
+                        output.append('      <td rowspan="%d">%s</td>' % (rowspan, nowrap(cur_algorithm)))
                     else:
-                        output.append('      <td>%s</td>' % cur_algorithm)
+                        output.append('      <td>%s</td>' % nowrap(cur_algorithm))
                 if row[1] != cur_mode:
                     cur_mode = row[1]
                     j = i + 1
                     while (j < len(tuples)
                            and tuples[j][4] == cur_deprecated
+                           and tuples[j][5] == cur_note
                            and tuples[j][0] == cur_algorithm
                            and tuples[j][1] == cur_mode):
                         j += 1
                     rowspan = j - i
-                    modestring = '<br>'.join(cur_mode)
+                    modestring = '<br>'.join([nowrap(x) for x in cur_mode])
                     if rowspan > 1:
                         output.append('      <td rowspan="%d">%s</td>' % (rowspan, modestring))
                     else:
                         output.append('      <td>%s</td>' % modestring)
-                output.append('      <td>%s</td>' % '<br>'.join(row[2]))
-                output.append('      <td>%s</td>' % row[3])
+                output.append('      <td>%s</td>' % '<br>'.join([nowrap(x) for x in row[2]]))
+                output.append('      <td>%s</td>' % nowrap(row[3]))
+                if show_notes:
+                    output.append('      <td>%s</td>' % row[5])
                 output.append('    </tr>')
                 i += 1
             output.append('  </tbody>')
@@ -177,6 +207,8 @@
             output.append('      <th>Algorithm</th>')
             output.append('      <th>Supported API Levels</th>')
             output.append('      <th>Enabled By Default</th>')
+            if show_notes:
+                output.append('      <th>Notes</th>')
             output.append('    </tr>')
             output.append('  </thead>')
             output.append('  <tbody>')
@@ -185,13 +217,18 @@
                     output.append('    <tr class="deprecated">')
                 else:
                     output.append('    <tr>')
-                output.append('      <td>{name}</td>'.format(**algorithm))
-                output.append('      <td>{supported_api_levels}</td>'.format(**algorithm))
+                output.append('      <td>%s</td>' % nowrap(algorithm['name']))
+                output.append('      <td>%s</td>' % nowrap(algorithm['supported_api_levels']))
                 enabled_alg = find_by_name(enabled, algorithm['name'])
                 if enabled_alg is None:
                     output.append('      <td></td>')
                 else:
-                    output.append('      <td>{supported_api_levels}</td>'.format(**enabled_alg))
+                    output.append('      <td>%s</td>' % nowrap(enabled_alg['supported_api_levels']))
+                if show_notes:
+                    if 'note' in algorithm:
+                        output.append('      <td>%s</td>' % algorithm['note'])
+                    else:
+                        output.append('      <td></td>')
                 output.append('    </tr>')
             output.append('  </tbody>')
             output.append('</table>')
@@ -202,6 +239,8 @@
             output.append('    <tr>')
             output.append('      <th>Algorithm</th>')
             output.append('      <th>Supported API Levels</th>')
+            if show_notes:
+                output.append('      <th>Notes</th>')
             output.append('    </tr>')
             output.append('  </thead>')
             output.append('  <tbody>')
@@ -211,8 +250,13 @@
                     output.append('    <tr class="deprecated">')
                 else:
                     output.append('    <tr>')
-                output.append('      <td>{name}</td>'.format(**algorithm))
-                output.append('      <td>{supported_api_levels}</td>'.format(**algorithm))
+                output.append('      <td>%s</td>' % nowrap(algorithm['name']))
+                output.append('      <td>%s</td>' % nowrap(algorithm['supported_api_levels']))
+                if show_notes:
+                    if 'note' in algorithm:
+                        output.append('      <td>%s</td>' % algorithm['note'])
+                    else:
+                        output.append('      <td></td>')
                 output.append('    </tr>')
             output.append('  </tbody>')
             output.append('</table>')
diff --git a/tools/docs/crypto/update_crypto_support.py b/tools/docs/crypto/update_crypto_support.py
index 3138d7d..83c4ec8 100755
--- a/tools/docs/crypto/update_crypto_support.py
+++ b/tools/docs/crypto/update_crypto_support.py
@@ -195,10 +195,10 @@
         alg_union = set(prev_algorithms) | set(current_category)
         for alg in alg_union:
             prev_alg = find_by_normalized_name(prev_category['algorithms'], alg)
-            if alg in name_dict:
-                new_algorithm = {'name': name_dict[alg]}
-            elif prev_alg is not None:
+            if prev_alg is not None:
                 new_algorithm = {'name': prev_alg['name']}
+            elif alg in name_dict:
+                new_algorithm = {'name': name_dict[alg]}
             else:
                 new_algorithm = {'name': alg}
             if category not in CASE_SENSITIVE_CATEGORIES:
@@ -226,6 +226,8 @@
             else:
                 # Only in the new set, so add it
                 new_level = '%d+' % api_level
+            if alg in prev_algorithms and 'note' in prev_alg:
+                new_algorithm['note'] = prev_alg['note']
             new_algorithm['supported_api_levels'] = new_level
             new_category['algorithms'].append(new_algorithm)
         if new_category['algorithms']:
diff --git a/tools/docs/crypto/update_crypto_support_test.py b/tools/docs/crypto/update_crypto_support_test.py
index 4fbc210..bf02438 100755
--- a/tools/docs/crypto/update_crypto_support_test.py
+++ b/tools/docs/crypto/update_crypto_support_test.py
@@ -34,6 +34,7 @@
 
 
 class TestUpdateData(unittest.TestCase):
+    maxDiff = None
     def test_find_by_name(self):
         self.assertIsNone(update_crypto_support.find_by_name([], 'foo'))
         self.assertIsNone(
@@ -359,6 +360,31 @@
                 'api_level': '72',
                 'last_updated': LAST_UPDATED_TEXT})
 
+    def test_update_data_preserve_notes(self):
+        self.assertEqual(
+            do_update_data(
+                {'categories': [
+                    {'name': 'MessageDigest',
+                     'algorithms': [
+                         {'name': 'SHA-1',
+                          'note': 'SHA-1 note',
+                          'supported_api_levels': '1+'},
+                         {'name': 'SHA-2',
+                          'supported_api_levels': '1-22',
+                          'deprecated': 'true'}]}]},
+                {'MessageDigest': ['SHA-1']}),
+            {'categories': [
+                {'name': 'MessageDigest',
+                 'algorithms': [
+                     {'name': 'SHA-1',
+                      'note': 'SHA-1 note',
+                      'supported_api_levels': '1+'},
+                     {'name': 'SHA-2',
+                      'supported_api_levels': '1-22',
+                      'deprecated': 'true'}]}],
+                'api_level': '72',
+                'last_updated': LAST_UPDATED_TEXT})
+
     def test_update_name_matching(self):
         self.assertEqual(
             do_update_data(
@@ -377,12 +403,12 @@
             {'categories': [
                 {'name': 'MessageDigest',
                  'algorithms': [
-                     {'name': 'Sha-1',
-                      'supported_api_levels': '1+'},
+                     {'name': 'SHA-3',
+                      'supported_api_levels': '7+'},
                      {'name': 'Sha-2',
                       'supported_api_levels': '1-22,72+'},
-                     {'name': 'Sha-3',
-                      'supported_api_levels': '7+'}]}],
+                     {'name': 'sha-1',
+                      'supported_api_levels': '1+'}]}],
                 'api_level': '72',
                 'last_updated': LAST_UPDATED_TEXT})
         self.assertEqual(
@@ -390,25 +416,25 @@
                 {'categories': [
                     {'name': 'MessageDigest',
                      'algorithms': [
-                         {'name': 'sha-1',
-                          'supported_api_levels': '1+'},
                          {'name': 'Sha-2',
                           'supported_api_levels': '1-22',
                           'deprecated': 'true'},
                          {'name': 'SHA-3',
-                          'supported_api_levels': '7+'}]}]},
+                          'supported_api_levels': '7+'},
+                         {'name': 'sha-1',
+                          'supported_api_levels': '1+'}]}]},
                 {'MessageDigest': ['SHA-1', 'SHA-3']},
                 {'SHA-1': 'Sha-1', 'SHA-3': 'Sha-3'}),
             {'categories': [
                 {'name': 'MessageDigest',
                  'algorithms': [
-                     {'name': 'Sha-1',
-                      'supported_api_levels': '1+'},
+                     {'name': 'SHA-3',
+                      'supported_api_levels': '7+'},
                      {'name': 'Sha-2',
                       'supported_api_levels': '1-22',
                       'deprecated': 'true'},
-                     {'name': 'Sha-3',
-                      'supported_api_levels': '7+'}]}],
+                     {'name': 'sha-1',
+                      'supported_api_levels': '1+'}]}],
                 'api_level': '72',
                 'last_updated': LAST_UPDATED_TEXT})