From 04136ef264fec469e90712313921936d2ba95e03 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 13:31:38 -0600 Subject: [PATCH] Support patterned answers --- devel/moth.py | 21 +++++--- example-puzzles/example/4/puzzle.moth | 2 + theme/puzzle.html | 1 + theme/puzzle.js | 78 +++++++++++++++++++-------- 4 files changed, 74 insertions(+), 28 deletions(-) diff --git a/devel/moth.py b/devel/moth.py index 0c8af68..58d17f7 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -22,11 +22,8 @@ messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' LOGGER = logging.getLogger(__name__) -def djb2hash(str): - h = 5381 - for c in str.encode("utf-8"): - h = ((h * 33) + c) & 0xffffffff - return h +def sha256hash(str): + return hashlib.sha256(str.encode("utf-8")).hexdigest() @contextlib.contextmanager def pushd(newdir): @@ -129,6 +126,7 @@ class Puzzle: self.summary = None self.authors = [] self.answers = [] + self.xAnchors = {"begin", "end"} self.scripts = [] self.pattern = None self.hint = None @@ -214,6 +212,16 @@ class Puzzle: if not isinstance(val, str): raise ValueError("Answers must be strings, got %s, instead" % (type(val),)) self.answers.append(val) + elif key == 'x-answer-pattern': + a = val.strip("*") + assert "*" not in a, "Patterns may only have * at the beginning and end" + assert "?" not in a, "Patterns do not currently support ? characters" + assert "[" not in a, "Patterns do not currently support character ranges" + self.answers.append(a) + if val.startswith("*"): + self.xAnchors.discard("begin") + if val.endswith("*"): + self.xAnchors.discard("end") elif key == "answers": for answer in val: if not isinstance(answer, str): @@ -447,12 +455,13 @@ class Puzzle: 'success': self.success, 'solution': self.solution, 'ksas': self.ksas, + 'xAnchors': list(self.xAnchors), } def hashes(self): "Return a list of answer hashes" - return [djb2hash(a) for a in self.answers] + return [sha256hash(a) for a in self.answers] class Category: diff --git a/example-puzzles/example/4/puzzle.moth b/example-puzzles/example/4/puzzle.moth index b021ecc..c06a653 100644 --- a/example-puzzles/example/4/puzzle.moth +++ b/example-puzzles/example/4/puzzle.moth @@ -1,6 +1,8 @@ Summary: Answer patterns Answer: command.com Answer: COMMAND.COM +X-Answer-Pattern: PINBALL.* +X-Answer-Pattern: pinball.* Author: neale Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3} diff --git a/theme/puzzle.html b/theme/puzzle.html index 88de8dc..a7be166 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -22,6 +22,7 @@
+ Team ID:
Answer:
diff --git a/theme/puzzle.js b/theme/puzzle.js index f0bbba3..23a14df 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -52,16 +52,46 @@ function devel_addin(obj, e) { } - // The routine used to hash answers in compiled puzzle packages -function djb2hash(buf) { - let h = 5381 - for (let c of (new TextEncoder).encode(buf)) { // Encode as UTF-8 and read in each byte - // JavaScript converts everything to a signed 32-bit integer when you do bitwise operations. - // So we have to do "unsigned right shift" by zero to get it back to unsigned. - h = (((h * 33) + c) & 0xffffffff) >>> 0 +async function sha256Hash(message) { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string + return hashHex; +} + +// Is the provided answer possibly correct? +async function possiblyCorrect(answer) { + for (let correctHash of window.puzzle.hashes) { + // CPU time is cheap. Especially if it's not our server's time. + // So we'll just try absolutely everything and see what happens. + // We're counting on hash collisions being extremely rare with the algorithm we use. + // And honestly, this pales in comparison to the amount of CPU being eaten by + // something like the github 404 page. + for (let len = 0; len <= answer.length; len += 1) { + if (window.puzzle.xAnchors.includes("end") && (len != answer.length)) { + console.log(` Skipping unanchored end (len=${len})`) + continue + } + console.log(" What about length", len) + for (let pos = 0; pos < answer.length - len + 1; pos += 1) { + if (window.puzzle.xAnchors.includes("begin") && (pos > 0)) { + console.log(` Skipping unanchored begin (pos=${pos})`) + continue + } + let sub = answer.substring(pos, pos+len) + let digest = await sha256Hash(sub) + + console.log(" Could it be", sub, digest) + if (digest == correctHash) { + console.log(" YESSS") + return sub + } + } + } } - return h + return false } @@ -80,6 +110,14 @@ function toast(message, timeout=5000) { // When the user submits an answer function submit(e) { e.preventDefault() + let data = new FormData(e.target) + + // Kludge for patterned answers + let xAnswer = data.get("xAnswer") + if (xAnswer) { + data.set("answer", xAnswer) + } + window.data = data fetch("answer", { method: "POST", body: new FormData(e.target), @@ -180,21 +218,17 @@ function answerCheck(e) { return } - let possiblyCorrect = false - let answerHash = djb2hash(answer) - for (let correctHash of window.puzzle.hashes) { - if (correctHash == answerHash) { - possiblyCorrect = true + possiblyCorrect(answer) + .then (correct => { + if (correct) { + document.querySelector("[name=xAnswer").value = correct + ok.textContent = "⭕" + ok.title = "Possibly correct" + } else { + ok.textContent = "❌" + ok.title = "Definitely not correct" } - } - - if (possiblyCorrect) { - ok.textContent = "❓" - ok.title = "Possibly correct" - } else { - ok.textContent = "⛔" - ok.title = "Definitely not correct" - } + }) } function init() {