da1320
#! /usr/bin/python
da1320
# SPDX-License-Identifier: CC0-1.0
da1320
da1320
import argparse
da1320
import errno
2c8f3d
import fnmatch
da1320
import io
da1320
import itertools
da1320
import os
da1320
import re
da1320
import shutil
da1320
import struct
da1320
import sys
2c8f3d
import tarfile
da1320
import tempfile
da1320
from subprocess import PIPE, Popen, STDOUT
da1320
da1320
# Python 3 shims
da1320
try:
da1320
    from functools import reduce
da1320
except:
da1320
    pass
da1320
try:
da1320
    from itertools import zip_longest as izip_longest
da1320
except:
da1320
    from itertools import izip_longest
da1320
da1320
# revs:
da1320
# [ { "path", "cpuid", "pf", "rev", "date" } ]
da1320
da1320
# artifacts:
da1320
#  * content summary (per-file)
da1320
#   * overlay summary (per-fms/pf)
da1320
#  * changelog (per-file?)
da1320
#  * discrepancies (per-fms/pf)
da1320
da1320
log_level = 0
da1320
print_date = False
2c8f3d
file_glob = ["*??-??-??", "*microcode*.dat"]
da1320
da1320
da1320
def log_status(msg, level=0):
da1320
    global log_level
da1320
da1320
    if log_level >= level:
da1320
        sys.stderr.write(msg + "\n")
da1320
da1320
da1320
def log_info(msg, level=2):
da1320
    global log_level
da1320
da1320
    if log_level >= level:
da1320
        sys.stderr.write("INFO: " + msg + "\n")
da1320
da1320
da1320
def log_warn(msg, level=1):
da1320
    global log_level
da1320
da1320
    if log_level >= level:
da1320
        sys.stderr.write("WARNING: " + msg + "\n")
da1320
da1320
da1320
def log_error(msg, level=-1):
da1320
    global log_level
da1320
da1320
    if log_level >= level:
da1320
        sys.stderr.write("ERROR: " + msg + "\n")
da1320
da1320
da1320
def remove_prefix(text, prefix):
da1320
    if isinstance(prefix, str):
da1320
        prefix = [prefix, ]
da1320
da1320
    for p in prefix:
da1320
        pfx = p if p.endswith(os.sep) else p + os.sep
da1320
        if text.startswith(pfx):
da1320
            return text[len(pfx):]
da1320
da1320
    return text
da1320
da1320
da1320
def file_walk(args, yield_dirs=False):
da1320
    for content in args:
da1320
        if os.path.isdir(content):
da1320
            if yield_dirs:
da1320
                yield ("", content)
da1320
            for root, dirs, files in os.walk(content):
da1320
                if yield_dirs:
da1320
                    for f in dirs:
da1320
                        p = os.path.join(root, f)
da1320
                        yield (remove_prefix(p, content), p)
da1320
                for f in files:
da1320
                    p = os.path.join(root, f)
da1320
                    yield (remove_prefix(p, content), p)
da1320
        elif os.path.exists(content):
da1320
            yield ("", content)
da1320
        else:
da1320
            raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), content)
da1320
da1320
da1320
def cpuid_fname(c):
2c8f3d
    # Note that the Extended Family is summed up with the Family,
2c8f3d
    # while the Extended Model is concatenated with the Model.
da1320
    return "%02x-%02x-%02x" % (
2c8f3d
        ((c >> 20) & 0xff) + ((c >> 8) & 0xf),
da1320
        ((c >> 12) & 0xf0) + ((c >> 4) & 0xf),
da1320
        c & 0xf)
da1320
da1320
2c8f3d
def read_revs_dir(path, args, src=None, ret=None):
da1320
    if ret is None:
da1320
        ret = []
da1320
da1320
    ucode_re = re.compile('[0-9a-f]{2}-[0-9a-f]{2}-0[0-9a-f]$')
da1320
    ucode_dat_re = re.compile('microcode.*\.dat$')
da1320
da1320
    for rp, ap in file_walk([path, ]):
da1320
        rp_fname = os.path.basename(rp)
da1320
        if not ucode_re.match(rp_fname) and not ucode_dat_re.match(rp_fname):
da1320
            continue
da1320
da1320
        # Text-based format
da1320
        data = None
da1320
        if ucode_dat_re.match(rp_fname):
da1320
            data = io.BytesIO()
da1320
            with open(ap, "r") as f:
da1320
                for line in f:
da1320
                    if line.startswith("/"):
da1320
                        continue
da1320
                    vals = line.split(",")
da1320
                    for val in vals:
da1320
                        val = val.strip()
da1320
                        if not val:
da1320
                            continue
