Repository navigation
Expand file tree
/
Copy pathformat.py
More file actions
67 lines (51 loc) · 2.02 KB
/
Copy pathformat.py
File metadata and controls
67 lines (51 loc) · 2.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"""Strict receipt parsing; mathematical inputs are bounded canonical strings."""
import hashlib
import json
import re
from fractions import Fraction
from pathlib import Path
def need(ok, message):
if not ok:
raise ValueError(message)
def keys(obj, names):
need(type(obj) is dict and set(obj) == set(names.split()), 'exact object keys')
def canonical(obj):
return json.dumps(obj, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('ascii')
def digest(obj):
return hashlib.sha256(canonical(obj)).hexdigest()
def equal(actual, expected, label):
need(canonical(actual) == canonical(expected), label)
def rational(raw):
need(type(raw) is str and len(raw) <= 100 and
re.fullmatch(r'-?(0|[1-9][0-9]*)(/[1-9][0-9]*)?', raw) is not None,
'canonical rational string')
value = Fraction(raw)
need(str(value) == raw, 'reduced rational required')
return value
def decimal(ctx, raw):
need(type(raw) is str and len(raw) <= 100 and
re.fullmatch(r'-?(0|[1-9][0-9]*)(\.[0-9]+)?(e-?[0-9]+)?', raw) is not None,
'finite decimal string')
if 'e' in raw:
need(abs(int(raw.split('e')[1])) <= 200, 'bounded decimal exponent')
value = ctx.mpf(raw)
need(ctx.isfinite(value), 'finite decimal')
return value
def load(path):
need(Path(path).stat().st_size <= 250_000, 'receipt size limit')
with Path(path).open('rb') as stream:
raw = stream.read(250_001)
need(len(raw) <= 250_000, 'receipt size limit')
def pairs(items):
result = {}
for key, value in items:
need(key not in result, 'duplicate JSON key')
result[key] = value
return result
def forbidden(value):
raise ValueError('floating/nonfinite JSON token forbidden')
def integer(value):
need(len(value) <= 50, 'bounded integer')
return int(value)
return json.loads(raw.decode('ascii'), object_pairs_hook=pairs,
parse_float=forbidden, parse_constant=forbidden, parse_int=integer)