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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
#!/usr/bin/env nix-shell
#! nix-shell -i "python3 -I" -p python3
from contextlib import contextmanager
from pathlib import Path
import re
alphabet = "0123456789abcdfghijklmnpqrsvwxyz"
inverted_alphabet = { c: i for i, c in enumerate(alphabet) }
def decode(s: str) -> bytes:
# only support sha256 hashes for now
assert len(s) == 52
out = [ 0 for _ in range(32) ]
# TODO: Do better than a list of byte-sized ints
for n, c in enumerate(reversed(s)):
digit = inverted_alphabet[c]
i, j = divmod(5 * n, 8)
out[i] = out[i] | (digit << j) & 0xff
rem = digit >> (8 - j)
if rem == 0:
continue
elif i < 31:
out[i+1] = rem
else:
raise ValueError(f"Invalid nix32 hash: '{s}'")
return bytes(out)
def toSRI(s: str) -> str:
from base64 import b64encode
digest = decode(s)
assert(len(digest) == 32)
return f"sha256-{b64encode(digest).decode()}"
RE = f"[{alphabet}]" "{52}";
# Ohno I used evil, irregular backrefs ^^'
_sha256_re = re.compile(f'sha256 = (?P<quote>["\'])(?P<nix32>{RE})(?P=quote);')
def defToSRI(s: str) -> str:
return _sha256_re.sub(
lambda m: f'hash = "{toSRI(m["nix32"])}";',
s,
)
@contextmanager
def atomicFileUpdate(target: Path):
'''Atomically replace the contents of a file.
Guarantees that no temporary files are left behind, and `target` is either
left untouched, or overwritten with new content if no exception was raised.
Yields a pair `(original, new)` of open files.
`original` is the pre-existing file at `target`, open for reading;
`new` is an empty, temporary file in the same filder, open for writing.
Upon exiting the context, the files are closed; if no exception was
raised, `new` (atomically) replaces the `target`, otherwise it is deleted.
'''
# That's mostly copied from noto-emoji.py, should DRY it out
from tempfile import mkstemp
fd, _p = mkstemp(
dir = target.parent,
prefix = target.name,
)
tmpPath = Path(_p)
try:
with target.open() as original:
with tmpPath.open('w') as new:
yield (original, new)
tmpPath.replace(target)
except Exception:
tmpPath.unlink(missing_ok = True)
raise
def fileToSRI(p: Path):
with atomicFileUpdate(p) as (og, new):
for line in og:
new.write(defToSRI(line))
if __name__ == "__main__":
from sys import argv, stderr
for arg in argv[1:]:
p = Path(arg)
if not p.is_file():
print(f"Argument '{arg}' is not a regular file's path", file=stderr)
else:
print(f"Processing '{arg}'")
fileToSRI(p)
|