da1320
                        data.write(struct.pack("
da1320
            sz = data.seek(0, os.SEEK_CUR)
da1320
            data.seek(0, os.SEEK_SET)
da1320
        else:
da1320
            sz = os.stat(ap).st_size
da1320
da1320
        try:
da1320
            with data or open(ap, "rb") as f:
da1320
                log_info("Processing %s" % ap)
da1320
                offs = 0
da1320
                while offs < sz:
da1320
                    f.seek(offs, os.SEEK_SET)
da1320
                    hdr = struct.unpack("IiIIIIIIIIII", f.read(48))
da1320
                    ret.append({"path": rp, "src": src or path,
da1320
                                "cpuid": hdr[3], "pf": hdr[6], "rev": hdr[1],
da1320
                                "date": hdr[2], "offs": offs, "cksum": hdr[4],
da1320
                                "data_size": hdr[7], "total_size": hdr[8]})
da1320
da1320
                    if hdr[8] and hdr[8] - hdr[7] > 48:
da1320
                        f.seek(hdr[7], os.SEEK_CUR)
da1320
                        ext_tbl = struct.unpack("IIIII", f.read(20))
da1320
                        log_status("Found %u extended signatures for %s:%#x" %
da1320
                                   (ext_tbl[0], rp, offs), level=1)
da1320
da1320
                        cur_offs = offs + hdr[7] + 48 + 20
da1320
                        ext_sig_cnt = 0
da1320
                        while cur_offs < offs + hdr[8] \
da1320
                                and ext_sig_cnt <= ext_tbl[0]:
da1320
                            ext_sig = struct.unpack("III", f.read(12))
2c8f3d
                            ignore = args.ignore_ext_dups and \
2c8f3d
                                (ext_sig[0] == hdr[3])
2c8f3d
                            if not ignore:
2c8f3d
                                ret.append({"path": rp, "src": src or path,
2c8f3d
                                            "cpuid": ext_sig[0],
2c8f3d
                                            "pf": ext_sig[1],
2c8f3d
                                            "rev": hdr[1], "date": hdr[2],
2c8f3d
                                            "offs": offs, "ext_offs": cur_offs,
2c8f3d
                                            "cksum": hdr[4],
2c8f3d
                                            "ext_cksum": ext_sig[2],
2c8f3d
                                            "data_size": hdr[7],
2c8f3d
                                            "total_size": hdr[8]})
da1320
                            log_status(("Got ext sig %#x/%#x for " +
2c8f3d
                                        "%s:%#x:%#x/%#x%s") %
2c8f3d
                                       (ext_sig[0], ext_sig[1],
2c8f3d
                                        rp, offs, hdr[3], hdr[6],
2c8f3d
                                        " (ignored)" if ignore else ""),
2c8f3d
                                       level=2)
da1320
da1320
                            cur_offs += 12
da1320
                            ext_sig_cnt += 1
da1320
da1320
                    offs += hdr[8] or 2048
da1320
        except Exception as e:
da1320
            log_error("a problem occurred while processing %s: %s" % (ap, e),
da1320
                      level=1)
da1320
da1320
    return ret
da1320
da1320
2c8f3d
def read_revs_rpm(path, args, ret=None):
da1320
    if ret is None:
da1320
        ret = []
da1320
da1320
    dir_tmp = tempfile.mkdtemp()
da1320
da1320
    log_status("Trying to extract files from RPM \"%s\"..." % path,
da1320
               level=1)
da1320
da1320
    rpm2cpio = Popen(args=["rpm2cpio", path], stdout=PIPE, stderr=PIPE,
da1320
                     close_fds=True)
2c8f3d
    cpio = Popen(args=["cpio", "-idmv"] + file_glob,
da1320
                 cwd=dir_tmp, stdin=rpm2cpio.stdout,
da1320
                 stdout=PIPE, stderr=STDOUT)
da1320
    out, cpio_stderr = cpio.communicate()
da1320
    rpm2cpio_out, rpm2cpio_err = rpm2cpio.communicate()
da1320
da1320
    rpm2cpio_ret = rpm2cpio.returncode
da1320
    cpio_ret = cpio.returncode
da1320
da1320
    log_info("rpm2cpio exit code: %d, cpio exit code: %d" %
da1320
             (rpm2cpio_ret, cpio_ret))
da1320
    if rpm2cpio_err:
da1320
        log_info("rpm2cpio stderr:\n%s" % rpm2cpio_err, level=3)
da1320
    if out:
da1320
        log_info("cpio output:\n%s" % out, level=3)
da1320
    if cpio_stderr:
da1320
        log_info("cpio stderr:\n%s" % cpio_stderr, level=3)
da1320
da1320
    if rpm2cpio_ret == 0 and cpio_ret == 0:
2c8f3d
        ret = read_revs_dir(dir_tmp, args, path)
da1320
da1320
    shutil.rmtree(dir_tmp)
da1320
da1320
    return ret
da1320
da1320
2c8f3d
def read_revs_tar(path, args, ret=None):
2c8f3d
    if ret is None:
2c8f3d
        ret = []
2c8f3d
2c8f3d
    dir_tmp = tempfile.mkdtemp()
2c8f3d
2c8f3d
    log_status("Trying to extract files from tarball \"%s\"..." % path,
2c8f3d
               level=1)
2c8f3d
2c8f3d
    try:
2c8f3d
        with tarfile.open(path, "r:*") as tar:
2c8f3d
            for ti in tar:
2c8f3d
                if any(fnmatch.fnmatchcase(ti.name, p) for p in file_glob):
2c8f3d
                    d = os.path.normpath(os.path.join("/",
2c8f3d
                                         os.path.dirname(ti.name)))
2c8f3d
                    # For now, strip exactl one level
2c8f3d
                    d = os.path.join(*(d.split(os.path.sep)[2:]))
2c8f3d
                    n = os.path.join(d, os.path.basename(ti.name))
2c8f3d
2c8f3d
                    if not os.path.exists(d):
2c8f3d
                        os.makedirs(d)
2c8f3d
                    t = tar.extractfile(ti)
2c8f3d
                    with open(n, "wb") as f:
2c8f3d
                        shutil.copyfileobj(t, f)
2c8f3d
                    t.close()
2c8f3d
2c8f3d
        ret = read_revs_dir(dir_tmp, args, path)
2c8f3d
    except Exception as err:
2c8f3d
        log_error("Error while reading \"%s\" as a tarball: \"%s\"" %
2c8f3d
                  (path, str(err)))
2c8f3d
2c8f3d
    shutil.rmtree(dir_tmp)
2c8f3d
2c8f3d
    return ret
2c8f3d
2c8f3d
2c8f3d
def read_revs(path, args, ret=None):
da1320
    if ret is None:
da1320
        ret = []
da1320
    if os.path.isdir(path):
2c8f3d
        return read_revs_dir(path, args, ret)
2c8f3d
    elif tarfile.is_tarfile(path):
2c8f3d
        return read_revs_tar(path, args, ret)
da1320
    else:
2c8f3d
        return read_revs_rpm(path, args, ret)
da1320
da1320
da1320
def gen_mc_map(mc_data, merge=False, merge_path=False):
da1320
    """
da1320
    Converts an array of microcode file information to a map with path/sig/pf
da1320
    as a key.
da1320
da1320
    merge: whether to leave only the newest mc variant in the map or leave all
da1320
           possible variants.
da1320
    """
da1320
    res = dict()
da1320
da1320
    for mc in mc_data:
da1320
        key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"])
da1320
da1320
        if key not in res:
da1320
            res[key] = dict()
da1320
da1320
        cpuid = mc["cpuid"]
da1320
        cur_pf = mc["pf"]
da1320
        pid = 1
da1320
        while cur_pf > 0:
da1320
            if cur_pf & 1 and not (merge and pid in res[key]
da1320
                                   and res[key][pid]["rev"][0] >= mc["rev"]):
da1320
                if pid not in res[cpuid] or merge:
da1320
                    res[cpuid][pid] = []
da1320
                res[cpuid][pid].append(mc)
da1320
da1320
            cur_pf = cur_pf / 2
da1320
            pid = pid * 2
da1320
da1320
    return res
da1320
da1320
da1320
def gen_fn_map(mc_data, merge=False, merge_path=False):
da1320
    res = dict()
da1320
da1320
    for mc in mc_data:
da1320
        key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"])
da1320
        if key in res:
da1320
            log_warn("Duplicate path/cpuid/pf: %s/%#x/%#x" % key)
da1320
        else:
da1320
            res[key] = []
da1320
        if merge and len(res[key]):
da1320
            if mc["rev"] > res[key][0]["rev"]:
da1320
                res[key][0] = mc
da1320
        else:
da1320
            res[key].append(mc)
da1320
da1320
    return res
da1320
da1320
da1320
def revcmp(a, b):
da1320
    return b["rev"] - a["rev"]
da1320
da1320
da1320
class ChangeLogEntry:
da1320
    ADDED = 0
da1320
    REMOVED = 1
da1320
    UPDATED = 2
da1320
    DOWNGRADED = 3
da1320
    OTHER = 4
da1320
da1320
da1320
def mc_stripped_path(mc):
da1320
    paths = ("usr/share/microcode_ctl/ucode_with_caveats/intel",
da1320
             "usr/share/microcode_ctl/ucode_with_caveats",
da1320
             "usr/share/microcode_ctl",
da1320
             "lib/firmware",
da1320
             "etc/firmware",
da1320
             )
da1320
da1320
    return remove_prefix(mc["path"], paths)
da1320
da1320
da1320
class mcnm:
da1320
    MCNM_ABBREV = 0
da1320
    MCNM_FAMILIES = 1
da1320
    MCNM_MODELS = 2
da1320
    MCNM_FAMILIES_MODELS = 3
da1320
    MCNM_CODENAME = 4
da1320
da1320
2c8f3d
def get_mc_cnames(mc, cmap, mode=mcnm.MCNM_ABBREV, stringify=True,
2c8f3d
                  segment=False):
da1320
    if not isinstance(mc, dict):
da1320
        mc = mc_from_mc_key(mc)
da1320
    sig = mc["cpuid"]
da1320
    pf = mc["pf"]
da1320
    res = []
da1320
da1320
    if not cmap:
da1320
        return None
da1320
    if sig not in cmap:
da1320
        log_info("No codename information for sig %#x" % sig)
da1320
        return None
da1320
da1320
    cnames = cmap[sig]
da1320
da1320
    if mode in (mcnm.MCNM_FAMILIES, mcnm.MCNM_MODELS,
da1320
                mcnm.MCNM_FAMILIES_MODELS):
da1320
        for c in cnames:
da1320
            if not (pf & c["pf_mask"]):
da1320
                continue
da1320
            for m, f in ((mcnm.MCNM_FAMILIES, "families"),
da1320
                         (mcnm.MCNM_MODELS, "models")):
da1320
                if m & mode == 0:
da1320
                    continue
da1320
                if f not in c or not c[f]:
da1320
                    log_info("No %s for sig %#x in %r" % (f, sig, c))
da1320
                    continue
da1320
da1320
                res.append(c[f])
da1320
da1320
        return ", ".join(res) or None
da1320
da1320
    steppings = dict()
da1320
    suffices = dict()
da1320
    for c in cnames:
da1320
        if pf and not (pf & c["pf_mask"]):
da1320
            continue
da1320
da1320
        if mode == mcnm.MCNM_ABBREV and "abbrev" in c and c["abbrev"]:
da1320
            cname = c["abbrev"]
da1320
        else:
da1320
            cname = c["codename"]
da1320
2c8f3d
        if segment:
2c8f3d
            cname = c["segment"] + " " + cname
2c8f3d
da1320
        if cname not in suffices:
da1320
            suffices[cname] = set()
da1320
        if "variant" in c and c["variant"]:
da1320
            suffices[cname] |= set(c["variant"])
da1320
da1320
        if cname not in steppings:
da1320
            steppings[cname] = set()
da1320
        if c["stepping"]:
da1320
            steppings[cname] |= set(c["stepping"])
da1320
da1320
    for cname in sorted(steppings.keys()):
2c8f3d
        cname_res = [cname]
da1320
        if len(suffices[cname]):
2c8f3d
            cname_res[0] += "-" + "/".join(sorted(suffices[cname]))
da1320
        if len(steppings[cname]):
2c8f3d
            cname_res.append("/".join(sorted(steppings[cname])))
2c8f3d
        res.append(" ".join(cname_res) if stringify else cname_res)
da1320
2c8f3d
    return (", ".join(res) or None) if stringify else res
da1320
da1320
da1320
def mc_from_mc_key(k):
da1320
    return dict(zip(("path", "cpuid", "pf"), k))
da1320
da1320
2c8f3d
def mc_path(mc, pf_sfx=True, midword=None, cmap=None, cname_segment=False):
da1320
    if not isinstance(mc, dict):
da1320
        mc = mc_from_mc_key(mc)
da1320
    path = mc_stripped_path(mc) if mc["path"] is not None else None
da1320
    cpuid_fn = cpuid_fname(mc["cpuid"])
da1320
    fname = os.path.basename(mc["path"] or cpuid_fn)
da1320
    midword = "" if midword is None else " " + midword
2c8f3d
    cname = get_mc_cnames(mc, cmap, segment=cname_segment)
da1320
    cname_str = " (" + cname + ")" if cname else ""
da1320
da1320
    if pf_sfx:
da1320
        sfx = "/0x%02x" % mc["pf"]
da1320
    else:
da1320
        sfx = ""
da1320
da1320
    if not path or path == os.path.join("intel-ucode", cpuid_fn):
da1320
        return "%s%s%s%s" % (fname, sfx, cname_str, midword)
da1320
    else:
da1320
        return "%s%s%s%s (in %s)" % (cpuid_fn, sfx, cname_str, midword, path)
da1320
da1320
da1320
def gen_changelog_file(old, new):
da1320
    pass
da1320
da1320
da1320
def mc_cmp(old_mc, new_mc):
da1320
    res = []
da1320
da1320
    old_mc_revs = [x["rev"] for x in old_mc]
da1320
    new_mc_revs = [x["rev"] for x in new_mc]
da1320
    common = set(old_mc_revs) & set(new_mc_revs)
da1320
    old_rev_list = [x for x in sorted(old_mc_revs) if x not in common]
da1320
    new_rev_list = [x for x in sorted(new_mc_revs) if x not in common]
da1320
da1320
    if len(old_rev_list) != 1 or len(new_rev_list) != 1:
da1320
        for i in new_mc:
da1320
            if i["rev"] in new_rev_list:
da1320
                res.append((ChangeLogEntry.ADDED, None, i))
da1320
        for i in old_mc:
da1320
            if i["rev"] in old_rev_list:
da1320
                res.append((ChangeLogEntry.REMOVED, i, None))
da1320
    else:
da1320
        for old in old_mc:
da1320
            if old["rev"] == old_rev_list[0]:
da1320
                break
da1320
        for new in new_mc:
da1320
            if new["rev"] == new_rev_list[0]:
da1320
                break
da1320
        if new["rev"] > old["rev"]:
da1320
            res.append((ChangeLogEntry.UPDATED, old, new))
da1320
        elif new["rev"] < old["rev"]:
da1320
            res.append((ChangeLogEntry.DOWNGRADED, old, new))
da1320
da1320
    return res
da1320
da1320
da1320
def gen_changelog(old, new):
da1320
    res = []
da1320
da1320
    old_map = gen_fn_map(old)
da1320
    new_map = gen_fn_map(new)
da1320
da1320
    old_files = set(old_map.keys())
da1320
    new_files = set(new_map.keys())
da1320
da1320
    both = old_files & new_files
da1320
    added = new_files - old_files
da1320
    removed = old_files - new_files
da1320
da1320
    for f in sorted(added):
da1320
        p = mc_path(new_map[f][0])
da1320
        for old_f in sorted(removed):
da1320
            old_p = mc_path(old_map[old_f][0])
da1320
            if p == old_p and f[1] == old_f[1] and f[2] == old_f[2]:
da1320
                log_info("Matched %s (%s and %s)" %
da1320
                         (p, old_map[old_f][0]["path"], new_map[f][0]["path"]))
da1320
                added.remove(f)
da1320
                removed.remove(old_f)
da1320
da1320
                res += mc_cmp(old_map[old_f], new_map[f])
da1320
da1320
    for f in sorted(added):
da1320
        for i in new_map[f]:
da1320
            res.append((ChangeLogEntry.ADDED, None, i))
da1320
    for f in sorted(removed):
da1320
        for i in old_map[f]:
da1320
            res.append((ChangeLogEntry.REMOVED, i, None))
da1320
    for f in sorted(both):
da1320
        res += mc_cmp(old_map[f], new_map[f])
da1320
da1320
    return res
da1320
da1320
da1320
def mc_date(mc):
da1320
    if isinstance(mc, dict):
da1320
        mc = mc["date"]
da1320
    return "%04x-%02x-%02x" % (mc & 0xffff, mc >> 24, (mc >> 16) & 0xff)
da1320
da1320
da1320
def mc_rev(mc, date=None):
da1320
    '''
da1320
    While revision is signed for comparison purposes, historically
da1320
    it is printed as unsigned,  Oh well.
da1320
    '''
da1320
    global print_date
da1320
da1320
    if mc["rev"] < 0:
da1320
        rev = 2**32 + mc["rev"]
da1320
    else:
da1320
        rev = mc["rev"]
da1320
da1320
    if date if date is not None else print_date:
da1320
        return "%#x (%s)" % (rev, mc_date(mc))
da1320
    else:
da1320
        return "%#x" % rev
da1320
da1320
2c8f3d
def print_changelog_rpm(clog, cmap, args):
2c8f3d
    for e, old, new in clog:
2c8f3d
        mc_str = mc_path(new if e == ChangeLogEntry.ADDED else old,
2c8f3d
                         midword="microcode",
2c8f3d
                         cmap=cmap, cname_segment=args.segment)
2c8f3d
da1320
        if e == ChangeLogEntry.ADDED:
2c8f3d
            print("Addition of %s at revision %s" % (mc_str, mc_rev(new)))
da1320
        elif e == ChangeLogEntry.REMOVED:
2c8f3d
            print("Removal of %s at revision %s" % (mc_str, mc_rev(old)))
da1320
        elif e == ChangeLogEntry.UPDATED:
da1320
            print("Update of %s from revision %s up to %s" %
2c8f3d
                  (mc_str, mc_rev(old), mc_rev(new)))
da1320
        elif e == ChangeLogEntry.DOWNGRADED:
da1320
                print("Downgrade of %s from revision %s down to %s" %
2c8f3d
                      (mc_str, mc_rev(old), mc_rev(new)))
da1320
        elif e == ChangeLogEntry.OTHER:
da1320
            print("Other change in %s:" % old["path"])
da1320
            print("  old: %#x/%#x: rev %s (offs %#x)" %
da1320
                  (old["cpuid"], old["pf"], mc_rev(old), old["offs"]))
da1320
            print("  new: %#x/%#x: rev %s (offs %#x)" %
da1320
                  (new["cpuid"], new["pf"], mc_rev(new), new["offs"]))
da1320
da1320
2c8f3d
def print_changelog_intel(clog, cmap, args):
2c8f3d
    def clog_sort_key(x):
2c8f3d
        res = str(x[0])
2c8f3d
2c8f3d
        if x[0] != ChangeLogEntry.ADDED:
2c8f3d
            res += "%08x%02x" % (x[1]["cpuid"], x[1]["pf"])
2c8f3d
        else:
2c8f3d
            res += "0" * 10
2c8f3d
2c8f3d
        if x[0] != ChangeLogEntry.REMOVED:
2c8f3d
            res += "%08x%02x" % (x[2]["cpuid"], x[2]["pf"])
2c8f3d
        else:
2c8f3d
            res += "0" * 10
2c8f3d
2c8f3d
        return res
2c8f3d
2c8f3d
    sorted_clog = sorted(clog, key=clog_sort_key)
2c8f3d
    sections = (("New Platforms",     (ChangeLogEntry.ADDED, )),
2c8f3d
                ("Updated Platforms", (ChangeLogEntry.UPDATED,
2c8f3d
                                       ChangeLogEntry.DOWNGRADED)),
2c8f3d
                ("Removed Platforms", (ChangeLogEntry.REMOVED, )))
2c8f3d
2c8f3d
    def print_line(e, old, new, types):
2c8f3d
        if e not in types:
2c8f3d
            return
2c8f3d
2c8f3d
        if not print_line.hdr:
2c8f3d
            print("""
2c8f3d
| Processor      | Stepping | F-M-S/PI    | Old Ver  | New Ver  | Products
2c8f3d
|:---------------|:---------|:------------|:---------|:---------|:---------""")
2c8f3d
            print_line.hdr = True
2c8f3d
2c8f3d
        mc = new if e == ChangeLogEntry.ADDED else old
2c8f3d
        cnames = get_mc_cnames(mc, cmap, stringify=False,
2c8f3d
                               segment=args.segment) or (("???", ""), )
2c8f3d
        for cn in cnames:
2c8f3d
            cname = cn[0]
2c8f3d
            stepping = cn[1] if len(cn) > 1 else ""
2c8f3d
        print("| %-14s | %-8s | %8s/%02x | %8s | %8s | %s" %
2c8f3d
              (cname,
2c8f3d
               stepping,
2c8f3d
               cpuid_fname(mc["cpuid"]), mc["pf"],
2c8f3d
               ("%08x" % old["rev"]) if e != ChangeLogEntry.ADDED else "",
2c8f3d
               ("%08x" % new["rev"]) if e != ChangeLogEntry.REMOVED else "",
2c8f3d
               get_mc_cnames(mc, cmap, mode=mcnm.MCNM_FAMILIES,
2c8f3d
                             segment=args.segment) or ""))
2c8f3d
2c8f3d
    for h, types in sections:
2c8f3d
        print("\n### %s" % h)
2c8f3d
        print_line.hdr = False
2c8f3d
        for e, old, new in sorted_clog:
2c8f3d
            print_line(e, old, new, types)
2c8f3d
2c8f3d
2c8f3d
def print_changelog(clog, cmap, args):
2c8f3d
    if args.format == "rpm":
2c8f3d
        print_changelog_rpm(clog, cmap, args)
2c8f3d
    elif args.format == "intel":
2c8f3d
        print_changelog_intel(clog, cmap, args)
2c8f3d
    else:
2c8f3d
        log_error(("unknown changelog format: \"%s\". " +
2c8f3d
                   "Supported formats are: rpm, intel.") % args.format)
2c8f3d
2c8f3d
da1320
class TableStyles:
da1320
    TS_CSV = 0
da1320
    TS_FANCY = 1
da1320
da1320
da1320
def print_line(line, column_sz):
da1320
    print(" | ".join([str(x).ljust(column_sz[i])
da1320
                      for i, x in zip(itertools.count(),
da1320
                                      itertools.chain(line,
da1320
                                      [""] * (len(column_sz) -
da1320
                                              len(line))))]).rstrip())
da1320
da1320
da1320
def print_table(items, header=[], style=TableStyles.TS_CSV):
da1320
    if style == TableStyles.TS_CSV:
da1320
        for i in items:
da1320
            print(";".join(i))
da1320
    elif style == TableStyles.TS_FANCY:
da1320
        column_sz = list(reduce(lambda x, y:
da1320
                                map(max, izip_longest(x, y, fillvalue=0)),
da1320
                                [[len(x) for x in i]
da1320
                                 for i in itertools.chain(header, items)]))
da1320
        for i in header:
da1320
            print_line(i, column_sz)
da1320
        if header:
da1320
            print("-+-".join(["-" * x for x in column_sz]))
da1320
        for i in items:
da1320
            print_line(i, column_sz)
da1320
da1320
da1320
def print_summary(revs, cmap, args):
da1320
    m = gen_fn_map(revs)
da1320
    cnames_mode = mcnm.MCNM_ABBREV if args.abbrev else mcnm.MCNM_CODENAME
da1320
da1320
    header = []
da1320
    if args.header:
2c8f3d
        header.append(["Path", "Offset", "Ext. Offset", "Data Size",
2c8f3d
                       "Total Size", "CPUID", "Platform ID Mask", "Revision",
2c8f3d
                       "Date", "Checksum", "Codenames"] +
da1320
                      (["Models"] if args.models else []))
da1320
    tbl = []
da1320
    for k in sorted(m.keys()):
da1320
        for mc in m[k]:
da1320
            tbl.append([mc_stripped_path(mc),
da1320
                        "0x%x" % mc["offs"],
da1320
                        "0x%x" % mc["ext_offs"] if "ext_offs" in mc else "-",
2c8f3d
                        "0x%05x" % mc["data_size"],
2c8f3d
                        "0x%05x" % mc["total_size"],
da1320
                        "0x%05x" % mc["cpuid"],
da1320
                        "0x%02x" % mc["pf"],
da1320
                        mc_rev(mc, date=False),
da1320
                        mc_date(mc),
2c8f3d
                        "0x%08x" % (mc["ext_cksum"]
2c8f3d
                                    if "ext_cksum" in mc else mc["cksum"]),
2c8f3d
                        get_mc_cnames(mc, cmap, cnames_mode,
2c8f3d
                                      segment=args.segment) or ""] +
da1320
                       ([get_mc_cnames(mc, cmap,
2c8f3d
                                       mcnm.MCNM_FAMILIES_MODELS,
2c8f3d
                                       segment=args.segment)]
da1320
                        if args.models else []))
da1320
da1320
    print_table(tbl, header, style=TableStyles.TS_FANCY)
da1320
da1320
da1320
def read_codenames_file(path):
da1320
    '''
da1320
    Supports two formats: new and old
da1320
     * old: tab-separated. Field order:
da1320
       Segment, (unused), Codename, (dash-separated) Stepping,
da1320
       Platform ID mask, CPUID, (unused) Update link, (unused) Specs link
da1320
     * new: semicolon-separated; support comments.  Distinguished
da1320
       by the first line that starts with octothorp.  Field order:
da1320
       Segment, Unused, Codename, Stepping, Platform ID mask, CPUID,
da1320
       Abbreviation, Variant(s), Families, Models
da1320
    '''
da1320
    old_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig",
da1320
                  "_update", "_specs"]
da1320
    new_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig",
da1320
                  "abbrev", "variant", "families", "models"]
