the store is UTF-8, so the tool is too: no locale can crash a wake
This commit is contained in:
parent
4b72c6ee7b
commit
d618a3a626
2 changed files with 50 additions and 4 deletions
24
memo
24
memo
|
|
@ -23,6 +23,14 @@ import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
# The store is UTF-8 by construction, so the tool reads and prints UTF-8
|
||||||
|
# no matter what the machine's locale says. Without this, one arrow in a
|
||||||
|
# memory makes wake crash on a latin-1 locale.
|
||||||
|
for _s in (sys.stdout, sys.stderr):
|
||||||
|
if hasattr(_s, "reconfigure"): # Python 3.7+
|
||||||
|
_s.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def pretty(p):
|
def pretty(p):
|
||||||
"""A path as the user would type it: keep symlinks, fold $HOME to ~."""
|
"""A path as the user would type it: keep symlinks, fold $HOME to ~."""
|
||||||
p, home = os.path.abspath(p), os.path.expanduser("~")
|
p, home = os.path.abspath(p), os.path.expanduser("~")
|
||||||
|
|
@ -162,7 +170,7 @@ def overrides(d):
|
||||||
p = os.path.join(d, "config")
|
p = os.path.join(d, "config")
|
||||||
if not os.path.exists(p):
|
if not os.path.exists(p):
|
||||||
return out
|
return out
|
||||||
for n, line in enumerate(open(p), 1):
|
for n, line in enumerate(open(p, encoding="utf-8"), 1):
|
||||||
line = line.split("#")[0].strip()
|
line = line.split("#")[0].strip()
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
|
|
@ -191,7 +199,7 @@ def write_config(d, over):
|
||||||
for k, (default, what) in KNOBS.items():
|
for k, (default, what) in KNOBS.items():
|
||||||
out.append("%-2s%-12s = %-6d # %s"
|
out.append("%-2s%-12s = %-6d # %s"
|
||||||
% ("" if k in over else "# ", k, over.get(k, default), what))
|
% ("" if k in over else "# ", k, over.get(k, default), what))
|
||||||
with open(os.path.join(d, "config"), "w") as f:
|
with open(os.path.join(d, "config"), "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(out) + "\n")
|
f.write("\n".join(out) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -274,7 +282,11 @@ def tree_get(d, lo, hi):
|
||||||
rec = f.read(TREE_REC)
|
rec = f.read(TREE_REC)
|
||||||
except FileNotFoundError: # not built yet; any other failure is real
|
except FileNotFoundError: # not built yet; any other failure is real
|
||||||
return None
|
return None
|
||||||
return rec.decode().rstrip() or None
|
try:
|
||||||
|
return rec.decode().rstrip() or None
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
die("The summary of #%d-%d is corrupt. Run: %s forget %d-%d"
|
||||||
|
% (lo, hi - 1, ME, lo, hi - 1))
|
||||||
|
|
||||||
|
|
||||||
def pad(text, rec):
|
def pad(text, rec):
|
||||||
|
|
@ -745,7 +757,11 @@ def cmd_import(d, args):
|
||||||
For bootstrapping an identity from older records. Used once."""
|
For bootstrapping an identity from older records. Used once."""
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
die("usage: %s import <file> # lines of 'YYYY-MM-DD <text>'" % ME)
|
die("usage: %s import <file> # lines of 'YYYY-MM-DD <text>'" % ME)
|
||||||
src = open(args[0]).readlines()
|
try:
|
||||||
|
src = open(args[0], encoding="utf-8").readlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
die("%s is not UTF-8 text. Convert it, then import again."
|
||||||
|
% pretty(args[0]))
|
||||||
last = log_get(d, log_len(d) - 1)[1] if log_len(d) else "0000-00-00"
|
last = log_get(d, log_len(d) - 1)[1] if log_len(d) else "0000-00-00"
|
||||||
out = []
|
out = []
|
||||||
for i, line in enumerate(src, 1):
|
for i, line in enumerate(src, 1):
|
||||||
|
|
|
||||||
30
test.py
30
test.py
|
|
@ -537,6 +537,36 @@ check(r.returncode == 1 and "forget 0-15" in r.stderr,
|
||||||
"a blank half summary must point at forget: " + r.stdout + r.stderr)
|
"a blank half summary must point at forget: " + r.stdout + r.stderr)
|
||||||
shutil.rmtree(d4)
|
shutil.rmtree(d4)
|
||||||
|
|
||||||
|
# the store is UTF-8 whatever the locale says: without pinning the streams,
|
||||||
|
# one arrow in a memory made wake crash forever on a latin-1 machine
|
||||||
|
d5 = tempfile.mkdtemp(prefix="optmem-utf8-")
|
||||||
|
run("note", "an arrow \u2192 survives any locale", store=d5)
|
||||||
|
r_ = subprocess.run(memo + ["wake"], capture_output=True,
|
||||||
|
env=dict(os.environ, MEMORY_DIR=d5,
|
||||||
|
PYTHONIOENCODING="latin-1"))
|
||||||
|
check(r_.returncode == 0 and "\u2192".encode() in r_.stdout,
|
||||||
|
"wake must print UTF-8 on a non-UTF-8 locale: "
|
||||||
|
+ repr(r_.stdout + r_.stderr))
|
||||||
|
|
||||||
|
# an import file that is not UTF-8 is refused -- neither a traceback nor,
|
||||||
|
# worse, silently mis-decoded into mojibake and stored forever
|
||||||
|
with open(os.path.join(d5, "latin1.txt"), "wb") as f:
|
||||||
|
f.write(b"2027-01-01 caf\xe9 in latin-1\n")
|
||||||
|
r = run("import", os.path.join(d5, "latin1.txt"), store=d5)
|
||||||
|
check(r.returncode == 1 and "not UTF-8" in r.stderr,
|
||||||
|
"a non-UTF-8 import file must be refused: " + r.stdout + r.stderr)
|
||||||
|
|
||||||
|
# a summary record holding invalid UTF-8 is the blank-record dead end in
|
||||||
|
# another coat: it must name forget, not print a traceback
|
||||||
|
run("note", "utf8 probe second memory", store=d5)
|
||||||
|
run("nap", "0-1", "both utf8 probes", store=d5)
|
||||||
|
with open(os.path.join(d5, "TREE", "2"), "r+b") as f:
|
||||||
|
f.write(b"\xff\xfe corrupt bytes")
|
||||||
|
r = run("zoom", "0-3", store=d5)
|
||||||
|
check(r.returncode == 1 and "forget 0-1" in r.stderr,
|
||||||
|
"a corrupt summary must point at forget: " + r.stdout + r.stderr)
|
||||||
|
shutil.rmtree(d5)
|
||||||
|
|
||||||
# re-running init on a lived-in store must not touch one byte of it: the
|
# re-running init on a lived-in store must not touch one byte of it: the
|
||||||
# whole setup is idempotent, so it is safe on every provision. `init` only
|
# whole setup is idempotent, so it is safe on every provision. `init` only
|
||||||
# ever makedirs(exist_ok), opens LOG.txt for APPEND, and writes `config`
|
# ever makedirs(exist_ok), opens LOG.txt for APPEND, and writes `config`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue