diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc955c..10bd406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- We are now using SHA256 instead of djb2hash ### Added - URL parameter to points.json to allow returning only the JSON for a single team by its team id (e.g., points.json?id=abc123). @@ -13,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - add_script_stream convenience function allows easy script addition to puzzle - Autobuild Docker images to test buildability - Extract and use X-Forwarded-For headers in mothd logging +- Mothballs can now specify `X-Answer-Pattern` header fields, which allow `*` + at the beginning, end, or both, of an answer. This is `X-` because we + are hoping to change how this works in the future. ### Fixed - Handle cases where non-legacy puzzles don't have an `author` attribute - Handle YAML-formatted file and script lists as expected diff --git a/devel/devel-server.py b/devel/devel-server.py index e1e6cae..1c91c00 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -77,12 +77,12 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): "status": "success", "data": { "short": "", - "description": "Provided answer was not in list of answers" + "description": "%r was not in list of answers" % self.req.get("answer") }, } if self.req.get("answer") in puzzle.answers: - ret["data"]["description"] = "Answer is correct" + ret["data"]["description"] = "Answer %r is correct" % self.req.get("answer") self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() diff --git a/devel/moth.py b/devel/moth.py index 420b272..0d9c411 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): @@ -130,6 +127,7 @@ class Puzzle: self.summary = None self.authors = [] self.answers = [] + self.xAnchors = {"begin", "end"} self.scripts = [] self.pattern = None self.hint = None @@ -215,6 +213,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): @@ -448,12 +456,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 @@