da1320
    new_fmt = False
da1320
    field_names = old_fields
da1320
da1320
    res = dict()
da1320
da1320
    try:
da1320
        with open(path, "r") as f:
da1320
            for line in f:
da1320
                line = line.strip()
da1320
                if len(line) == 0:
da1320
                    continue
da1320
                if line[0] == '#':
da1320
                    new_fmt = True
da1320
                    field_names = new_fields
da1320
                    continue
da1320
da1320
                fields = line.split(";" if new_fmt else "\t",
da1320
                                    1 + len(field_names))
da1320
                fields = dict(zip(field_names, fields))
da1320
                if "sig" not in fields:
da1320
                    log_warn("Skipping %r (from \"%s\")" % (fields, line))
da1320
                    continue
da1320
da1320
                sig = fields["sig"] = int(fields["sig"], 16)
da1320
                fields["pf_mask"] = int(fields["pf_mask"], 16)
da1320
                fields["stepping"] = fields["stepping"].split(",")
da1320
                if "variant" in fields:
da1320
                    if fields["variant"]:
da1320
                        fields["variant"] = fields["variant"].split(",")
da1320
                    else:
da1320
                        fields["variant"] = []
da1320
da1320
                if sig not in res:
da1320
                    res[sig] = list()
da1320
                res[sig].append(fields)
