from difflib import SequenceMatcher
from itertools import zip_longest
from logging import getLogger
from typing import Any, Mapping, Tuple
from flask import render_template, url_for
from jsondiff import symbols
from markupsafe import Markup, escape
from sec_certs.cert_rules import cc_rules, fips_rules
from sec_certs.utils.extract import scheme_frontpage_functions
from ..common.objformats import WorkingFormat
logger = getLogger(__name__)
def has_symbols(obj):
def walk(o):
if isinstance(o, dict):
for k in o:
if k in symbols._all_symbols_:
return True
elif walk(o[k]):
return True
elif isinstance(o, (tuple, list, set)):
for k in o:
if walk(k):
return True
return False
return walk(obj)
def apply_explicit_diff(dct, diff):
"""
Apply an explicit diff constructed by jsondiff.
:param dct: The object to apply on.
:param diff: The diff.
:return: A new object with the diff applied.
"""
def walk(obj, d):
if isinstance(obj, dict):
c = dict(obj)
if symbols.insert in d:
c.update(dict(d[symbols.insert]))
if symbols.update in d:
for k, v in d[symbols.update].items():
if has_symbols(v):
c[k] = walk(c[k], v)
else:
c[k] = v
if symbols.delete in d:
for k in d[symbols.delete]:
del c[k]
return c
elif isinstance(obj, (list, tuple)):
original_type = type(obj)
c = list(obj)
if symbols.delete in d:
for pos in d[symbols.delete]:
c.pop(pos)
if symbols.insert in d:
for pos, value in d[symbols.insert]:
c.insert(pos, value)
for k, v in d.items():
if k is not symbols.delete and k is not symbols.insert:
k = int(k)
c[k] = walk(c[k], v)
if original_type is not list:
c = original_type(c)
return c
elif isinstance(obj, set):
c = set(obj)
if symbols.discard in d:
for x in d[symbols.discard]:
c.discard(x)
if symbols.add in d:
for x in d[symbols.add]:
c.add(x)
return c
return obj
return walk(dct, diff)
class DiffRenderer:
collection: str
diff_collection: str
log_collection: str
templates: Mapping[str, str]
k2map: Mapping[str, Tuple[str, bool]]
def render_diff(self, hashid, cert, diff, **kwargs) -> Markup:
"""
Render a diff into HTML.
This is quite disgusting, but works for now.
"""
def render_code_template(template_str: str, vjson, **kws) -> Markup:
template = Markup(template_str)
return template.format(vjson=str(WorkingFormat(vjson).to_storage_format().to_json_mapping()), **kws)
if diff["type"] == "new":
return Markup(render_template(self.templates["new"], cert=diff["diff"], hashid=hashid, **kwargs))
elif diff["type"] == "back":
return Markup(render_template(self.templates["back"], cert=cert, hashid=hashid, **kwargs))
elif diff["type"] == "remove":
return Markup(render_template(self.templates["remove"], cert=cert, hashid=hashid, **kwargs))
elif diff["type"] == "change":
changes = []
for k1, v1 in diff["diff"].items():
if k1 == symbols.update:
for k2, v2 in v1.items():
details = []
if has_symbols(v2):
for k3, v3 in v2.items():
if k3 == symbols.update:
if isinstance(v3, dict):
for prop, val in v3.items():
if has_symbols(val):
detail = f"The {prop} property was updated."
if symbols.insert in val:
detail = render_code_template(
"The {prop} property was updated, with the {vjson} values inserted.",
val[symbols.insert],
prop=str(prop),
)
if symbols.discard in val:
detail = render_code_template(
"The {prop} property was updated, with the {vjson} values discarded.",
val[symbols.discard],
prop=str(prop),
)
if symbols.update in val:
detail = render_code_template(
"The {prop} property was updated, with the {vjson} data.",
val[symbols.update],
prop=str(prop),
)
if symbols.add in val:
detail = render_code_template(
"The {prop} property was updated, with the {vjson} values added.",
val[symbols.add],
prop=str(prop),
)
details.append(detail)
else:
details.append(
render_code_template(
"The {prop} property was set to {vjson}.",
val,
prop=str(prop),
)
)
elif k3 == symbols.insert:
if has_symbols(v3):
logger.error(f"Should not happen, ins: {k3}, {v3}")
else:
details.append(
render_code_template(
"The following values were inserted: {vjson}.", v3
)
)
elif k3 == symbols.delete:
details.append(
render_code_template(
"The following properties were deleted: {vjson}.", v3
)
)
elif k3 == symbols.add:
if has_symbols(v3):
logger.error(f"Should not happen, add: {k3}, {v3}")
else:
details.append(
render_code_template(
"The following values were added: {vjson}.", v3
)
)
elif k3 == symbols.discard:
if has_symbols(v3):
logger.error(f"Should not happen, discard: {k3}, {v3}")
else:
details.append(
render_code_template(
"The following values were removed: {vjson}.", v3
)
)
else:
logger.error(f"Should not happen: {k3}, {v3}")
else:
details.append(render_code_template("The new value is {vjson}.", v2))
# Add the rendered change into the list.
changes.append((self.k2map.get(k2, (k2, False)), details))
return Markup(
render_template(self.templates["change"], cert=cert, changes=changes, hashid=hashid, **kwargs)
)
else:
raise ValueError("Invalid diff type")
def bold(a: Any) -> Markup:
return Markup(f"{escape(a)}")
def normal(a: Any) -> Markup:
return escape(a)
def comma_separate(values):
return Markup(", ".join(map(str, values)))
def diff_none():
return None
def diff_int():
def compare(a, b):
return a == b
def render(equal: bool, a: Any, b: Any) -> Markup:
return (normal if equal else bold)(a)
return compare, render
def diff_bool():
def compare(a, b):
return a == b
def render(equal: bool, a: Any, b: Any) -> Markup:
return (normal if equal else bold)(a)
return compare, render
def diff_str():
def compare(a, b):
return a == b
def render(equal: bool, a: Any, b: Any) -> Markup:
if equal:
return normal(a)
if a is None:
return Markup("")
if b is None:
return bold(a)
matcher = SequenceMatcher(lambda x: x == " ", a, b)
if matcher.ratio() < 0.2:
return bold(a)
result = ""
prev_end = 0
for start_a, start_b, n in matcher.get_matching_blocks():
end = start_a + n
if n == 1:
# add a[prev_end:end] to result in bold (skip single character matches)
result += bold(a[prev_end:end])
else:
# add a[prev_end:start_a] to result in normal
result += bold(a[prev_end:start_a])
# add a[start_a:start_a+n] to result in bold
result += normal(a[start_a:end])
# set prev_end to start_a+n
prev_end = end
return Markup(result)
return compare, render
def diff_ident():
compare_str, render_str = diff_str()
def render(equal: bool, a: Any, b: Any) -> Markup:
if equal:
return Markup(f'Equal')
else:
return Markup(f'Different')
return compare_str, render
def diff_url():
compare_str, render_str = diff_str()
def render(equal: bool, a: Any, b: Any) -> Markup:
if a:
return Markup(f'{render_str(equal, a, b)}')
else:
return render_str(equal, a, b)
return compare_str, render
def diff_set(elem_diff):
_, render_elem = elem_diff
def compare(a, b):
return a == b
def render(equal: bool, a: Any, b: Any) -> Markup:
return comma_separate([render_elem(elem in b if b else False, elem, None) for elem in a]) if a else escape("{}")
return compare, render
def diff_list(elem_diff):
compare_elem, render_elem = elem_diff
def compare(a, b):
return a == b
def render(equal: bool, a: Any, b: Any) -> Markup:
return (
comma_separate(
[
render_elem(compare_elem(a_elem, b_elem), a_elem, b_elem)
for a_elem, b_elem in zip_longest(a, b if b else [])
]
)
if a
else escape("[]")
)
return compare, render
def diff_keywords():
def compare(a, b):
return a == b
compare_set, render_set = diff_set(diff_str())
compare_list, render_list = diff_list(diff_str())
compare_int, render_int = diff_int()
def render(equal: bool, a: Any, b: Any) -> Markup:
def render_dict(one, other):
if not one:
return Markup("")
if not other:
other = {}
items = []
for key, val in sorted(one.items()):
label = (bold if key not in other else normal)(key)
change = False
span = False
if isinstance(val, dict):
other_val = other.get(key, {})
item = render_dict(val, other_val)
elif isinstance(val, set):
other_val = other.get(key, set())
item = render_set(compare_set(val, other_val), val, other_val)
elif isinstance(val, list):
other_val = other.get(key, [])
item = render_list(compare_list(val, other_val), val, other_val)
else:
other_val = other.get(key, None)
item = render_int(compare_int(val, other_val), val, other_val)
span = True
change = val != other_val
if span:
if change:
line = f'{label}: {item}'
else:
line = f"{label}: {item}"
else:
line = f"{label}: {item}"
items.append(Markup(f"