427 lines
22 KiB
Python
427 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""implementare-integritate.py: an INDEPENDENT implementation, written from the rules and not from the JavaScript code, in another
|
|
language, with nothing but the Python standard library. It exists to show that the conformance vectors are not tied to one
|
|
implementation: the same runner scores it, and it passes every vector whose rules it claims.
|
|
|
|
It claims little, on purpose, and says so through --capabilities:
|
|
envelope envelope.form, envelope.integrity (it cannot check an ML-DSA signature or read a chain: vectors that exercise
|
|
those rules are reported by the runner as NOT COVERED, never as passed)
|
|
dataset-record dataset.form, dataset.commitment, dataset.field, dataset.inclusion (RFC 9162 inclusion, salted field commitments)
|
|
device-reputation reputation.form, reputation.figures (2026-10-20: the formula aere-device-reputation/1 of the device operation
|
|
component, computed again from the facts and compared with the figures stated; written from the formula in words in that
|
|
component's README, not from its code: the windows are found by bisection over their starts, not by division, the
|
|
gaps by one pass over the windows, and the comparison is of canonical texts written by this file, never of Python values,
|
|
in which True == 1. The form of the facts follows the members and bounds that README names and the patterns the
|
|
published form gives; the operation log itself carries ML-DSA-65 signatures, which this file cannot check, so its
|
|
vectors are NOT COVERED)
|
|
|
|
python3 implementare-integritate.py --capabilities
|
|
python3 implementare-integritate.py <vector.json>
|
|
python3 implementare-integritate.py --batch <list.json>
|
|
|
|
Limits of this implementation: numbers are written the way JavaScript writes them only for integers and for the common decimal
|
|
floats (no exponent forms); object keys are sorted by code point, which equals JavaScript's order for keys in the Basic
|
|
Multilingual Plane. Files are read with Python's json module: a file nested deeper than its recursion limit is not read (an error,
|
|
which the runner scores BROKEN, never a verdict), and the non-JSON words NaN and Infinity, which JSON.parse refuses, are read. It
|
|
never opens a connection: --online is not claimed.
|
|
"""
|
|
import bisect
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
# a whole number of 5,000 digits is read, as JavaScript reads it (to a number it then judges), instead of stopping the reader
|
|
if hasattr(sys, 'set_int_max_str_digits'):
|
|
sys.set_int_max_str_digits(0)
|
|
|
|
LEVELS = ['integrity', 'authenticity', 'anchoring', 'finality']
|
|
H64 = re.compile(r'^0x[0-9a-fA-F]{64}$')
|
|
H64_LOW = re.compile(r'^0x[0-9a-f]{64}$')
|
|
KIND_RECORD = 'aere-verifiable-record'
|
|
|
|
|
|
def sha256_hex(b):
|
|
return '0x' + hashlib.sha256(b).hexdigest()
|
|
|
|
|
|
def js_number(x):
|
|
if isinstance(x, bool):
|
|
raise ValueError('not a number')
|
|
if isinstance(x, int):
|
|
return str(x)
|
|
if x != x or x in (float('inf'), float('-inf')):
|
|
raise ValueError('a number must be finite')
|
|
if x == int(x) and abs(x) < 1e21:
|
|
return str(int(x))
|
|
r = repr(x)
|
|
if 'e' in r or 'E' in r:
|
|
raise ValueError('exponent form: not reproduced by this implementation')
|
|
return r
|
|
|
|
|
|
def js_stringify(x):
|
|
"""JSON.stringify(x) for parsed JSON: insertion order of keys, no whitespace."""
|
|
if x is None:
|
|
return 'null'
|
|
if x is True:
|
|
return 'true'
|
|
if x is False:
|
|
return 'false'
|
|
if isinstance(x, str):
|
|
return json.dumps(x, ensure_ascii=False)
|
|
if isinstance(x, (int, float)):
|
|
return js_number(x)
|
|
if isinstance(x, list):
|
|
return '[' + ','.join(js_stringify(v) for v in x) + ']'
|
|
if isinstance(x, dict):
|
|
return '{' + ','.join(json.dumps(k, ensure_ascii=False) + ':' + js_stringify(v) for k, v in x.items()) + '}'
|
|
raise ValueError('unsupported value')
|
|
|
|
|
|
def canonical(x):
|
|
"""Canonical JSON of the dataset format: keys sorted at every depth, no whitespace."""
|
|
if isinstance(x, list):
|
|
return '[' + ','.join(canonical(v) for v in x) + ']'
|
|
if isinstance(x, dict):
|
|
return '{' + ','.join(json.dumps(k, ensure_ascii=False) + ':' + canonical(x[k]) for k in sorted(x.keys())) + '}'
|
|
return js_stringify(x)
|
|
|
|
|
|
def verdict(artifact, failed_by_level, reasons, passed, partial=None):
|
|
"""The one verdict form: four levels, each PASSED, FAILED or NOT_MEASURED with its reason."""
|
|
levels = []
|
|
for l in LEVELS:
|
|
if failed_by_level.get(l):
|
|
levels.append({'level': l, 'state': 'FAILED', 'rules': sorted(set(failed_by_level[l])), 'reason': '; '.join(reasons)})
|
|
elif partial and l in partial:
|
|
levels.append({'level': l, 'state': 'NOT_MEASURED', 'rules': [], 'cause': partial[l][0], 'reason': partial[l][1]})
|
|
elif l in passed:
|
|
levels.append({'level': l, 'state': 'PASSED', 'rules': [], 'reason': passed[l]})
|
|
else:
|
|
levels.append({'level': l, 'state': 'NOT_MEASURED', 'rules': [], 'cause': 'not-judged', 'reason': 'this implementation does not judge this level'})
|
|
failed = sorted({r for x in levels for r in x['rules']})
|
|
is_partial = any(x['state'] == 'NOT_MEASURED' and x.get('cause') in ('claim-not-checked', 'unmeasurable') for x in levels)
|
|
v = 'INVALID' if failed else ('PARTIAL' if is_partial or levels[0]['state'] != 'PASSED' else 'VALID')
|
|
return {'v': 1, 'kind': 'aere-conformance-verdict', 'artifact': artifact, 'verdict': v, 'levels': levels, 'failed': failed}
|
|
|
|
|
|
def judge_envelope(inp):
|
|
env = inp.get('envelope')
|
|
if isinstance(inp.get('raw'), str): # the text of a file, in place of the envelope: it may not be JSON at all
|
|
try:
|
|
env = json.loads(inp['raw'])
|
|
except ValueError:
|
|
return verdict('envelope', {'integrity': ['envelope.form']}, ['the file is not JSON'], {})
|
|
ok_form = isinstance(env, dict) and isinstance(env.get('statement'), dict) and isinstance(env.get('statementHash'), str) and bool(H64.match(env['statementHash']))
|
|
if not ok_form:
|
|
return verdict('envelope', {'integrity': ['envelope.form']}, ['the envelope lacks a statement (object) and a statementHash (0x + 64 hex)'], {})
|
|
h = sha256_hex(js_stringify(env['statement']).encode('utf-8'))
|
|
if h != env['statementHash'].lower():
|
|
return verdict('envelope', {'integrity': ['envelope.integrity']}, ['sha256 of the statement text is ' + h[:18] + '.., not the statementHash'], {})
|
|
partial = {}
|
|
if env.get('signature'):
|
|
partial['authenticity'] = ('unmeasurable', 'the envelope carries a signature this implementation cannot check')
|
|
if env.get('notarization'):
|
|
why = ('claim-not-checked', 'the envelope claims a notarization; this implementation never reads a chain')
|
|
partial['anchoring'] = why
|
|
partial['finality'] = why
|
|
return verdict('envelope', {}, [], {'integrity': 'form and statement hash hold'}, partial)
|
|
|
|
|
|
def node_hash(a, b):
|
|
return hashlib.sha256(b'\x01' + a + b).digest()
|
|
|
|
|
|
def unhex(s):
|
|
if not isinstance(s, str) or not H64_LOW.match(s):
|
|
raise ValueError('not a 32-byte hash')
|
|
return bytes.fromhex(s[2:])
|
|
|
|
|
|
def is_int(x):
|
|
return type(x) is int
|
|
|
|
|
|
def verify_inclusion(leaf, index, tree_size, path, root):
|
|
"""RFC 9162, 2.1.3.2."""
|
|
try:
|
|
if not (is_int(index) and is_int(tree_size) and 0 <= index < tree_size and isinstance(path, list)):
|
|
return False
|
|
fn, sn, r = index, tree_size - 1, unhex(leaf)
|
|
for p in path:
|
|
pb = unhex(p)
|
|
if sn == 0:
|
|
return False
|
|
if (fn & 1) or fn == sn:
|
|
r = node_hash(pb, r)
|
|
if not (fn & 1):
|
|
while not (fn & 1) and fn != 0:
|
|
fn >>= 1
|
|
sn >>= 1
|
|
else:
|
|
r = node_hash(r, pb)
|
|
fn >>= 1
|
|
sn >>= 1
|
|
return sn == 0 and '0x' + r.hex() == str(root)
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def judge_record(inp):
|
|
rec, com = inp.get('record'), inp.get('commitment')
|
|
failed, reasons = [], []
|
|
if not isinstance(rec, dict) or rec.get('kind') != KIND_RECORD or not isinstance(com, dict):
|
|
return verdict('dataset-record', {'integrity': ['dataset.form']}, ['not a ' + KIND_RECORD], {})
|
|
if not (rec.get('commitmentHash') == sha256_hex(canonical(com).encode('utf-8')) and rec.get('version') == com.get('version')):
|
|
failed.append('dataset.commitment')
|
|
reasons.append('the record names another commitment')
|
|
|
|
def row():
|
|
c, disclosed = rec.get('c'), rec.get('disclosed')
|
|
if not (isinstance(c, (dict, list)) and isinstance(disclosed, (dict, list))):
|
|
failed.append('dataset.form')
|
|
reasons.append('malformed row')
|
|
return
|
|
for k, d in (disclosed.items() if isinstance(disclosed, dict) else enumerate(disclosed)):
|
|
if not (isinstance(d, dict) and isinstance(d.get('salt'), str)):
|
|
failed.append('dataset.field')
|
|
reasons.append('disclosed field ' + str(k) + ' without a salt')
|
|
return
|
|
want = sha256_hex((d['salt'] + '|' + str(k) + '|' + canonical(d.get('value'))).encode('utf-8'))
|
|
if not (isinstance(c, dict) and c.get(k) == want):
|
|
failed.append('dataset.field')
|
|
reasons.append('the disclosed value of "' + str(k) + '" does not match its commitment')
|
|
return
|
|
leaf = sha256_hex(b'\x00' + canonical({'id': rec.get('id'), 'c': c}).encode('utf-8'))
|
|
lp = rec.get('logProof')
|
|
if not (isinstance(lp, dict) and verify_inclusion(leaf, lp.get('index'), com.get('size'), lp.get('path'), com.get('logRoot')) and lp.get('index') == rec.get('i')):
|
|
failed.append('dataset.inclusion')
|
|
reasons.append('the record is not in the log of version ' + str(com.get('version')))
|
|
|
|
row()
|
|
passed = {} if failed else {'integrity': 'the record opens its commitments and is in the log'}
|
|
return verdict('dataset-record', {'integrity': failed} if failed else {}, reasons, passed)
|
|
|
|
|
|
# ---------------------------------------------------------------- device-reputation (2026-10-20)
|
|
# The formula, in the words of the device operation README ("The reputation formula"): windows of W = windowS * 1000 ms over [from, to);
|
|
# a counted record covers by the device's clock the window its `at` falls in, and is witnessed in the window its `lo` falls in when its whole
|
|
# interval [lo, hi] lies in that window; availability, per mille rounded down, the runs of windows not covered by the clock, the proofs of
|
|
# equivocation and the retirements copied, the age from the first counted record and from the lowest witnessed upper bound, the evidence
|
|
# of every window covered either way, and the hash of the canonical facts. Integers only.
|
|
REP_KIND, REP_FORMULA, FACTS_KIND = 'aere-device-reputation', 'aere-device-reputation/1', 'aere-device-operation-facts'
|
|
FACT_MEMBERS = ('v', 'kind', 'verdict', 'rules', 'unjudged', 'manufacturer', 'keys', 'records', 'uncounted', 'equivocations', 'retirements', 'chains')
|
|
REC_MEMBERS = ('seq', 'hash', 'at', 'lo', 'hi', 'state', 'counter')
|
|
REP_STATES = ('operating', 'idle', 'fault', 'maintenance')
|
|
REP_VERDICTS = ('VALID', 'PARTIAL', 'INVALID')
|
|
SAFE_INT = 9007199254740991 # 2^53 - 1: what JavaScript holds exactly
|
|
TIME_MAX = 4503599627370496 # 2^52 ms
|
|
WINDOW_MAX_S = 366 * 86400
|
|
WINDOWS_MAX, RECORDS_MAX, KEYS_MAX, EQUIV_MAX, CHAINS_MAX, NAMES_MAX = 100000, 100000, 16, 4096, 64, 64
|
|
P_NAME = re.compile(r'[a-z][a-z0-9-]{0,63}')
|
|
P_HASH = re.compile(r'0x[0-9a-f]{64}')
|
|
P_DEVICE = re.compile(r'aere-device:[0-9a-f]{40}')
|
|
P_AERE_ID = re.compile(r'aere-id:[0-9a-f]{40}')
|
|
|
|
|
|
def whole(x, lo=0, hi=SAFE_INT):
|
|
"""The whole number JavaScript reads from this JSON value, or None. Python's json reads '7' as int and '7.0' or '7e0' as float;
|
|
JavaScript reads all three as the number 7. A boolean is never a number (in Python True == 1)."""
|
|
if isinstance(x, bool):
|
|
return None
|
|
if isinstance(x, float):
|
|
if not x.is_integer():
|
|
return None
|
|
x = int(x)
|
|
if not isinstance(x, int):
|
|
return None
|
|
return x if lo <= x <= hi else None
|
|
|
|
|
|
def text_of(x, pattern):
|
|
return isinstance(x, str) and pattern.fullmatch(x) is not None
|
|
|
|
|
|
def ascending(xs):
|
|
return all(xs[k] < xs[k + 1] for k in range(len(xs) - 1))
|
|
|
|
|
|
def members(o, names):
|
|
return isinstance(o, dict) and set(o) == set(names)
|
|
|
|
|
|
def facts_problem(f):
|
|
"""None when the facts are of the form the formula reads; otherwise what is not."""
|
|
if not members(f, FACT_MEMBERS):
|
|
return 'facts: not exactly ' + ', '.join(FACT_MEMBERS)
|
|
if whole(f['v']) != 1 or f['kind'] != FACTS_KIND:
|
|
return 'facts: not ' + FACTS_KIND + ' version 1'
|
|
if not (isinstance(f['verdict'], str) and f['verdict'] in REP_VERDICTS):
|
|
return 'facts: verdict'
|
|
for k in ('rules', 'unjudged'):
|
|
x = f[k]
|
|
if not (isinstance(x, list) and len(x) <= NAMES_MAX and all(text_of(n, P_NAME) for n in x) and ascending(x)):
|
|
return 'facts: ' + k + ' is a sorted list of distinct names'
|
|
if not (f['manufacturer'] is None or text_of(f['manufacturer'], P_AERE_ID)):
|
|
return 'facts: manufacturer'
|
|
if not (isinstance(f['keys'], list) and len(f['keys']) <= KEYS_MAX and all(text_of(k, P_DEVICE) for k in f['keys'])):
|
|
return 'facts: keys'
|
|
rs = f['records']
|
|
if not (isinstance(rs, list) and len(rs) <= RECORDS_MAX):
|
|
return 'facts: records'
|
|
before = None
|
|
for r in rs:
|
|
if not members(r, REC_MEMBERS):
|
|
return 'facts: a record is not exactly ' + ', '.join(REC_MEMBERS)
|
|
seq, at, counter = whole(r['seq']), whole(r['at'], 0, TIME_MAX), whole(r['counter'])
|
|
bounds_ok = all(r[b] is None or whole(r[b], 0, TIME_MAX) is not None for b in ('lo', 'hi'))
|
|
if seq is None or at is None or counter is None or not bounds_ok or not text_of(r['hash'], P_HASH) or not (isinstance(r['state'], str) and r['state'] in REP_STATES):
|
|
return 'facts: a record has a member of another form'
|
|
if before is not None and not (before[0] < seq and before[1] <= at and before[2] <= counter):
|
|
return 'facts: a record does not follow the one before it'
|
|
before = (seq, at, counter)
|
|
if whole(f['uncounted']) is None:
|
|
return 'facts: uncounted'
|
|
es = f['equivocations']
|
|
if not (isinstance(es, list) and len(es) <= EQUIV_MAX):
|
|
return 'facts: equivocations'
|
|
keys_e = []
|
|
for e in es:
|
|
if not (members(e, ('seq', 'a', 'b')) and whole(e['seq']) is not None and text_of(e['a'], P_HASH) and text_of(e['b'], P_HASH) and e['a'] != e['b']):
|
|
return 'facts: an equivocation is not { seq, a, b } with two different record hashes'
|
|
keys_e.append((whole(e['seq']), e['a'], e['b']))
|
|
if not ascending(keys_e):
|
|
return 'facts: equivocations are sorted by seq, a, b and distinct'
|
|
ts = f['retirements']
|
|
if not (isinstance(ts, list) and len(ts) <= KEYS_MAX and all(members(t, ('deviceId', 'at')) and text_of(t['deviceId'], P_DEVICE) and (t['at'] is None or whole(t['at'], 0, TIME_MAX) is not None) for t in ts)):
|
|
return 'facts: retirements'
|
|
c = f['chains']
|
|
if not members(c, ('anchors', 'beacons')):
|
|
return 'facts: chains'
|
|
for k in ('anchors', 'beacons'):
|
|
ids = c[k]
|
|
if not (isinstance(ids, list) and len(ids) <= CHAINS_MAX and all(whole(i, 1) is not None for i in ids) and ascending([whole(i, 1) for i in ids])):
|
|
return 'facts: chains.' + k
|
|
return None
|
|
|
|
|
|
def params_problem(p):
|
|
if not members(p, ('from', 'to', 'windowS')):
|
|
return 'params: not exactly from, to, windowS'
|
|
a, b, w = whole(p['from']), whole(p['to']), whole(p['windowS'], 1, WINDOW_MAX_S)
|
|
if a is None or b is None or not (a < b <= TIME_MAX):
|
|
return 'params: from and to'
|
|
if w is None:
|
|
return 'params: windowS'
|
|
if len(range(a, b, w * 1000)) > WINDOWS_MAX:
|
|
return 'params: windows'
|
|
return None
|
|
|
|
|
|
def reputation_of(f, p):
|
|
a, b, ws = whole(p['from']), whole(p['to']), whole(p['windowS'])
|
|
span = ws * 1000
|
|
starts = list(range(a, b, span)) # window i begins at starts[i]
|
|
n = len(starts)
|
|
ends = starts[1:] + [b] # and ends at the next start, the last at `to`
|
|
|
|
def window(t): # the window t falls in, or None outside [from, to)
|
|
if not a <= t < b:
|
|
return None
|
|
return bisect.bisect_right(starts, t) - 1
|
|
|
|
recs = [{'seq': whole(r['seq']), 'at': whole(r['at']), 'lo': None if r['lo'] is None else whole(r['lo']), 'hi': None if r['hi'] is None else whole(r['hi']), 'state': r['state']} for r in f['records']]
|
|
by_clock, by_witness = [None] * n, [None] * n
|
|
in_range, states = 0, dict((s, 0) for s in REP_STATES)
|
|
for r in recs:
|
|
i = window(r['at'])
|
|
if i is not None:
|
|
in_range += 1
|
|
states[r['state']] += 1
|
|
by_clock[i] = r['seq'] if by_clock[i] is None else min(by_clock[i], r['seq'])
|
|
if r['lo'] is not None and r['hi'] is not None and r['lo'] <= r['hi']:
|
|
k = window(r['lo'])
|
|
if k is not None and r['hi'] < ends[k]:
|
|
by_witness[k] = r['seq'] if by_witness[k] is None else min(by_witness[k], r['seq'])
|
|
runs, start = [], None
|
|
for i in range(n + 1):
|
|
empty = i < n and by_clock[i] is None
|
|
if empty and start is None:
|
|
start = i
|
|
elif not empty and start is not None:
|
|
runs.append([start, i - start])
|
|
start = None
|
|
covered_c = sum(1 for x in by_clock if x is not None)
|
|
covered_w = sum(1 for x in by_witness if x is not None)
|
|
first = min(recs, key=lambda r: r['seq']) if recs else None
|
|
his = [r['hi'] for r in recs if r['hi'] is not None]
|
|
low_hi = min(his) if his else None
|
|
return {
|
|
'v': 1, 'kind': REP_KIND, 'formula': REP_FORMULA,
|
|
'factsHash': sha256_hex(canonical(f).encode('utf-8')),
|
|
'params': {'from': a, 'to': b, 'windowS': ws},
|
|
'verdict': f['verdict'], 'rules': f['rules'], 'unjudged': f['unjudged'], 'manufacturer': f['manufacturer'], 'keys': f['keys'], 'chains': f['chains'],
|
|
'records': {'inRange': in_range, 'byState': states, 'uncounted': f['uncounted']},
|
|
'availability': {'windows': n, 'claimed': covered_c, 'witnessed': covered_w, 'claimedPerMille': (1000 * covered_c) // n, 'witnessedPerMille': (1000 * covered_w) // n},
|
|
'gaps': {'count': len(runs), 'longestWindows': max([r[1] for r in runs] or [0]), 'list': runs},
|
|
'equivocations': {'count': len(f['equivocations']), 'list': [[e['seq'], e['a'], e['b']] for e in f['equivocations']]},
|
|
'retirements': {'count': len(f['retirements']), 'list': [[t['deviceId'], t['at']] for t in f['retirements']]},
|
|
'age': {'claimedS': (b - first['at']) // 1000 if first is not None and first['at'] < b else None,
|
|
'witnessedS': (b - low_hi) // 1000 if low_hi is not None and low_hi < b else None},
|
|
'evidence': {'windows': [[i, by_clock[i], by_witness[i]] for i in range(n) if by_clock[i] is not None or by_witness[i] is not None]},
|
|
}
|
|
|
|
|
|
def judge_reputation(inp):
|
|
if not members(inp, ('facts', 'params', 'claimed')):
|
|
return verdict('device-reputation', {'integrity': ['reputation.form']}, ['the input is not exactly { facts, params, claimed }'], {})
|
|
why = facts_problem(inp['facts']) or params_problem(inp['params'])
|
|
if why:
|
|
return verdict('device-reputation', {'integrity': ['reputation.form']}, [why], {})
|
|
try:
|
|
same = canonical(inp['claimed']) == canonical(reputation_of(inp['facts'], inp['params']))
|
|
except (ValueError, RecursionError):
|
|
same = False # a claim this file cannot write as JavaScript writes it (an exponent form, Infinity) is not the formula's text
|
|
if not same:
|
|
return verdict('device-reputation', {'integrity': ['reputation.figures']}, ['the stated reputation is not what the formula gives for these facts and parameters'], {})
|
|
return verdict('device-reputation', {}, [], {'integrity': 'the stated reputation is what the formula ' + REP_FORMULA + ' gives'})
|
|
|
|
|
|
JUDGES = {'envelope': judge_envelope, 'dataset-record': judge_record, 'device-reputation': judge_reputation}
|
|
CAPABILITIES = {'v': 1, 'kind': 'aere-conformance-capabilities', 'name': 'independent integrity checker (Python, standard library)', 'artifacts': sorted(JUDGES),
|
|
'rules': ['dataset.commitment', 'dataset.field', 'dataset.form', 'dataset.inclusion', 'envelope.form', 'envelope.integrity', 'reputation.figures', 'reputation.form'], 'online': False, 'batch': True}
|
|
|
|
|
|
def one(path):
|
|
try:
|
|
with open(path, encoding='utf-8') as f:
|
|
j = json.load(f)
|
|
if not isinstance(j, dict) or j.get('artifact') not in JUDGES or not isinstance(j.get('input'), dict):
|
|
return {'error': 'not { artifact, input } with an artifact this implementation judges'}
|
|
return JUDGES[j['artifact']](j['input'])
|
|
except Exception as e: # a crash is reported as such: the runner scores it BROKEN, never as a refusal
|
|
return {'error': str(e)[:300]}
|
|
|
|
|
|
def main(a):
|
|
if '--capabilities' in a:
|
|
print(json.dumps(CAPABILITIES))
|
|
return 0
|
|
if '--batch' in a:
|
|
with open(a[a.index('--batch') + 1], encoding='utf-8') as f:
|
|
for e in json.load(f):
|
|
print(json.dumps(one(e['file'])))
|
|
return 0
|
|
files = [x for x in a if not x.startswith('--')]
|
|
if not files:
|
|
print('usage: implementare-integritate.py --capabilities | <vector.json> | --batch <list.json>', file=sys.stderr)
|
|
return 3
|
|
r = one(files[0])
|
|
print(json.dumps(r, indent=1))
|
|
return 3 if 'error' in r else {'VALID': 0, 'INVALID': 1}.get(r['verdict'], 2)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1:]))
|