da1320
    except Exception as e:
da1320
        log_error("a problem occurred while reading code names: %s" % e)
da1320
da1320
    return res
da1320
da1320
da1320
def print_discrepancies(rev_map, deps, cmap, args):
da1320
    """
da1320
    rev_map: dict "name": revs
da1320
    deps: list of tuples (name, parent/None)
da1320
    """
da1320
    sigs = set()
da1320
da1320
    for p, r in rev_map.items():
da1320
        sigs |= set(r.keys())
da1320
da1320
    if args.header:
da1320
        header1 = ["sig"]
da1320
        if args.print_vs:
da1320
            header2 = [""]
da1320
        for p, n, d in deps:
da1320
            header1.append(n)
da1320
            if args.print_vs:
da1320
                add = ""
da1320
                if d:
da1320
                    for pd, nd, dd in deps:
da1320
                        if pd == d:
da1320
                            add = "(vs. %s)" % nd
da1320
                            break
da1320
                header2.append(add)
da1320
        if args.models:
da1320
            header1.append("Model names")
da1320
            if args.print_vs:
da1320
                header2.append("")
da1320
    header = [header1] + ([header2] if args.print_vs else [])
da1320
da1320
    tbl = []
da1320
    for s in sorted(sigs):
da1320
        out = [mc_path(s)]
