Source code for crypto_enigma.machine

#!/usr/bin/env python
# encoding: utf8

# Copyright (C) 2016 by Roy Levien.
# This file is part of crypto-enigma, an Enigma Machine simulator.
# released under the BSD-3 License (see LICENSE.txt).

""" 
This module supports all of the functionality of an Enigma machine using an `EnigmaConfig` class and
several utility functions, which support examination of its state
(including the details of the cyphers used for encoding messages), stepping during operation, and encoding messages.

"""


# The mark_func argument should take a single character and return a string representing that character, "marked" to
# highlight it in a the string representing a mapping. Ideally, the number of added printed characters should be even.
from __future__ import (absolute_import, print_function, division, unicode_literals)

from unicodedata import combining

from .components import *
from .exceptions import *


[docs]class EnigmaConfig(object): """An Enigma machine configuration. A class representing the state of an Enigma machine, providing functionality for * :ref:`generating a machine configuration <config_creation>` from a conventional specification, * :ref:`examining the state <config_state>` of a configuration, * simulating the :ref:`operation <config_operation>` of a machine by stepping between states, and * :ref:`encoding messages <config_encoding>`. """ def __init__(self, components, positions, rings): """The core properties of an `EnigmaConfig` embody a low level specification of an Enigma configuration. The conventional historical specification of an Enigma machine (as used in `~EnigmaConfig.config_enigma`) includes redundant elements, and conceals properties that are directly relevant to the operation of the machine and the encoding it performs — notably the actual rotational positions of the components. A complete "low level" formal characterization of the state of an Enigma machine consists of three elements: lists of `~EnigmaConfig.components`, their `~EnigmaConfig.positions`, and the settings of their `~EnigmaConfig.rings`. Here these lists are in *processing order* — as opposed to the physical order used in conventional specifications — and the have positions and ring settings generalized and "padded" for consistency to include the plugboard and reflector. Note that though it is not likely to be useful, these elements can be used to instantiate an `EnigmaConfig`: >>> cfg_conv = EnigmaConfig.config_enigma("B-I-II-III", "ABC", "XO.YM.QL", "01.02.03") >>> cfg_intl = EnigmaConfig(cfg_conv.components, cfg_conv.positions, cfg_conv.rings) >>> cfg_conv == cfg_intl True They may also be useful in extending the functionality provided here, for example in constructing additional representations of configurations beyond those provided in `config_string`: >>> [b'{} {}'.format(c, p) for c, p in zip(cfg_intl.components, cfg_intl.positions)[1:]] ['III 1', 'II 1', 'I 1', 'B 1'] """ # TBD - Assertions plugboard <<< assert all(name in rotors for name in components[1:-1]) assert components[-1] in reflectors assert len(rings) == len(positions) == len(components) assert all(1 <= rng <= 26 for rng in rings) assert all(1 <= pos <= 26 for pos in positions) #assert all(chr_A0(pos) in LETTERS for pos in positions) self._components = tuple(components) self._positions = tuple(positions) self._rings = tuple(rings) self._stages = tuple(range(0, len(self._components)))
[docs] @staticmethod @require_unicode('rotor_names', 'window_letters', 'plugs', 'rings') def config_enigma(rotor_names, window_letters, plugs, rings): """Create an `EnigmaConfig` from strings specifying its state. A (safe public, "smart") constructor that does validation and takes a conventional specification as input, in the form of four strings. Following convention, the elements of these specifications are in physical machine order as the operator sees them, which is the reverse of the order in which they are encountered in processing. Validation is permissive, allowing for ahistorical collections and numbers of rotors (including reflectors at the rotor stage, and trivial degenerate machines; e.g., `config_enigma("-", "A", "", "01")`, and any number of (non-contradictory) plugboard wirings (including none). Args: rotor_names (unicode): The |Walzenlage|_: The conventional letter or Roman numeral designations (its `~.components.Component.name`) of the rotors, including reflector, separated by dashes (e.g. `'b-β-V-I-II'`). (See `components`.) window_letters (unicode): The |Walzenstellung|_ (or, incorrectly, the *Grundstellung*): The letters visible at the windows (e.g. `'MQR'`). (See `windows`.) plugs (unicode): The |Steckerverbindungen|_: The plugboard specification (its `~.components.Component.name`) as a conventional string of letter pairs separated by periods, (e.g., `'AU.ZM.ZL.RQ'`). (See `components`.) rings (unicode): The |Ringstellung|_: The location of the letter ring on each rotor (specifcially, the number on the rotor under ring letter **A**), separated by periods (e.g. `'22.11.16'`). (See `rings`.) Returns: EnigmaConfig: A new Enigma machine configuration created from the specification arguments. Raises: EnigmaValueError: Raised when arguments do not pass validation. Example: .. _testsetup_properties: >>> cfg = EnigmaConfig.config_enigma("c-β-V-III-II", "LQVI", "AM.EU.ZL", "16.01.21.11") # doctest: +SKIP .. testsetup:: properties cfg = EnigmaConfig.config_enigma("c-β-V-III-II".decode(), u"LQVI", u"AM.EU.ZL", u"16.01.21.11") """ rotor_names_list = rotor_names.split('-') ring_numbers = [int(x) for x in rings.split('.')] # TBD - Validation for plugboard <<< # A bunch of checks to provide better feedback than (and befor lower-level) assertions for name in rotor_names_list[1:]: if name not in rotors: raise EnigmaValueError('Bad configuration - Invalid rotor name, {0}'.format(name)) if rotor_names_list[0] not in reflectors: raise EnigmaValueError('Bad configuration: invalid reflector name, {0}'.format(rotor_names_list[0])) if not (len(ring_numbers) == len(window_letters) == len(rotor_names_list)-1): raise EnigmaValueError('Bad configuration: number rotors ({0}), rings ({1}), and window letters ({2}) must match'.format( len(rotor_names_list)-1, len(ring_numbers), len(window_letters) )) for rng in ring_numbers: if not (1 <= rng <= 26): raise EnigmaValueError('Bad configuration: invalid ring position number, {0}'.format(rng)) for wind in window_letters: if wind not in LETTERS: raise EnigmaValueError('Bad configuration: window letter, {0}'.format(wind)) comps = (rotor_names + '-' + plugs).split('-')[::-1] winds = [num_A0(c) for c in 'A' + window_letters + 'A'][::-1] rngs = [int(x) for x in ('01.' + rings + '.01').split('.')][::-1] return EnigmaConfig(comps, [((w - r + 1) % 26) + 1 for w, r in zip(winds, rngs)], rngs)
[docs] @staticmethod @require_unicode('string') def config_enigma_from_string(string): """Create an `EnigmaConfig` from a single string specifying its state. Args: string (unicode): The elements of a conventional specification (as supplied to `config_enigma`) joined by spaces into a single string. Returns: EnigmaConfig: A new Enigma machine configuration created from the specification argument. Raises: EnigmaValueError: Raised when argument does not pass validation. Example: This is just a shortcut for invoking `config_enigma` using a sigle string: >>> cfg_str = "c-β-V-III-II LQVI AM.EU.ZL 16.01.21.11" # doctest: +SKIP >>> EnigmaConfig.config_enigma_from_string(cfg_str) == EnigmaConfig.config_enigma(*cfg_str.split(' ')) # doctest: +SKIP True Note that the `string` argument corresponds to the string representation of an `EnigmaConfig` >>> print(EnigmaConfig.config_enigma_from_string(cfg_str)) # doctest: +SKIP c-β-V-III-II LQVI AM.EU.ZL 16.01.21.11 so that this method is useful for instantiation of an `EnigmaConfig` from such strings (e.g., in files): >>> unicode(EnigmaConfig.config_enigma_from_string(cfg_str)) == unicode(cfg_str) # doctest: +SKIP True """ split_string = filter(lambda s: s != '', string.split(' ')) if len(split_string) != 4: raise EnigmaValueError('Bad string - {0} should have 4 elements'.format(split_string)) rotor_names, window_letters, plugs, rings = split_string return EnigmaConfig.config_enigma(rotor_names, window_letters, plugs, rings)
def _window_letter(self, st): return chr_A0((self._positions[st] + self._rings[st] - 2) % 26) @property def components(self): """The identities of the components in the Enigma machine. For rotors (including the reflector) these correspond to the the `~EnigmaConfig.config_enigma.rotor_names` supplied to `config_enigma`, while for the plugboard this is just the `~EnigmaConfig.config_enigma.plugs` argument. Returns: tuple: The `~.components.Component.name` of each `~.components.Component` in an `EnigmaConfig`, in processing order. Example: Using `cfg` as defined :ref:`above <testsetup_properties>`: .. doctest:: properties >>> cfg.components # doctest: +SKIP (u'AM.EU.ZL', u'II', u'III', u'V', u'\u03b2', u'c') """ return self._components @property def positions(self): """The rotational positions of the components in the Enigma machine. For rotors, this is to the number on the rotor (not letter ring) that is at the "window position", and is computed from the `~EnigmaConfig.config_enigma.window_letters` and `~EnigmaConfig.config_enigma.rings` parameters for `config_enigma`. This (alone) determines permutations applied to components' `~.components.Component.wiring` to produce the :ref:`mapping <config_state_mappings>` for a configuration and thus the :ref:`message encoding <config_encoding_message>` it performs. Note that this is the only property of an enigma machine that changes when it is stepped (see `step`), and the changes in the letters visible at the `windows` are the (only) visible manifestation of this change. Returns: tuple: The generalized rotational position of each of the components in an `EnigmaConfig`, in machine processing order. Example: Using `cfg` as defined :ref:`above <testsetup_properties>`: .. doctest:: properties >>> cfg.positions (1, 25, 2, 17, 23, 1) Note that for the plugboard and reflector, the position will always be **1** since the former cannot rotate, and the latter does not (neither will be different in a new configuration generated by `step`):: cfg.positions[0] == 1 cfg.positions[-1] == 1 """ return self._positions @property def rings(self): """The ring settings in the Enigma machine. For rotors, these are the `~EnigmaConfig.config_enigma.rings` parameter for `config_enigma`. Returns: tuple: The generalized location of ring letter **A** on the rotor for each of the `components` in an `EnigmaConfig`, in machine processing order. Example: Using `cfg` as defined :ref:`above <testsetup_properties>`: .. doctest:: properties >>> cfg.rings (1, 11, 21, 1, 16, 1) Note that for the plugboard and reflector, this will always be **1** since the former lacks a ring, and for latter ring position is irrelevant (the letter ring is not visible, and has no effect on when turnovers occur):: cfg.rings[0] == 1 cfg.rings[-1] == 1 """ return self._rings
[docs] def windows(self): """The letters at the windows of an Enigma machine. This is the (only) visible manifestation of configuration changes during :ref:`operation <config_operation>`. Returns: unicode: The letters at the windows in an `EnigmaConfig`, in physical, conventional order. Example: Using `cfg` as defined :ref:`above <testsetup_properties>`: .. doctest:: properties >>> cfg.windows() u'LQVI' """ # return ''.join(list(reversed([self._window_letter(st) for st in self._stages][1:-1]))) return ''.join([self._window_letter(st) for st in self._stages][1:-1][::-1])
# return ''.join([self._window_letter(st) for st in self._stages][-2:0:-1])
[docs] def step(self): """Step the Enigma machine to a new machine configuration. Step the Enigma machine by rotating the rightmost (first) rotor one position, and other rotors as determined by the `positions` of rotors in the machine, based on the positions of their `.components.Component.turnovers`. In the physical machine, a step occurs in response to each operator keypress, prior to processing that key's letter (see `enigma_encoding`). Stepping leaves the `components` and `rings` of a configuration unchanged, changing only `positions`, which is manifest in changes of the letters visible at the `windows`: Returns: EnigmaConfig: A new Enigma configuration. Examples: Using the initial configuration >>> cfg = EnigmaConfig.config_enigma("c-γ-V-I-II", "LXZO", "UX.MO.KZ.AY.EF.PL", "03.17.04.01") # doctest: +SKIP .. testsetup:: step cfg = EnigmaConfig.config_enigma("c-γ-V-I-II".decode(), u"LXZO", u"UX.MO.KZ.AY.EF.PL", u"03.17.04.01") the consequences of the stepping process can be observed by examining the `windows` of each stepped configuration: .. doctest:: step >>> print(cfg.windows()) LXZO >>> print(cfg.step().windows()) LXZP >>> print(cfg.step().step().windows()) LXZQ >>> print(cfg.step().step().step().windows()) LXZR >>> print(cfg.step().step().step().step().windows()) LXZS >>> print(cfg.step().step().step().step().step().windows()) LXZT This, and the fact that only positions (and thus window letters) change as the result of stepping, can be visualized in more detail using `print_operation`: .. doctest:: step >>> cfg.print_operation(steps=5, format='config') c-γ-V-I-II LXZO UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZP UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZQ UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZR UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZS UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZT UX.MO.KZ.AY.EF.PL 03.17.04.01 """ def is_turn(stg): return self._window_letter(stg) in component(self.components[stg]).turnovers def pos_inc(stg): if stg == 0: return 0 elif stg > 3: return 0 elif stg == 1: return 1 elif stg == 2 and is_turn(2): return 1 elif is_turn(stg - 1): return 1 else: return 0 stepped_positions = [((self._positions[stage] + pos_inc(stage) - 1) % 26) + 1 for stage in self._stages] return EnigmaConfig(self._components, stepped_positions, self._rings)
[docs] def stepped_configs(self, steps=None): """Generate a series of stepped Enigma machine configurations. Args: steps (int, optional): An optional limit on the number of steps to take in generating configurations. Yields: EnigmaConfig: The `EnigmaConfig` resulting from applying `step` to the previous one. Examples: This allows the examples above to be rewritten as .. doctest:: step >>> for c in cfg.stepped_configs(5): ... print(c.windows()) LXZO LXZP LXZQ LXZR LXZS LXZT >>> for c in cfg.stepped_configs(5): ... print(c) c-γ-V-I-II LXZO UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZP UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZQ UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZR UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZS UX.MO.KZ.AY.EF.PL 03.17.04.01 c-γ-V-I-II LXZT UX.MO.KZ.AY.EF.PL 03.17.04.01 """ cur_config = self cur_step = 0 while steps is None or cur_step <= steps: if cur_step > 0: cur_config = cur_config.step() yield cur_config cur_step += 1
# REV - Caching here isn't really needed
[docs] @cached({}) def stage_mapping_list(self): """The list of mappings for each stage of an Enigma machine. The list of |mappings| for each stage of in an `EnigmaConfig`: The encoding performed by the `~.components.Component` *at that point* in the progress through the machine. These are arranged in processing order, beginning with the encoding performed by the plugboard, followed by the forward (see `~.component.Direction`) encoding performed by each rotor (see `~.components.Component.mapping`), then the reflector, followed by the reverse encodings by each rotor, and finally by the plugboard again. Returns: list of Mapping: A list of |mappings| preformed by the corresponding stage of the `EnigmaConfig` (see `~.components.Component.mapping`). Examples: This can be used to obtain lists of mappings for analysis: .. testsetup:: config_mappings cfg = EnigmaConfig.config_enigma("b-γ-VII-V-IV".decode(), u"VBOA", u"NZ.AY.FG.UX.MO.PL", u"05.16.11.21") .. doctest:: config_mappings >>> cfg = EnigmaConfig.config_enigma("b-γ-VII-V-IV", "VBOA", "NZ.AY.FG.UX.MO.PL", "05.16.11.21") # doctest: +SKIP >>> cfg.stage_mapping_list() # doctest: +ELLIPSIS [u'YBCDEGFHIJKPOZMLQRSTXVWUAN', u'DUSKOCLBRFHZNAEXWGQVYMIPJT', ...] or more clearly .. doctest:: config_mappings >>> for m in cfg.stage_mapping_list(): ... print(m) YBCDEGFHIJKPOZMLQRSTXVWUAN DUSKOCLBRFHZNAEXWGQVYMIPJT CEPUQLOZJDHTWSIFMKBAYGRVXN PCITOWJZDSYERHBNXVUFQLAMGK UZYIGEPSMOBXTJWDNAQVKCRHLF ENKQAUYWJICOPBLMDXZVFTHRGS RKVPFZEXDNUYIQJGSWHMATOLCB WOBILTYNCGZVXPEAUMJDSRFQKH TSAJBPVKOIRFQZGCEWNLDXMYUH NHFAOJRKWYDGVMEXSICZBTQPUL YBCDEGFHIJKPOZMLQRSTXVWUAN This list is a core part of the "internal" view of machine stage prduced by `config_string` (compare the second through the next-to-last lines with the above): .. doctest:: config_mappings >>> print(cfg.config_string(format='internal')) ABCDEFGHIJKLMNOPQRSTUVWXYZ P YBCDEGFHIJKPOZMLQRSTXVWUAN NZ.AY.FG.UX.MO.PL 1 DUSKOCLBRFHZNAEXWGQVYMIPJT A 07 IV 2 CEPUQLOZJDHTWSIFMKBAYGRVXN O 05 V 3 PCITOWJZDSYERHBNXVUFQLAMGK B 13 VII 4 UZYIGEPSMOBXTJWDNAQVKCRHLF V 18 γ R ENKQAUYWJICOPBLMDXZVFTHRGS b 4 RKVPFZEXDNUYIQJGSWHMATOLCB γ 3 WOBILTYNCGZVXPEAUMJDSRFQKH VII 2 TSAJBPVKOIRFQZGCEWNLDXMYUH V 1 NHFAOJRKWYDGVMEXSICZBTQPUL IV P YBCDEGFHIJKPOZMLQRSTXVWUAN NZ.AY.FG.UX.MO.PL XZJVGSEMTCYUHWQROPFILDNAKB Note that, because plugboard mapping is established by paired exchanges of letters it is always the case that: .. doctest:: config_mappings >>> cfg.stage_mapping_list()[0] == cfg.stage_mapping_list()[-1] True """ return ([component(comp).mapping(pos, Direction.FWD) for (comp, pos) in zip(self._components, self._positions)] + [component(comp).mapping(pos, Direction.REV) for (comp, pos) in zip(self._components, self._positions)][:-1][::-1])
# REV - Caching here isn't really needed
[docs] @cached({}) def enigma_mapping_list(self): """The list of progressive mappings of an Enigma machine at each stage. The list of |mappings| an `EnigmaConfig` has performed by each stage: The encoding performed by the `EnigmaConfig` as a whole *up to that point* in the progress through the machine. These are arranged in processing order, beginning with the encoding performed by the plugboard, followed by the forward (see `~.component.Direction`) encoding performed up to each rotor (see `~.components.Component.mapping`), then the reflector, followed by the reverse encodings up to each rotor, and finally by the plugboard again. Returns: list of Mapping: A list of |mappings| preformed by the `EnigmaConfig` up to the corresponding stage of the `EnigmaConfig` (see `~.components.Component.mapping`). Examples: This can be used to obtain lists of mappings for analysis: .. doctest:: config_mappings >>> cfg = EnigmaConfig.config_enigma("b-γ-VII-V-IV", "VBOA", "NZ.AY.FG.UX.MO.PL", "05.16.11.21") # doctest: +SKIP >>> cfg.enigma_mapping_list() # doctest: +ELLIPSIS [u'YBCDEGFHIJKPOZMLQRSTXVWUAN', u'JUSKOLCBRFHXETNZWGQVPMIYDA', ...] or more clearly .. doctest:: config_mappings >>> for m in cfg.enigma_mapping_list(): ... print(m) YBCDEGFHIJKPOZMLQRSTXVWUAN JUSKOLCBRFHXETNZWGQVPMIYDA DYBHITPEKLZVQASNROMGFWJXUC TGCZDFNOYEKLXPUHVBRJWASMQI VPYFIEJWLGBXHDKSCZAORUQTNM TMGUJAIHOYNRWQCZKSELXFDVBP MIEANRDXJCQWOSVBUHFYLZPTKG XCLWPMIQGBUFEJROSNTKVHADZY YAFMCQOEVSDPBIWGNZLRXKTJHU UNJVFSEOTCAXHWQRMLGIPDZYKB XZJVGSEMTCYUHWQROPFILDNAKB Since these may be thought of as cumulative encodings by the machine, the final element of the list will be the mapping used by the machine for encoding: .. doctest:: config_mappings >>> cfg.enigma_mapping() == cfg.enigma_mapping_list()[-1] True """ return list(accumulate(self.stage_mapping_list(), lambda s, m: Mapping(m.encode_string(s))))
[docs] def enigma_mapping(self): """The mapping used by an Enigma machine for encoding. The |mapping| used by an `EnigmaConfig` to encode a letter entered at the keyboard. Returns: Mapping: The |mapping| used by the `EnigmaConfig` encode a single character. Examples: This is the final element in the corresponding `enigma_mapping_list`: .. doctest:: config_mappings >>> cfg.enigma_mapping() u'XZJVGSEMTCYUHWQROPFILDNAKB' """ return self.enigma_mapping_list()[-1]
[docs] @require_unicode('message') def enigma_encoding(self, message): """Encode a message using the machine configuration. Encode a string, interpreted as a message (see `make_message`), using the (starting) machine configuration, by stepping (see `step`) the configuration prior to processing each character of the message. This produces a new configuration (with new `positions` only) for encoding each character, which serves as the "starting" configuration for subsequent processing of the message. Args: message (unicode): A message to encode. Returns: unicode: The machine-encoded message. Examples: Given machine configuration >>> cfg = EnigmaConfig.config_enigma("b-γ-V-VIII-II", "LFAP", "UX.MO.KZ.AY.EF.PL", "03.17.04.11") # doctest: +SKIP .. testsetup:: enigma_encoding cfg = EnigmaConfig.config_enigma("b-γ-V-VIII-II".decode("UTF-8"), u"LFAP", u"UX.MO.KZ.AY.EF.PL", u"03.17.04.11") the message `'KRIEG'` is encoded to `'GOWNW'`: .. doctest:: enigma_encoding >>> cfg.enigma_encoding('KRIEG') u'GOWNW' The details of this encoding and its relationship to stepping from one configuration to another are illustrated using `print_operation`: .. doctest:: enigma_encoding >>> cfg.print_operation("KRIEG", format='windows', show_encoding=True, show_step=True) 0000 LFAP 0001 LFAQ K > G 0002 LFAR R > O 0003 LFAS I > W 0004 LFAT E > N 0005 LFAU G > W Note that because of the way the Enigma machine is designed, it is always the case (provided that `msg` is all uppercase letters) that:: cfg.enigma_encoding(cfg.enigma_encoding(msg)) == msg """ message = EnigmaConfig.make_message(message) return ''.join([step_config.enigma_mapping().encode_char(letter) for (letter, step_config) in zip(message, self.step().stepped_configs())])
# ASK - Equvalent to Haskell read (if this is like show, or is _repr_ show; eval(repr(obj)) )? <<< def __unicode__(self): return "{0} {1} {2} {3}".format('-'.join(self._components[1:][::-1]), self.windows(), self._components[0], '.'.join(['{:02d}'.format(r) for r in self._rings[1:-1]][::-1])) def __str__(self): return unicode(self).encode('utf-8') def __repr__(self): return '{0} ({1})'.format(object.__repr__(self), unicode(self)).encode('utf-8') def __eq__(self, cfg): return all([self.components == cfg.components, self.positions == cfg.positions, self.rings == cfg.rings]) @staticmethod def _marked_mapping(mapping, i, mark_func=None): def marked_char(c): if mark_func is None: # REV - Why does this end up here if mark_func is supplied with non-Unicode? return c + '\u0332\u0305' #return '[' + c + ']' # REV - Would be nice, but has limited support: http://www.fileformat.info/info/unicode/char/20de/ # return c + u'\u20DE' else: return mark_func(c) pads = ' ' * ((sum(not combining(c) for c in marked_char('A')) - 1) // 2) return mapping[:i] + marked_char(mapping[i]) + mapping[i + 1:] if 0 <= i < len(mapping) else pads + mapping + pads # TBD - Add assertions to all that they get Unicode <<< @staticmethod def _locate_letter(mapping, letter, string): # locate the index of the encoding with mapping of letter, in string # REV - Use of out of bounds index (-1) as failure return value; callers must check bounds (see above) <<< return string.index(mapping.encode_char(letter)) if letter in string else -1 # Ensures a single uppercase character ("those that are valid Enigma input") or space, defaulting to a space @staticmethod def _make_enigma_char(letter): return filter(lambda l: l in LETTERS + ' ', (letter + ' ').upper())[0] def _config_string(self, letter, mark_func=None): cfg_mapping = self.enigma_mapping() return '{0} {1} {2} {3}'.format(letter + ' >' if letter in LETTERS else ' ', EnigmaConfig._marked_mapping(cfg_mapping, EnigmaConfig._locate_letter(cfg_mapping, letter, cfg_mapping), mark_func), self.windows(), ' '.join(['{:02d}'.format(p) for p in self.positions[1:-1]][::-1])) def _config_string_internal(self, letter, mark_func=None): cfg_mapping = self.enigma_mapping() cfg_mapping_list = self.enigma_mapping_list() stg_mapping_list = self.stage_mapping_list() def reflect_info(stg_info): return stg_info + stg_info[::-1][1:] def pad_info(stg_info, pad): return [pad] + stg_info + ([pad] * len(self.positions)) # REV - Better way that avoids recalcs of cfg_mapping and cfg_mapping_list? letter_locations = [EnigmaConfig._locate_letter(m, l, s) for (m, l, s) in zip([Mapping(LETTERS)] + cfg_mapping_list + [cfg_mapping], [letter] * (len(self._stages) * 2 + 1), [Mapping(LETTERS)] + stg_mapping_list + [cfg_mapping])] stg_labels = reflect_info(['P'] + list(self._stages)[1:-1] + ['R']) stg_mappings = [EnigmaConfig._marked_mapping(m, i, mark_func) for (m, i) in zip(stg_mapping_list, letter_locations[1:-1])] stg_windows = pad_info(list(self.windows())[::-1], ' ') stg_positions = pad_info(['{:02d}'.format(p) for p in self.positions][1:-1], ' ') stg_coponents = reflect_info(self.components) return ("{0} {1}\n".format(letter + ' >' if letter in LETTERS else ' ', EnigmaConfig._marked_mapping(LETTERS, letter_locations[1], mark_func)) + ''.join([' {0} {1} {2} {3} {4}\n'.format(stg_lbl, stg_map, stg_wind, stg_pos, stg_comp) for (stg_lbl, stg_map, stg_wind, stg_pos, stg_comp) in zip(stg_labels, stg_mappings, stg_windows, stg_positions, stg_coponents)]) + "{0} {1}".format(self.enigma_mapping().encode_char(letter) + ' <' if letter in LETTERS else ' ', EnigmaConfig._marked_mapping(cfg_mapping, letter_locations[-1], mark_func)) )
[docs] @staticmethod @require_unicode('string') def make_message(string): """Convert a string to valid Enigma machine input. Replace any symbols for which there are standard *Kriegsmarine* substitutions, remove any remaining non-letter characters, and convert to uppercase. This function is applied automatically to `message` arguments for functions defined here (`enigma_encoding`). Args: string (unicode): A string to convert to valid Enigma machine input. Returns: unicode: A string of valid Enigma machine input characters. """ subs = [(' ', ''), ('.', 'X'), (',', 'Y'), ("'", 'J'), ('>', 'J'), ('<', 'J'), ('!', 'X'), ('?', 'UD'), ('-', 'YY'), (':', 'XX'), ('(', 'KK'), (')', 'KK'), ('1', 'YQ'), ('2', 'YW'), ('3', 'YE'), ('4', 'YR'), ('5', 'YT'), ('6', 'YZ'), ('7', 'YU'), ('8', 'YI'), ('9', 'YO'), ('0', 'YP')] msg = filter(lambda c: c in LETTERS, reduce(lambda s, (o, n): s.replace(o, n), subs, string.upper())) assert all(letter in LETTERS for letter in msg) return msg
# TBD - Additional formats, e.g., components listed, etc. _FMTS_INTERNAL = ['internal', 'detailed', 'schematic'] _FMTS_SINGLE = ['single', 'summary'] _FMTS_WINDOWS = ['windows', 'winds'] _FMTS_CONFIG = ['config', 'configuration', 'spec', 'specification'] _FMTS_ENCODING = ['encoding'] _FMTS_DEBUG = ['debug'] # TBD - Add encoding note to config and windows (e.g with P > K) <<< # TBD - Add components format that lists the components and their attributes <<<
[docs] @require_unicode('letter') def config_string(self, letter='', format='single', show_encoding=False, mark_func=None): """A string representing a schematic of an Enigma machine's state. A string representing the stat of an `EnigmaConfig` in a selected format (see examples), optionally indicating how specified character is encoded by the configuration. Args: letter (unicode, optional): A character to indicate the encoding of by the `EnigmaConfig`. format (str, optional): A string specifying the format used to display the `EnigmaConfig`. show_encoding (bool, optional): Whether to indicate the encoding for formats that do not include it by default. mark_func (function, optional): A `function` that highlights its argument by taking a single character as an argument and returning a string with additional characters added to (usually surrounding) that charater. Used in cases where default method of highlighting the encoded-to character (see `~.cypher.Mapping`) does not display correctly or clearly. Returns: str: A string schematically representing an `EnigmaConfig` Examples: A variety of formats are available for representing the state of the Enigma machine: .. testsetup:: enigma_config_string cfg = EnigmaConfig.config_enigma("b-γ-V-VIII-II".decode("UTF-8"), u"LFAQ", u"UX.MO.KZ.AY.EF.PL", u"03.17.04.11") .. doctest:: enigma_config_string >>> cfg = EnigmaConfig.config_enigma("b-γ-V-VIII-II", "LFAQ", "UX.MO.KZ.AY.EF.PL",u"03.17.04.11") # doctest: +SKIP >>> print(cfg.config_string(format='single')) CMAWFEKLNVGHBIUYTXZQOJDRPS LFAQ 10 16 24 07 >>> print(cfg.config_string(format='internal')) ABCDEFGHIJKLMNOPQRSTUVWXYZ P YBCDFEGHIJZPONMLQRSTXVWUAK UX.MO.KZ.AY.EF.PL 1 LORVFBQNGWKATHJSZPIYUDXEMC Q 07 II 2 BJYINTKWOARFEMVSGCUDPHZQLX A 24 VIII 3 ILHXUBZQPNVGKMCRTEJFADOYSW F 16 V 4 YDSKZPTNCHGQOMXAUWJFBRELVI L 10 γ R ENKQAUYWJICOPBLMDXZVFTHRGS b 4 PUIBWTKJZSDXNHMFLVCGQYROAE γ 3 UFOVRTLCASMBNJWIHPYQEKZDXG V 2 JARTMLQVDBGYNEIUXKPFSOHZCW VIII 1 LFZVXEINSOKAYHBRGCPMUDJWTQ II P YBCDFEGHIJZPONMLQRSTXVWUAK UX.MO.KZ.AY.EF.PL CMAWFEKLNVGHBIUYTXZQOJDRPS >>> print(cfg.config_string(format='windows')) LFAQ >>> print(cfg.config_string(format='config')) b-γ-V-VIII-II LFAQ UX.MO.KZ.AY.EF.PL 03.17.04.11 >>> print(cfg.config_string(format='encoding', letter='K')) K > G Use `format='single'` or omit the argument to display a summary of the Enigma machine configuration as its `~.cypher.Mapping` (see `enigma_mapping`), the letters at the `windows`, and the `positions` of the rotors. If a valid message character is provided as a value for `letter`, that is indicated as input and the letter it is encoded to is highlighted. For example, .. doctest:: enigma_config_string >>> print(cfg.config_string(letter='K')) K > CMAWFEKLNVG̲̅HBIUYTXZQOJDRPS LFAQ 10 16 24 07 shows the process of encoding of the letter **K** to **G**. The default method of highlighting the encoded-to character (see `~.cypher.Mapping`) may not display correctly on all systems, so the `marc_func` argument can be used to define a simpler marking that does: .. doctest:: enigma_config_string >>> print(cfg.config_string(letter='K', mark_func=lambda c: '[' + c + ']')) K > CMAWFEKLNV[G]HBIUYTXZQOJDRPS LFAQ 10 16 24 07 >>> print(cfg.config_string(letter='K', mark_func=lambda c: '(' + c + ')')) K > CMAWFEKLNV(G)HBIUYTXZQOJDRPS LFAQ 10 16 24 07 Use `format='internal'` to display a summary of the Enigma machine configuration as a detailed schematic of each processing stage of the `EnigmaConfig` (proceeding from top to bottom), in which * each line indicates the `~.cypher.Mapping` preformed by the component at that stage (see `stage_mapping_list`); * each line begins with an indication of the stage (rotor number, **P** for plugboard, or **R** for reflector) at that stage, and ends with the specification (see `~.components.Component.name`) of the component at that stage; * rotors additionally indicate their window letter, and position; and * if a valid `letter` is provided, it is indicated as input and its encoding at each stage is marked; The schematic is followed by the mapping for the machine as a whole (as for the `'single'` format), and preceded by a (trivial, no-op) keyboard "mapping" for reference. For example, .. doctest:: enigma_config_string >>> print(cfg.config_string(letter='K', format='internal', mark_func=lambda c: '(' + c + ')')) K > ABCDEFGHIJ(K)LMNOPQRSTUVWXYZ P YBCDFEGHIJ(Z)PONMLQRSTXVWUAK UX.MO.KZ.AY.EF.PL 1 LORVFBQNGWKATHJSZPIYUDXEM(C) Q 07 II 2 BJ(Y)INTKWOARFEMVSGCUDPHZQLX A 24 VIII 3 ILHXUBZQPNVGKMCRTEJFADOY(S)W F 16 V 4 YDSKZPTNCHGQOMXAUW(J)FBRELVI L 10 γ R ENKQAUYWJ(I)COPBLMDXZVFTHRGS b 4 PUIBWTKJ(Z)SDXNHMFLVCGQYROAE γ 3 UFOVRTLCASMBNJWIHPYQEKZDX(G) V 2 JARTML(Q)VDBGYNEIUXKPFSOHZCW VIII 1 LFZVXEINSOKAYHBR(G)CPMUDJWTQ II P YBCDFE(G)HIJZPONMLQRSTXVWUAK UX.MO.KZ.AY.EF.PL G < CMAWFEKLNV(G)HBIUYTXZQOJDRPS shows the process of encoding of the letter **K** to **G**: * **K** is entered at the keyboard, which is then * encoded by the plugboard (**P**), which includes **KZ** in its specification (see Name), to **Z**, which is then * encoded by the first rotor (**1**), a **II** rotor in the 06 position (and **Q** at the window), to **C**, which is then * encoded by the second rotor (**2**), a **VIII** rotor in the 24 position (and **A** at the window), to **Y**, which is then * encoded by the third rotor (**3**), a **V** rotor in the 16 position (and **F** at the window), to **S**, which is then * encoded by the fourth rotor (**4**), a **γ** rotor in the 10 position (and **L** at the window), to **J**, which is then * encoded by the reflector rotor (**U**), a **b** reflector, to **I**, which reverses the signal sending it back through the rotors, where it is then * encoded in reverse by the fourth rotor (**4**), to **Z**, which is then * encoded in reverse by the third rotor (**3**), to **G**, which is then * encoded in reverse by the second rotor (**2**), to **Q**, which is then * encoded in reverse by the first rotor (**1**), to **G**, which is then * left unchanged by the plugboard (**P**), and finally * displayed as **G**. Note that (as follows from Mapping) the position of the marked letter at each stage is the alphabetic position of the marked letter at the previous stage. This can be represented schematically (with input arriving and output exiting on the left) as .. image:: _static/figs/configinternal.jpg :scale: 85 % :alt: Detailed schematic of encoding of K to G :align: center Use `format='windows'` to simply show the letters at the `windows` as the operator would see them. .. doctest:: enigma_config_string >>> print(cfg.config_string(format='windows')) LFAQ And use `format='config'` to simply show a conventional specification of an `EnigmaConfig` (as used for `config_enigma_from_string`): .. doctest:: enigma_config_string >>> print(cfg.config_string(format='config')) b-γ-V-VIII-II LFAQ UX.MO.KZ.AY.EF.PL 03.17.04.11 For both of the preceeding two formats, it is possible to also indicate the encoding of a character (not displayed by default) by setting `show_encoding` to `True`: .. doctest:: enigma_config_string >>> print(cfg.config_string(format='windows', letter='K')) LFAQ >>> print(cfg.config_string(format='windows', letter='K', show_encoding=True)) LFAQ K > G >>> print(cfg.config_string(format='config', letter='K')) b-γ-V-VIII-II LFAQ UX.MO.KZ.AY.EF.PL 03.17.04.11 >>> print(cfg.config_string(format='config', letter='K', show_encoding=True)) b-γ-V-VIII-II LFAQ UX.MO.KZ.AY.EF.PL 03.17.04.11 K > G Use `format='encoding'` to show this encoding alone: .. doctest:: enigma_config_string >>> print(cfg.config_string(format='encoding', letter='K')) K > G Note that though the examples above have been wrapped in `print` for clarity, these functions return strings: .. doctest:: enigma_config_string >>> cfg.config_string(format='windows', letter='K', show_encoding=True) u'LFAQ K > G' >>> cfg.config_string(format='internal').split('\\n') # doctest: +ELLIPSIS [u' ABCDEFGHIJKLMNOPQRSTUVWXYZ', u' P YBCDFEGHIJZPONMLQRSTXVWUAK UX.MO.KZ.AY.EF.PL', ...] """ # TBD - Check that mark_func returns Unicode, or that it 'succeeds'? - #13 letter = EnigmaConfig._make_enigma_char(letter) encoding_string = '' if letter in LETTERS and (show_encoding or format in EnigmaConfig._FMTS_ENCODING): encoding_string = ' {0} > {1}'.format(letter, self.enigma_mapping().encode_char(letter)) if format in EnigmaConfig._FMTS_INTERNAL: return self._config_string_internal(letter, mark_func) elif format in EnigmaConfig._FMTS_SINGLE: return self._config_string(letter, mark_func) elif format in EnigmaConfig._FMTS_WINDOWS: return self.windows() + encoding_string # !!! BUG - config and debug both give: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 2: ordinal not in range(128) <<< elif format in EnigmaConfig._FMTS_CONFIG: return str(self) + encoding_string elif format in EnigmaConfig._FMTS_DEBUG: return self.__repr__() + encoding_string elif format in EnigmaConfig._FMTS_ENCODING: return encoding_string[2:] else: raise EnigmaDisplayError('Bad argument - Unrecognized format, {0}'.format(format))
@require_unicode('letter') def config_string_internal(self, letter='', mark_func=None): """ .. deprecated:: 0.0.2 This function has been removed; use `config_string` instead. """ return self.config_string(letter, format='internal', mark_func=mark_func)
[docs] @require_unicode('message') def print_operation(self, message='', steps=None, overwrite=False, format='single', initial=True, delay=0.1, show_step=False, show_encoding=False, mark_func=None): """Show the operation of the Enigma machine as a series of configurations. Print out the operation of the Enigma machine as a series of `EnigmaConfig`, as it encodes a `message` and/or for a specified number of `steps`. Args: message (unicode): A message to encode. Characters that are not letters will be replaced with standard *Kriegsmarine* substitutions or be removed (see `make_message`). Each character will be used as a `letter` in the `config_string` specified by the `format`. steps (int, optional): A number of steps to run; if omitted when a `message` is provided, will default to the length of the message; otherwise defaults to 1 overwrite (bool, optional): Whether to overwrite the display of each step after a pause. (May result in garbled output on some systems.) format (str, optional): A string specifying the format used to display the `EnigmaConfig` at each step of message processing; see `config_string`. initial (bool, optional): Whether to show the initial starting step; the `EnigmaConfig` before encoding begins. delay (float, optional): The number of seconds to wait (see `time.sleep`) between the display of each processing step; defaults to 0.2. show_step (bool, optional): Whether to include the step number in the display. show_encoding (bool, optional): Whether to indicate the encoding of each character for formats that do not include it by default; see `config_string`. mark_func (function, optional): A `function` that highlights its argument by taking a single character as an argument and returning a string with additional characters added to (usually surrounding) that charater. Used in cases where default method of highlighting the encoded-to character (see `~.cypher.Mapping`) does not display correctly or clearly. Examples: (For details on differences among formats used for displaying each step, see the examples for `config_string`.) Show the operation of a machine for 10 steps, indicating step numbers: .. testsetup:: enigma_print_operation cfg = EnigmaConfig.config_enigma("B-I-III-I".decode("UTF-8"),u"EMO", u"UX.MO.AY", u"13.04.11") .. doctest:: enigma_print_operation >>> cfg = EnigmaConfig.config_enigma("B-I-III-I", "EMO", "UX.MO.AY", "13.04.11") # doctest: +SKIP >>> cfg.print_operation(format='single', steps=10, show_step=True) 0000 CNAUJVQSLEMIKBZRGPHXDFYTWO EMO 19 10 05 0001 UNXKGVERLYDIQBTWMHZOAFPCJS EMP 19 10 06 0002 QTYJZXUPKDIMLSWHAVNBGROFCE EMQ 19 10 07 0003 DMXAPTRWKYINBLUESGQFOZHCJV ENR 19 11 08 0004 IUSMHRPEAQTVDYWGJFCKBLOZNX ENS 19 11 09 0005 WMVXQRLSPYOGBTKIEFHNZCADJU ENT 19 11 10 0006 WKIQXNRSCVBOYFLUDGHZPJAEMT ENU 19 11 11 0007 RVPTWSLKYXHGNMQCOAFDZBEJIU ENV 19 11 12 0008 IYTKRVSMALDJHZWXUEGCQFOPBN ENW 19 11 13 0009 PSWGMODULZVIERFAXNBYHKCQTJ ENX 19 11 14 0010 IVOWZKHGARFSPUCMXJLYNBDQTE ENY 19 11 15 Show the operation of a machine as it encodes a message, with step numbers: .. doctest:: enigma_print_operation >>> cfg.print_operation(format='single', message='TESTING', show_step=True) 0000 CNAUJVQSLEMIKBZRGPHXDFYTWO EMO 19 10 05 0001 T > UNXKGVERLYDIQBTWMHZO̲̅AFPCJS EMP 19 10 06 0002 E > QTYJZ̲̅XUPKDIMLSWHAVNBGROFCE EMQ 19 10 07 0003 S > DMXAPTRWKYINBLUESGQ̲̅FOZHCJV ENR 19 11 08 0004 T > IUSMHRPEAQTVDYWGJFCK̲̅BLOZNX ENS 19 11 09 0005 I > WMVXQRLSP̲̅YOGBTKIEFHNZCADJU ENT 19 11 10 0006 N > WKIQXNRSCVBOYF̲̅LUDGHZPJAEMT ENU 19 11 11 0007 G > RVPTWSL̲̅KYXHGNMQCOAFDZBEJIU ENV 19 11 12 Show the same process, but just what the operator would see: .. doctest:: enigma_print_operation >>> cfg.print_operation(format='windows', message='TESTING', show_encoding=True, show_step=True) 0000 EMO 0001 EMP T > O 0002 EMQ E > Z 0003 ENR S > Q 0004 ENS T > K 0005 ENT I > P 0006 ENU N > F 0007 ENV G > L Show detailed internal version of the same process: .. doctest:: enigma_print_operation >>> cfg.print_operation(format='internal', message='TESTING', show_step=True) # doctest: +ELLIPSIS 0000 ABCDEFGHIJKLMNOPQRSTUVWXYZ P YBCDEFGHIJKLONMPQRSTXVWUAZ UX.MO.AY 1 HCZMRVJPKSUDTQOLWEXNYFAGIB O 05 I 2 KOMQEPVZNXRBDLJHFSUWYACTGI M 10 III 3 AXIQJZKRMSUNTOLYDHVBWEGPFC E 19 I R YRUHQSLDPXNGOKMIEBFZCWVJAT B 3 ATZQVYWRCEGOILNXDHJMKSUBPF I 2 VLWMEQYPZOANCIBFDKRXSGTJUH III 1 WZBLRVXAYGIPDTOHNEJMKFQSUC I P YBCDEFGHIJKLONMPQRSTXVWUAZ UX.MO.AY CNAUJVQSLEMIKBZRGPHXDFYTWO <BLANKLINE> 0001 T > ABCDEFGHIJKLMNOPQRST̲̅UVWXYZ P YBCDEFGHIJKLONMPQRST̲̅XVWUAZ UX.MO.AY 1 BYLQUIOJRTCSPNKVDWMX̲̅EZFHAG P 06 I 2 KOMQEPVZNXRBDLJHFSUWYACT̲̅GI M 10 III 3 AXIQJZKRMSUNTOLYDHVB̲̅WEGPFC E 19 I R YR̲̅UHQSLDPXNGOKMIEBFZCWVJAT B 3 ATZQVYWRCEGOILNXDH̲̅JMKSUBPF I 2 VLWMEQYP̲̅ZOANCIBFDKRXSGTJUH III 1 YAKQUWZXFHOCSNGM̲̅DILJEPRTBV I P YBCDEFGHIJKLO̲̅NMPQRSTXVWUAZ UX.MO.AY O < UNXKGVERLYDIQBTWMHZO̲̅AFPCJS <BLANKLINE> 0002 E > ABCDE̲̅FGHIJKLMNOPQRSTUVWXYZ P YBCDE̲̅FGHIJKLONMPQRSTXVWUAZ UX.MO.AY 1 XKPTH̲̅NIQSBROMJUCVLWDYEGZFA Q 07 I 2 KOMQEPVZ̲̅NXRBDLJHFSUWYACTGI M 10 III 3 AXIQJZKRMSUNTOLYDHVBWEGPFC̲̅ E 19 I R YRU̲̅HQSLDPXNGOKMIEBFZCWVJAT B ... """ def print_config_string(cfg_str): if step_num != 0 or initial: if show_step: if format=='internal': cfg_str = '{0:04d}\n{1}'.format(step_num, cfg_str) else: cfg_str = '{0:04d} {1}'.format(step_num, cfg_str) if overwrite and step_num <= steps: print_over(cfg_str, (0 if initial else 1) < step_num, delay) else: print(cfg_str) if not overwrite and format=='internal' and step_num < steps: print('') message = EnigmaConfig.make_message(message) if message != '': steps = len(message) if steps is None else min(steps, len(message)) elif steps is not None: message = ' ' * steps else: message = ' ' steps = 1 for (step_num, cfg, letter) in zip(range(0, steps+1), self.stepped_configs(), ' ' + message[:steps]): if not initial and step_num == 0: continue print_config_string(cfg.config_string(letter, format=format, show_encoding=show_encoding, mark_func=mark_func))
@require_unicode('message') def print_operation_internal(self, message, mark_func=None): """ .. deprecated:: 0.0.2 This function has been removed; use `print_operation` instead. """ self.print_operation(message, format='internal', mark_func=mark_func) @staticmethod @require_unicode('msg') def _postprocess(msg): return '\n'.join(chunk_of(' '.join(chunk_of(msg, 4)), 60))
[docs] @require_unicode('message') def print_encoding(self, message): """Show the conventionally formatted encoding of a message. Print out the encoding of a message by an (initial) `EnigmaConfig`, formatted into conventional blocks of four characters. Args: message (unicode): A message to encode. Characters that are not letters will be replaced with standard *Kriegsmarine* substitutions or be removed (see `make_message`). Examples: .. testsetup:: enigma_print_encoding cfg = EnigmaConfig.config_enigma("c-β-V-VI-VIII".decode("UTF-8"), u"CDTJ", u"AE.BF.CM.DQ.HU.JN.LX.PR.SZ.VW", u"05.16.05.12") .. doctest:: enigma_print_encoding >>> cfg = EnigmaConfig.config_enigma("c-β-V-VI-VIII", "CDTJ", "AE.BF.CM.DQ.HU.JN.LX.PR.SZ.VW", "05.16.05.12") # doctest: +SKIP >>> cfg.print_encoding("FOLGENDES IST SOFORT BEKANNTZUGEBEN") RBBF PMHP HGCZ XTDY GAHG UFXG EWKB LKGJ """ print(EnigmaConfig._postprocess(self.enigma_encoding(EnigmaConfig.make_message(message))))
# TBD - Tidy printing code so that the structures and names in config_string_internal and config_string match <<< # TBD - Check spacing of lines, esp at end in .._string and print_... methods <<< # ASK - Idiom for printing loops? # REV - Keep list(reverse( conversions as [::-1] throughout?