commit 0dd22bd7bc35f530c5b3b757e6db0b2077716091 Author: Kristóf Tóth Date: Thu Feb 10 00:43:17 2022 +0100 Implement OpenSSH style identicon calculation diff --git a/identicon/__init__.py b/identicon/__init__.py new file mode 100644 index 0000000..84d3068 --- /dev/null +++ b/identicon/__init__.py @@ -0,0 +1 @@ +from .identicon import Identicon diff --git a/identicon/identicon.py b/identicon/identicon.py new file mode 100644 index 0000000..bc785e0 --- /dev/null +++ b/identicon/identicon.py @@ -0,0 +1,89 @@ +from dataclasses import dataclass +from copy import deepcopy + + +@dataclass +class Coordinate: + x: int + y: int + + def __add__(self, other): + return self.__class__( + self.x+other.x, + self.y+other.y + ) + + +class Identicon: + WIDTH = 17 + HEIGHT = 9 + SYMBOLS = ' .o+=*BOX@%&#/^' + START_SYMBOL = 'S' + END_SYMBOL = 'E' + BISHOP_START = Coordinate(8, 4) + + MOVES = { + 0: Coordinate(-1, -1), # 00 ↖ + 1: Coordinate( 1, -1), # 01 ↗ + 2: Coordinate(-1, 1), # 10 ↙ + 3: Coordinate( 1, 1), # 11 ↘ + } + + def __init__(self, data): + self._data = data + self._grid = [[0] * self.WIDTH for _ in range(self.HEIGHT)] + self._bishop_position = deepcopy(self.BISHOP_START) + self._end_position = None + + def calculate(self): + for command in self._get_commands(): + self._execute(command) + self._end_position = self._bishop_position + + def _get_commands(self): + commands = [] + for octet in self._data: + commands.extend([ + octet & 0b11, # 00 00 00 [00] + (octet >> 2) & 0b11, # 00 00 [00] 00 + (octet >> 4) & 0b11, # 00 [00] 00 00 + (octet >> 6) & 0b11, # [00] 00 00 00 + ]) + return commands + + def _execute(self, command): + move = self.MOVES[command] + self._bishop_position = self._ensure_within_grid_borders(self._bishop_position + move) + self._increment_current_position() + + def _ensure_within_grid_borders(self, coordinate: Coordinate): + coordinate.x = max(0, min(self.WIDTH-1, coordinate.x)) + coordinate.y = max(0, min(self.HEIGHT-1, coordinate.y)) + return coordinate + + def _increment_current_position(self): + self._grid[self._bishop_position.y][self._bishop_position.x] += 1 + + def __str__(self): + icon = self._header+'\n' + for y, row in enumerate(self._grid): + icon += '|' + for x, cell in enumerate(row): + icon += self._determine_symbol(Coordinate(x, y), cell) + icon += '|\n' + icon += self._header + + return icon + + @property + def _header(self): + return f'+{self.WIDTH*"-"}+' + + def _determine_symbol(self, coordinate, cell): + symbol = self.SYMBOLS[min(cell, len(self.SYMBOLS)-1)] + if coordinate == self.BISHOP_START: + symbol = self.START_SYMBOL + elif coordinate == self._end_position: + symbol = self.END_SYMBOL + + return symbol diff --git a/identicon/test_identicon.py b/identicon/test_identicon.py new file mode 100644 index 0000000..7165348 --- /dev/null +++ b/identicon/test_identicon.py @@ -0,0 +1,89 @@ +from base64 import b64decode +from dataclasses import dataclass +from textwrap import dedent + +import pytest + +from .identicon import Identicon + + +@dataclass +class ReferenceIcon: + fingerprint: bytes + icon: str + + +REFERENCE_ICONS = [ + ReferenceIcon( + bytearray.fromhex(''.join('b7:a3:e2:ce:09:06:ad:39:c8:1d:ad:b5:95:48:8f:99'.split(':'))), + dedent('''\ + +-----------------+ + | | + | | + | . | + | .o * . | + | ...E +S . | + |...++ o . . | + |..+oo. o | + | o o.. . . | + | o=.. | + +-----------------+''' + ) + ), + ReferenceIcon( + b64decode('sX58LC41tVlsctG1+H5PrkbMDfG374yghEg96KlnFZA=='), + dedent('''\ + +-----------------+ + | . .o| + | E + o| + | .. o = | + | o.o o B o| + | o S. . X +o| + | o +.+o.o =..| + | +.o.=.+. .+| + | .o .+ + ..=+| + | .o .o .oo=| + +-----------------+''' + ) + ), + ReferenceIcon( + bytearray.fromhex(''.join('9b:cf:42:fd:25:ff:ce:83:e9:e0:f1:d4:10:c3:ae:a8'.split(':'))), + dedent('''\ + +-----------------+ + | | + | . | + | + | + | . o | + | S. o | + | .oo o + | + | .o. = =o. | + | oo. *o.o | + | E .o..o o=| + +-----------------+''' + ) + ), + ReferenceIcon( + b64decode('5/WozN6loc0G8DxxrJhV8+aG/6Dvz1/gTBVipWU9nb0='), + dedent('''\ + +-----------------+ + | o.==| + | + =o=| + | o + +| + | . o o oE | + | SB.+.+o | + | oo*..*o. | + | .ooo* .| + | o *.=.o.| + | .*.*ooo*| + +-----------------+''' + ) + ) +] + + +@pytest.mark.parametrize("reference", REFERENCE_ICONS) +def test_identicon(reference): + icon = Identicon(reference.fingerprint) + icon.calculate() + + assert str(icon) == reference.icon