da1320
        print_out = not args.print_filter
da1320
        print_date = args.min_date is None
da1320
da1320
        for p, n, d in deps:
da1320
            cur = dict([(x["rev"], x) for x in rev_map[p][s]]) \
da1320
                  if s in rev_map[p] else []
da1320
            v = "/".join([mc_rev(y) for x, y in sorted(cur.items())]) \
da1320
                if cur else "-"
da1320
            if d is not None:
da1320
                prev = [x["rev"] for x in rev_map[d][s]] if s in rev_map[d] \
da1320
                        else []
da1320
                if [x for x in cur if x not in prev]:
da1320
                    v += " (*)"
da1320
                    print_out = True
da1320
            if args.min_date is not None and s in rev_map[p]:
da1320
                for x in rev_map[p][s]:
da1320
                    print_date |= mc_date(x) > args.min_date
da1320
            out.append(v)
da1320
da1320
        if print_out and print_date:
da1320
            if args.models:
2c8f3d
                out.append(get_mc_cnames(s, cmap, segment=args.segment) or "")
da1320
            tbl.append(out)
da1320
da1320
    print_table(tbl, header, style=TableStyles.TS_FANCY)
da1320
da1320
da1320
def cmd_summary(args):
da1320
    revs = []
da1320
    for p in args.filelist:
