| #!/usr/bin/env python3 | 
 | # -*- coding: utf-8 -*- | 
 | # | 
 | # Copyright (C) 2017-2020  The Project X-Ray Authors. | 
 | # | 
 | # Use of this source code is governed by a ISC-style | 
 | # license that can be found in the LICENSE file or at | 
 | # https://opensource.org/licenses/ISC | 
 | # | 
 | # SPDX-License-Identifier: ISC | 
 | """ | 
 | Canonicalize the Project X-Ray database files by sorting. The aim is to reduce | 
 | the diff output between runs to make it clearer what has changed. | 
 |  | 
 | DB Files | 
 | -------- | 
 |  | 
 | DB files are sorted into "natural" ordering. This is generally the order that a | 
 | human would sort them in rather than how they sort as ASCII strings. | 
 |  | 
 | For example with tags, a sequence of ABC1 to ABC12 would have the ASCII sort | 
 | order of; | 
 |  | 
 |     ABC1 | 
 |     ABC10 | 
 |     ABC11 | 
 |     ABC12 | 
 |     ABC2 | 
 |     ... | 
 |     ABC9 | 
 |  | 
 | We instead sort them like the following; | 
 |  | 
 |     ABC1 | 
 |     ABC2 | 
 |     ... | 
 |     ABC9 | 
 |     ABC10 | 
 |     ABC11 | 
 |     ABC12 | 
 |  | 
 | For the segbit files, we sort the bit definitions ignoring any leading | 
 | exclamation mark. Doing this generally makes it much easier to see patterns in | 
 | the bit descriptions and you end up with output like the following for 1-hot | 
 | encoded choices, | 
 |  | 
 |     ABC.CHOICE1 22_15 !22_16 !22_17 | 
 |     ABC.CHOICE2 !22_15 22_16 !22_17 | 
 |     ABC.CHOICE3 !22_15 !22_16 22_17 | 
 |  | 
 | JSON Files | 
 | ---------- | 
 |  | 
 | For the JSON files, we run them through Python's pretty printing module and | 
 | sort sets (lists where the order doesn't matter). | 
 |  | 
 | """ | 
 |  | 
 | import csv | 
 | import os | 
 | import random | 
 | import re | 
 | import sys | 
 |  | 
 | import json | 
 | import utils.xjson as xjson | 
 | import utils.cmp as cmp | 
 |  | 
 | from prjxray.util import OpenSafeFile | 
 |  | 
 | def split_all(s, chars): | 
 |     """Split on multiple character values. | 
 |  | 
 |     >>> split_all('a_b_c_d', '_. ') | 
 |     ['a', 'b', 'c', 'd'] | 
 |     >>> split_all('a b c d', '_. ') | 
 |     ['a', 'b', 'c', 'd'] | 
 |     >>> split_all('a.b.c.d', '_. ') | 
 |     ['a', 'b', 'c', 'd'] | 
 |     >>> split_all('a_b.c d', '_. ') | 
 |     ['a', 'b', 'c', 'd'] | 
 |     >>> split_all('a b_c.d', '_. ') | 
 |     ['a', 'b', 'c', 'd'] | 
 |     """ | 
 |     chars = list(chars) | 
 |  | 
 |     o = [s] | 
 |     while len(chars) > 0: | 
 |         c = chars.pop(0) | 
 |  | 
 |         n = [] | 
 |         for i in o: | 
 |             n += i.split(c) | 
 |  | 
 |         o = n | 
 |     return o | 
 |  | 
 |  | 
 | NUM_REGEX = re.compile('^(.*?)([0-9]*)$') | 
 |  | 
 |  | 
 | def extract_num(i): | 
 |     """Extract number from a string to be sorted. | 
 |  | 
 |     >>> extract_num('BLAH123') | 
 |     ('BLAH', 123) | 
 |     >>> extract_num('123') | 
 |     123 | 
 |     >>> extract_num('BLAH') | 
 |     'BLAH' | 
 |     >>> extract_num('') | 
 |     '' | 
 |     """ | 
 |     g = NUM_REGEX.match(i).groups() | 
 |     if len(g[-1]) == 0: | 
 |         return i | 
 |     i = int(g[-1]) | 
 |     if len(g[0]) == 0: | 
 |         return i | 
 |     else: | 
 |         return (g[0], i) | 
 |  | 
 |  | 
 | class bit(tuple): | 
 |     """Class representing a bit specifier. | 
 |  | 
 |     >>> a = bit.parse("02_12") | 
 |     >>> a | 
 |     bit(2, 12, True) | 
 |     >>> b = bit.parse("!02_03") | 
 |     >>> b | 
 |     bit(2, 3, False) | 
 |     >>> b == a | 
 |     False | 
 |     >>> b < a | 
 |     True | 
 |     >>> str(a) | 
 |     '02_12' | 
 |     >>> str(b) | 
 |     '!02_03' | 
 |  | 
 |     >>> bit.parseline("!30_04 !31_00 !31_01 31_02") | 
 |     [bit(30, 4, False), bit(31, 0, False), bit(31, 1, False), bit(31, 2, True)] | 
 |  | 
 |     >>> bit.parseline("31_02 !31_00 !31_01 !30_04") | 
 |     [bit(30, 4, False), bit(31, 0, False), bit(31, 1, False), bit(31, 2, True)] | 
 |     """ | 
 |  | 
 |     @classmethod | 
 |     def parse(cls, s): | 
 |         mode = s[0] != '!' | 
 |         s = s.replace('!', '') | 
 |         assert '_' in s, s | 
 |         a, b = s.split('_', 1) | 
 |         assert '_' not in b, s | 
 |         return cls((extract_num(a), extract_num(b), mode)) | 
 |  | 
 |     @classmethod | 
 |     def parseline(cls, s): | 
 |         bits = [cls.parse(b) for b in s.split(' ')] | 
 |         bits.sort() | 
 |         return bits | 
 |  | 
 |     def __repr__(self): | 
 |         return "bit" + tuple.__repr__(self) | 
 |  | 
 |     def __str__(self): | 
 |         s = self | 
 |         return "{}{:02d}_{:02d}".format(['!', ''][s[2]], s[0], s[1]) | 
 |  | 
 |  | 
 | def convert_bit(i): | 
 |     """Convert a bit pattern into sortable form. | 
 |  | 
 |     >>> convert_bit("02_12") | 
 |     bit(2, 12, True) | 
 |     >>> convert_bit("!02_12") | 
 |     bit(2, 12, False) | 
 |     >>> convert_bit("!02_02") | 
 |     bit(2, 2, False) | 
 |     >>> convert_bit("always") | 
 |     'always' | 
 |     """ | 
 |     if '_' not in i: | 
 |         return i | 
 |     return bit.parse(i) | 
 |  | 
 |  | 
 | def segbit_line_sort_bits(l): | 
 |     """Sort the bit section of a segbit line. | 
 |  | 
 |     >>> segbit_line_sort_bits("A !28_35 !27_39 27_37") | 
 |     'A 27_37 !27_39 !28_35' | 
 |  | 
 |     >>> segbit_line_sort_bits("B !28_35 !27_39 !27_37") | 
 |     'B !27_37 !27_39 !28_35' | 
 |  | 
 |     >>> segbit_line_sort_bits("C 28_35 00_00 !27_37") | 
 |     'C 00_00 !27_37 28_35' | 
 |  | 
 |     """ | 
 |     tag, *segbits = l.split() | 
 |  | 
 |     segbits = [bit.parse(b) for b in segbits] | 
 |     segbits.sort() | 
 |  | 
 |     return "{} {}".format(tag, " ".join(str(s) for s in segbits)) | 
 |  | 
 |  | 
 | def sortable_tag(t): | 
 |     """ | 
 |     >>> sortable_tag("CLBLL_L.CLBLL_L_A.CLBLL_L_A1") | 
 |     ('CLBLL', 'L', 'CLBLL', 'L', 'A', 'CLBLL', 'L', ('A', 1)) | 
 |  | 
 |     >>> sortable_tag("CLBLL_L.CLBLL_LOGIC_OUTS23.CLBLL_LL_DMUX") | 
 |     ('CLBLL', 'L', 'CLBLL', 'LOGIC', ('OUTS', 23), 'CLBLL', 'LL', 'DMUX') | 
 |  | 
 |     >>> sortable_tag("BRAM_L.RAMB18_Y0.INIT_B[9]") | 
 |     ('BRAM', 'L', ('RAMB', 18), ('Y', 0), 'INIT', 'B', 9) | 
 |  | 
 |     >>> sortable_tag("BRAM_L.RAMB18_Y0.READ_WIDTH_A_18") | 
 |     ('BRAM', 'L', ('RAMB', 18), ('Y', 0), 'READ', 'WIDTH', 'A', 18) | 
 |     """ | 
 |     return tuple(extract_num(i) for i in split_all(t, '_.[]') if i != '') | 
 |  | 
 |  | 
 | def sortable_line_from_mask(l): | 
 |     """Convert a line in a mask_XXXX.db file to something sortable. | 
 |  | 
 |     Example lines from mask_XXX.db file | 
 |     >>> a, b = sortable_line_from_mask("bit 00_00") | 
 |     >>> a | 
 |     bit(0, 0, True) | 
 |     >>> b | 
 |     'bit 00_00' | 
 |  | 
 |     >>> a, b = sortable_line_from_mask("bit 09_39") | 
 |     >>> a | 
 |     bit(9, 39, True) | 
 |     >>> b | 
 |     'bit 09_39' | 
 |     """ | 
 |     tag, b = l.split(' ', 1) | 
 |     assert tag == 'bit', tag | 
 |     return bit.parse(b), l | 
 |  | 
 |  | 
 | def sortable_line_from_ppips(l): | 
 |     """Convert a line in a ppips_XXX.db file to something sortable. | 
 |  | 
 |     Example lines from ppips_XXX.db file | 
 |     >>> a, b = sortable_line_from_ppips("CLBLL_L.CLBLL_L_A.CLBLL_L_A1 hint") | 
 |     >>> a | 
 |     (('CLBLL', 'L', 'CLBLL', 'L', 'A', 'CLBLL', 'L', ('A', 1)), 'hint') | 
 |     >>> b | 
 |     'CLBLL_L.CLBLL_L_A.CLBLL_L_A1 hint' | 
 |  | 
 |     >>> a, b = sortable_line_from_ppips("CLBLL_L.CLBLL_LOGIC_OUTS23.CLBLL_LL_DMUX always") | 
 |     >>> a | 
 |     (('CLBLL', 'L', 'CLBLL', 'LOGIC', ('OUTS', 23), 'CLBLL', 'LL', 'DMUX'), 'always') | 
 |     >>> b | 
 |     'CLBLL_L.CLBLL_LOGIC_OUTS23.CLBLL_LL_DMUX always' | 
 |     """ | 
 |     assert ' ' in l, repr(l) | 
 |     tag, ptype = l.split(' ', 1) | 
 |     tag = sortable_tag(tag) | 
 |     return (tag, ptype), l | 
 |  | 
 |  | 
 | def sortable_line_from_segbits(l): | 
 |     """Convert a line in segbits_XXX.db file to something sortable. | 
 |  | 
 |     >>> (tag, bits), b = sortable_line_from_segbits("BRAM_L.RAMB18_Y0.INIT_B[9] 27_15") | 
 |     >>> tag | 
 |     ('BRAM', 'L', ('RAMB', 18), ('Y', 0), 'INIT', 'B', 9) | 
 |     >>> bits | 
 |     (bit(27, 15, True),) | 
 |     >>> b | 
 |     'BRAM_L.RAMB18_Y0.INIT_B[9] 27_15' | 
 |  | 
 |     >>> (tag, bits), b = sortable_line_from_segbits("BRAM_L.RAMB18_Y0.READ_WIDTH_A_18 !28_35 !27_39 27_37") | 
 |     >>> tag | 
 |     ('BRAM', 'L', ('RAMB', 18), ('Y', 0), 'READ', 'WIDTH', 'A', 18) | 
 |     >>> bits | 
 |     (bit(27, 37, True), bit(27, 39, False), bit(28, 35, False)) | 
 |     >>> b | 
 |     'BRAM_L.RAMB18_Y0.READ_WIDTH_A_18 27_37 !27_39 !28_35' | 
 |     """ | 
 |     tag, sbit = l.split(' ', 1) | 
 |     tag = sortable_tag(tag) | 
 |  | 
 |     bits = bit.parseline(sbit) | 
 |  | 
 |     l = segbit_line_sort_bits(l) | 
 |     return (tag, tuple(bits)), l | 
 |  | 
 |  | 
 | def sortable_line_from_origin_segbits(l): | 
 |     tag, origin, sbit = l.split(' ', 2) | 
 |     tag = sortable_tag(tag) | 
 |  | 
 |     bits = bit.parseline(sbit) | 
 |  | 
 |     return (tag, tuple(bits)), l | 
 |  | 
 |  | 
 | def sort_db(pathname): | 
 |     """Sort a XXX.db file.""" | 
 |     filename = os.path.split(pathname)[-1] | 
 |     if filename.startswith('segbits_'): | 
 |         if 'origin_info' in filename: | 
 |             sortable_line_from_dbfile = sortable_line_from_origin_segbits | 
 |         else: | 
 |             sortable_line_from_dbfile = sortable_line_from_segbits | 
 |     elif 'origin_info' in filename: | 
 |         return False | 
 |     elif filename.startswith('ppips_'): | 
 |         sortable_line_from_dbfile = sortable_line_from_ppips | 
 |     elif filename.startswith('grid-'): | 
 |         sortable_line_from_dbfile = sortable_line_from_ppips | 
 |     elif filename.startswith('mask_'): | 
 |         sortable_line_from_dbfile = sortable_line_from_mask | 
 |     else: | 
 |         return False | 
 |  | 
 |     with OpenSafeFile(pathname) as f: | 
 |         lines = f.readlines() | 
 |  | 
 |     tosort = [] | 
 |     for l in lines: | 
 |         l = l.strip() | 
 |         if not l: | 
 |             continue | 
 |         tosort.append(sortable_line_from_dbfile(l)) | 
 |  | 
 |     tosort.sort(key=cmp.cmp_key) | 
 |  | 
 |     with open(pathname, 'w') as f: | 
 |         for _, l in tosort: | 
 |             f.write(l) | 
 |             f.write('\n') | 
 |  | 
 |     return True | 
 |  | 
 |  | 
 | def sort_csv(pathname): | 
 |     rows = [] | 
 |     fields = [] | 
 |     delimiter = None | 
 |     with open(pathname, newline='') as f: | 
 |         if pathname.endswith('.csv'): | 
 |             delimiter = ',' | 
 |         elif pathname.endswith('.txt'): | 
 |             delimiter = ' ' | 
 |         reader = csv.DictReader(f, delimiter=delimiter) | 
 |         fields.extend(reader.fieldnames) | 
 |         rows.extend(reader) | 
 |         del reader | 
 |  | 
 |     def sort_key(r): | 
 |         v = [] | 
 |         for field in fields: | 
 |             v.append(sortable_tag(r[field])) | 
 |         return tuple(v) | 
 |  | 
 |     rows.sort(key=sort_key) | 
 |  | 
 |     with open(pathname, 'w', newline='') as f: | 
 |         writer = csv.DictWriter( | 
 |             f, fields, delimiter=delimiter, lineterminator='\n') | 
 |         writer.writeheader() | 
 |         writer.writerows(rows) | 
 |  | 
 |     return True | 
 |  | 
 |  | 
 | def sort_json(filename): | 
 |     """Sort a XXX.json file.""" | 
 |  | 
 |     try: | 
 |         with OpenSafeFile(filename) as f: | 
 |             d = json.load(f) | 
 |     except json.JSONDecodeError as e: | 
 |         print(e) | 
 |         return False | 
 |  | 
 |     with OpenSafeFile(filename, 'w') as f: | 
 |         xjson.pprint(f, d) | 
 |  | 
 |     return True | 
 |  | 
 |  | 
 | def sort_db_text(n): | 
 |     rows = [] | 
 |     with OpenSafeFile(n) as f: | 
 |         for l in f: | 
 |             rows.append(([extract_num(s) for s in l.split()], l)) | 
 |  | 
 |     rows.sort(key=lambda i: i[0]) | 
 |  | 
 |     with OpenSafeFile(n, 'w') as f: | 
 |         for l in rows: | 
 |             f.write(l[-1]) | 
 |  | 
 |     return True | 
 |  | 
 |  | 
 | def sort_file(n): | 
 |  | 
 |     assert os.path.exists(n) | 
 |  | 
 |     base, ext = os.path.splitext(n) | 
 |     dirname, base = os.path.split(base) | 
 |  | 
 |     # Leave db files with fuzzer of origin untouched | 
 |     if "origin_info" in n and not base.startswith('segbits'): | 
 |         print("Ignoring     file {:45s}".format(n), flush=True) | 
 |         return | 
 |  | 
 |     if ext == '.db': | 
 |         print("Sorting DB   file {:45s}".format(n), end=" ", flush=True) | 
 |         x = sort_db(n) | 
 |     elif ext == '.json': | 
 |         print("Sorting JSON file {:45s}".format(n), end=" ", flush=True) | 
 |         x = sort_json(n) | 
 |     elif ext in ('.csv', '.txt'): | 
 |         if n.endswith('-db.txt'): | 
 |             print("Sorting txt  file {:45s}".format(n), end=" ", flush=True) | 
 |             x = sort_db_text(n) | 
 |         else: | 
 |             print("Sorting CSV  file {:45s}".format(n), end=" ", flush=True) | 
 |             x = sort_csv(n) | 
 |     else: | 
 |         print("Ignoring     file {:45s}".format(n), end=" ", flush=True) | 
 |         x = True | 
 |     if x: | 
 |         print(".. success.") | 
 |     else: | 
 |         print(".. failed.") | 
 |  | 
 |  | 
 | def sort_dir(dirname): | 
 |     for n in sorted(os.listdir(dirname)): | 
 |         n = os.path.join(dirname, n) | 
 |         if os.path.isdir(n): | 
 |             print("Entering     dir  {:45s}".format(n), flush=True) | 
 |             sort_dir(n) | 
 |             continue | 
 |         elif not os.path.isfile(n): | 
 |             print("Ignoring non-file {:45s}".format(n), flush=True) | 
 |             continue | 
 |  | 
 |         sort_file(n) | 
 |  | 
 |  | 
 | def main(argv): | 
 |     if argv[1:]: | 
 |         for n in argv[1:]: | 
 |             sort_file(n) | 
 |     else: | 
 |         sort_dir('.') | 
 |     return 0 | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     sys.exit(main(sys.argv)) |