2c8f3d
        revs = read_revs(p, args, ret=revs)
da1320
da1320
    codenames_map = read_codenames_file(args.codenames)
da1320
da1320
    print_summary(revs, codenames_map, args)
da1320
da1320
    return 0
da1320
da1320
da1320
def cmd_changelog(args):
da1320
    codenames_map = read_codenames_file(args.codenames)
da1320
    base_path = args.filelist[0]
da1320
    upd_path = args.filelist[1]
da1320
2c8f3d
    base = read_revs(base_path, args)
2c8f3d
    upd = read_revs(upd_path, args)
da1320
da1320
    print_changelog(gen_changelog(base, upd), codenames_map, args)
da1320
da1320
    return 0
da1320
da1320
da1320
def cmd_discrepancies(args):
da1320
    """
da1320
    filenames:
da1320
     * "<" prefix (possibly multiple times) to refer to a previous entry
da1320
       to compare against
da1320
     * "[name]" prefix is a name reference
da1320
    """
da1320
    codenames_map = read_codenames_file(args.codenames)
da1320
    rev_map = dict()
da1320
    deps = list()
da1320
    cur = -1
da1320
da1320
    for path in args.filelist:
da1320
        orig_path = path
da1320
        name = None
da1320
        cur += 1
da1320
        dep = None
da1320
        while True:
da1320
            if path[0] == '<':
da1320
                path = path[1:]
da1320
                dep = cur - 1 if dep is None else dep - 1
da1320
            elif path[0] == '[' and path.find(']') > 0:
da1320
                pos = path.find(']')
da1320
                name = path[1:pos]
da1320
                path = path[pos + 1:]
da1320
            else:
da1320
                break
da1320
        if name is None:
da1320
            name = path
da1320
        if dep is not None and dep < 0:
da1320
            log_error("Incorrect dep reference for '%s' (points to index %d)" %
da1320
                      (orig_path, dep))
da1320
            return 1
da1320
        deps.append((path, name, deps[dep][0] if dep is not None else None))
2c8f3d
        rev_map[path] = gen_fn_map(read_revs(path, args), merge=args.merge,
da1320
                                   merge_path=True)
da1320
da1320
    print_discrepancies(rev_map, deps, codenames_map, args)
da1320
da1320
    return 0
da1320
da1320
da1320
def parse_cli():
da1320
    root_parser = argparse.ArgumentParser(prog="gen_updates",
da1320
                                          description="Intel CPU Microcode " +
da1320
                                          "parser")
da1320
    root_parser.add_argument("-C", "--codenames", default='codenames',
da1320
                             help="Code names file")
da1320
    root_parser.add_argument("-v", "--verbose", action="count", default=0,
da1320
                             help="Increase output verbosity")
2c8f3d
    root_parser.add_argument("-E", "--no-ignore-ext-duplicates",
2c8f3d
                             action="store_const", dest="ignore_ext_dups",
2c8f3d
                             default=False, const=False,
2c8f3d
                             help="Do not ignore duplicates of the main " +
2c8f3d
                                  "signature in the extended signature header")
2c8f3d
    root_parser.add_argument("-e", "--ignore-ext-duplicates",
2c8f3d
                             action="store_const", dest="ignore_ext_dups",
2c8f3d
                             const=True,
2c8f3d
                             help="Ignore duplicates of the main signature " +
2c8f3d
                                   "in the extended signature header")
2c8f3d
    root_parser.add_argument("-t", "--print-segment", action="store_const",
2c8f3d
                             dest="segment", const=True,
2c8f3d
                             help="Print model segment")
2c8f3d
    root_parser.add_argument("-T", "--no-print-segment", action="store_const",
2c8f3d
                             dest="segment", const=False, default=False,
2c8f3d
                             help="Do not print model segment")
da1320
da1320
    cmdparsers = root_parser.add_subparsers(title="Commands",
da1320
                                            help="main gen_updates commands")
da1320
da1320
    parser_s = cmdparsers.add_parser("summary",
da1320
                                     help="Generate microcode summary")
da1320
    parser_s.add_argument("-a", "--abbreviate", action="store_const",
da1320
                          dest="abbrev", const=True, default=True,
da1320
                          help="Abbreviate code names")
da1320
    parser_s.add_argument("-A", "--no-abbreviate", action="store_const",
da1320
                          dest="abbrev", const=False,
da1320
                          help="Do not abbreviate code names")
da1320
    parser_s.add_argument("-m", "--print-models", action="store_const",
da1320
                          dest="models", const=True, default=False,
da1320
                          help="Print models")
da1320
    parser_s.add_argument("-M", "--no-print-models",
da1320
                          action="store_const", dest="models",
da1320
                          const=False, help="Do not print models")
da1320
    parser_s.add_argument("-H", "--no-print-header",
da1320
                          action="store_const", dest="header",
da1320
                          const=False, default=True,
da1320
                          help="Do not print hader")
da1320
    parser_s.add_argument("filelist", nargs="*", default=[],
da1320
                          help="List or RPMs/directories to process")
da1320
    parser_s.set_defaults(func=cmd_summary)
da1320
da1320
    parser_c = cmdparsers.add_parser("changelog",
da1320
                                     help="Generate changelog")
2c8f3d
    parser_c.add_argument("-F", "--format", choices=["rpm", "intel"],
2c8f3d
                          default="rpm", help="Changelog format")
da1320
    parser_c.add_argument("filelist", nargs=2,
da1320
                          help="RPMs/directories to compare")
da1320
    parser_c.set_defaults(func=cmd_changelog)
da1320
da1320
    parser_d = cmdparsers.add_parser("discrepancies",
da1320
                                     help="Generate discrepancies")
da1320
    parser_d.add_argument("-s", "--merge-revs", action="store_const",
da1320
                          dest="merge", const=True, default=False,
da1320
                          help="Merge revisions that come" +
da1320
                               " from different files")
da1320
    parser_d.add_argument("-S", "--no-merge-revs", action="store_const",
da1320
                          dest="merge", const=False,
da1320
                          help="Do not Merge revisions that come" +
da1320
                               " from different files")
da1320
    parser_d.add_argument("-v", "--print-vs", action="store_const",
da1320
                          dest="print_vs", const=True, default=False,
da1320
                          help="Print base version ")
da1320
    parser_d.add_argument("-V", "--no-print-vs", action="store_const",
da1320
                          dest="print_vs", const=False,
da1320
                          help="Do not Merge revisions that come" +
da1320
                               " from different files")
da1320
    parser_d.add_argument("-m", "--print-models", action="store_const",
da1320
                          dest="models", const=True, default=True,
da1320
                          help="Print model names")
da1320
    parser_d.add_argument("-M", "--no-print-models", action="store_const",
da1320
                          dest="models", const=False,
da1320
                          help="Do not print model names")
da1320
    parser_d.add_argument("-H", "--no-print-header", action="store_const",
da1320
                          dest="header", const=False, default=True,
da1320
                          help="Do not print hader")
da1320
    parser_d.add_argument("-a", "--print-all-files", action="store_const",
da1320
                          dest="print_filter", const=False, default=True,
da1320
                          help="Print all files")
da1320
    parser_d.add_argument("-c", "--print-changed-files", action="store_const",
da1320
                          dest="print_filter", const=True,
da1320
                          help="Print only changed files")
da1320
    parser_d.add_argument("-d", "--min-date", action="store",
da1320
                          help="Minimum date filter")
da1320
    parser_d.add_argument("filelist", nargs='*',
da1320
                          help="RPMs/directories to compare")
da1320
    parser_d.set_defaults(func=cmd_discrepancies)
da1320
da1320
    args = root_parser.parse_args()
da1320
    if not hasattr(args, "func"):
da1320
        root_parser.print_help()
da1320
        return None
2c8f3d
2c8f3d
    global log_level
2c8f3d
    log_level = args.verbose
2c8f3d
da1320
    return args
da1320
da1320
da1320
def main():
da1320
    args = parse_cli()
da1320
    if args is None:
da1320
        return 1
da1320
da1320
    return args.func(args)
da1320
da1320
da1320
if __name__ == "__main__":
da1320
    sys.exit(main())