from __future__ import annotations
from typing import Optional, Union, Callable, List, Tuple, Set, Dict
import copy
import itertools
import random
import csv
import os
import math
import MzEnigma
[docs]class SpruchScoring(object):
"""Toolbox for scoring of a spruch. In the background, this object holds
dictionary with character n-grams. By default, the dictionary with the
longest n-gram is used.
:param language: language to be used for ngram scoring (required)
:param normalize: normalize ngram values with respect to maxScore
:param logarithmic: 10*math.log10(ngramDict[score]/minScore)
"""
def __init__(self, language : str, normalize : bool = True, logarithmic : bool = False):
self.resPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Resources')
assert language in ['english', 'german'], "{}: Language {} not supported, use 'english' or 'german'".format(self.__class__.__name__, language)
self.language = language
self.normalize = normalize
self.logarithmic = logarithmic
self.ngramDict : Dict[int, Dict[str, float]] = dict()
self.ngramLimits : Dict[int, Dict[str, float]] = dict()
self.ngramDict[1], self.ngramLimits[1] = self._loadStatistics('{}_monograms.txt'.format(language))
self.ngramDict[2], self.ngramLimits[2] = self._loadStatistics('{}_bigrams.txt'.format(language))
self.ngramDict[3], self.ngramLimits[3] = self._loadStatistics('{}_trigrams.txt'.format(language))
if self.ngramDict[3] is None:
del self.ngramDict[3]
del self.ngramLimits[3]
if self.ngramDict[2] is None:
del self.ngramDict[2]
del self.ngramLimits[2]
if self.ngramDict[1] is None:
del self.ngramDict[1]
del self.ngramLimits[1]
self.numberOfChars = max(self.ngramDict.keys())
self.saTemperature = 0.0
self.saThreshold = 0.01
[docs] def setSATemperature(self, spruch : str, numberOfChars : int = 0) -> float:
"""Enables the temperature for simulated annealing (used by `newNgramScore`)
:param spruch: spruch of typical length to be scored (required)
:param numberOfChars: n-gram dictionary to be used (default: longest n-grams)
"""
nSpruch = len(spruch)
if nSpruch == 0:
self.saTemperature = 0
elif nSpruch <= 30:
self.saTemperature = 400.0
elif nSpruch <= 50:
self.saTemperature = 400.0 - (400.0 - 315.0) * (nSpruch - 30) / (50 - 30)
elif nSpruch <= 75:
self.saTemperature = 315.0 - (315.0 - 240.0) * (nSpruch - 50) / (75 - 50)
elif nSpruch <= 100:
self.saTemperature = 240.0 - (240.0 - 220.0) * (nSpruch - 75) / (100 - 75)
elif nSpruch <= 150:
self.saTemperature = 220.0 - (220.0 - 200.0) * (nSpruch - 100) / (150 - 100)
else:
self.saTemperature = 200.0
if numberOfChars < 1:
assert 'numberOfChars' in vars(self), '{}.setSATemperature: no ngrams loaded'.format(self.__class__.__name__)
numberOfChars = self.numberOfChars
else:
assert numberOfChars in self.ngramDict, '{}.setSATemperature: ngramDict[{}] not found'.format(self.__class__.__name__, numberOfChars)
self.saTemperature /= 10^8
return self.saTemperature
[docs] def ngramScore(self, spruch : str, numberOfChars : int = 0, validChars : str = MzEnigma.stdAlphabet) -> float:
"""N-gram score of a spruch
:param spruch: spruch to be scored (required)
:param numberOfChars: n-gram dictionary to be used (default: longest n-grams)
:param validChars: alphabet of the spruch
:returns: N-gram score
"""
_score = 0
if numberOfChars < 1:
assert 'numberOfChars' in vars(self), '{}.ngramScore: no ngrams loaded'.format(self.__class__.__name__)
numberOfChars = self.numberOfChars
else:
assert numberOfChars in self.ngramDict, '{}.ngramScore: ngramDict[{}] not found'.format(self.__class__.__name__, numberOfChars)
nChars = len(list(self.ngramDict[numberOfChars].keys())[0])
for c in validChars:
assert c in self.ngramDict[numberOfChars], '{}.ngramScore: {} not in ngramDict[{}]'.format(self.__class__.__name__, c, numberOfChars)
for n in range(len(spruch) - nChars + 1):
ngram = spruch[n: n + nChars]
for c in ngram:
if c in validChars:
_score += self.ngramDict[numberOfChars][ngram]
break
return _score/(len(spruch) - nChars + 1)
[docs] def newNgramScore(self, spruch : str, currentScore : float, validChars : str = MzEnigma.stdAlphabet) -> float:
"""Compares the N-gram score of a spruch with the currentScore.
Uses simulated if enabled.
:param spruch: spruch to be scored (required)
:param currentScore: current score (required)
:param validChars: alphabet of the spruch
:returns: N-gram score
"""
score = self.ngramScore(spruch, validChars = validChars)
diffScore = score - currentScore
if diffScore >= 0:
return score
if self.saTemperature <= 0:
return currentScore
diffScore /= self.saTemperature
prob = math.exp(diffScore)
if prob <= random.uniform(self.saThreshold, 1.0):
score = currentScore
return score
[docs] @staticmethod
def indexOfCoincidence(spruch : str, validChars : str = MzEnigma.stdAlphabet) -> float:
"""index of coincidence of a spruch,
i.e. relative frequency of characters independent of the underlying language
:param spruch: spruch to be scored (required)
:param validChars: alphabet of the spruch
:returns: index of coincidence
"""
nAlphabet = len(validChars)
nSpruch = len(spruch)
iocList = nAlphabet * [0.0]
for c in spruch:
iocList[validChars.index(c)] += 1
ioc = 0
for n in range(nAlphabet):
ioc += iocList[n] * (iocList[n] - 1)
return ioc/(nSpruch * (nSpruch-1))
def _loadStatistics(self, statfile : str) -> Dict[str, float]:
ngramDict : Dict[str, Union[float, int]] = dict()
alphabet = set(MzEnigma.stdAlphabet) | set(MzEnigma.sgsAlphabet)
actPath = os.path.join(self.resPath, statfile)
if not os.path.exists(actPath):
return None, None
with open(actPath, newline='') as csvfile:
for ngram, nDetected in csv.reader(csvfile, delimiter=' '):
isInside = True
for c in ngram:
isInside &= c in alphabet
if isInside:
nDetected = float(nDetected)
assert nDetected > 0
ngramDict[ngram] = nDetected
maxValue = max(ngramDict.values())
minValue = min(ngramDict.values())
if self.logarithmic:
ngramDict.update({ngram : 10*math.log10(ngramDict[ngram]/minValue) for ngram in ngramDict.keys()})
elif self.normalize:
ngramDict.update({ngram : ngramDict[ngram]/maxValue for ngram in ngramDict.keys()})
else:
minValue /= maxValue
maxValue = 1
return ngramDict, (minValue, maxValue)
def __repr__(self) -> str:
return '{}\n language: {}\n normalized: {}\n logarithmic: {}'.format(self.__class__.__name__, self.language, self.normalize, self.logarithmic)
[docs]class Enigma(object):
"""Represents an Enigma engine with all available accessories.
If no umkehrwalzen are defined, the encryption is not symmetric any more !
:param model: model of the Enigma (required)
:param umkehrwalzen: available objects of type 'Umkehrwalze' (if any)
:param walzen: available objects of type 'Walze' (at least 1 required)
:param numberOfWalzen: number of objects of type 'Walze' to be inserted
:param steckerbrett: object of type 'Steckerbrett', if any
:param zusatzwalzen: available objects of type 'Zusatzwalze', if any
:param notify: notification function (e.g. print)
"""
def __init__(self,
model : str = '',
umkehrwalzen : List[MzEnigma.Umkehrwalze] = None,
walzen : List[MzEnigma.Walze] = None,
numberOfWalzen : int = 0,
steckerbrett : Optional[MzEnigma.Steckerbrett] = None,
zusatzwalzen : List[MzEnigma.Zusatzwalze] = None) -> None:
assert model, '{}: At least 1 character in model required'.format(self.__class__.__name__)
self._model = model
assert walzen, '{}: At least 1 walze required'.format(self.__class__.__name__)
alphabet = walzen[0].alphabet
assert all(v.alphabet == alphabet for v in walzen), '{}: Alphabets in all walzen must match'.format(self.__class__.__name__)
self._walzen = walzen
if umkehrwalzen:
assert all(v.alphabet == alphabet for v in umkehrwalzen), '{}: Alphabets in all umkehrwalzen must match'.format(self.__class__.__name__)
self._umkehrwalzen = umkehrwalzen
assert numberOfWalzen > 0 and numberOfWalzen <= len(walzen), '{}: 0 < numberOfWalzen < {} required'.format(self.__class__.__name__, len(walzen))
self._numberOfWalzen = numberOfWalzen
if steckerbrett:
assert steckerbrett.alphabet == alphabet, '{}: Alphabets in steckerbrett must match'.format(self.__class__.__name__)
self._steckerbrett = steckerbrett
if zusatzwalzen:
assert all(v.alphabet == alphabet for v in zusatzwalzen), '{}: Alphabets in all zusatzwalzen must match'.format(self.__class__.__name__)
self._zusatzwalzen = zusatzwalzen
[docs] @staticmethod
def validCribPositions(encodedSpruch : str = '', crib : str = '') -> List[int]:
"""This function delivers all positions where no crib letter encrypts as itself
:param encodedSpruch: message to be decoded
:param crib: substring to be examined
:returns: list of valid positions relative to the encodedSpruch
"""
assert len(encodedSpruch) > 0, 'validCribPositions: encodedSpruch is missing'
assert len(crib) > 0, 'validCribPositions: crib is missing'
assert len(crib) <= len(encodedSpruch), 'validCribPositions: len(encodedSpruch) == {} < len(crib) == {}'.format(len(encodedSpruch), len(crib))
validPosList = list()
for startPos in range(0, len(encodedSpruch) - len(crib) + 1):
# match = difflib.SequenceMatcher(None, encodedSpruch[startPos:], crib)
isMatched = False
for ps, pc in zip(encodedSpruch[startPos:], crib):
if ps == pc:
isMatched = True
break
if not isMatched:
validPosList.append(startPos)
return validPosList
@property
def alphabet(self) -> str:
"""
:getter: Returns the alphabet of the engine
:setter: None
"""
return self._walzen[0].alphabet
@property
def model(self) -> str:
"""
:getter: Returns the name of the engine
:setter: None
"""
return self._model
@property
def umkehrwalzen(self) -> List[MzEnigma.Umkehrwalze]:
"""
:getter: Returns a list of Umkehrwalzen
:setter: None
"""
return self._umkehrwalzen
@property
def walzen(self) -> List[MzEnigma.Walze]:
"""
:getter: Returns a list of Walzen
:setter: None
"""
return self._walzen
@property
def numberOfWalzen(self) -> int:
"""
:getter: Returns a number of Walzen built-in at the engine's operation
:setter: None
"""
return self._numberOfWalzen
@property
def steckerbrett(self) -> Optional[int]:
"""
:getter: Returns the steckerbrett if any
:setter: None
"""
return self._steckerbrett
@property
def zusatzwalzen(self) -> Optional[List[MzEnigma.Zusatzwalze]]:
"""
:getter: Returns a list of Zusatzwalzen if any
:setter: None
"""
return self._zusatzwalzen
def __eq__(self, enigma : Enigma) -> bool:
return self._model == enigma.model \
and self._umkehrwalzen == enigma.umkehrwalzen \
and self._walzen == enigma.walzen \
and self._numberOfWalzen == enigma.numberOfWalzen \
and self._steckerbrett == enigma.steckerbrett \
and self._zusatzwalzen == enigma.zusatzwalzen
def __repr__(self) -> str:
content = 'class: {}\nmodel: {}\nalphabet: {}\nUmkehrwalzen:'.format(self.__class__.__name__, self._model, self.alphabet)
for walze in self._umkehrwalzen:
for line in walze.__repr__().split('\n'):
if 'class:' not in line and 'alphabet:' not in line:
content += '\n ' + line
content += '\nnumber of Walzen: {}\nWalzen:'.format(self._numberOfWalzen)
for walze in self._walzen:
for line in walze.__repr__().split('\n'):
if 'class:' not in line and 'alphabet:' not in line:
content += '\n ' + line
if self._steckerbrett:
content += '\nSteckerbrett:'
for line in self._steckerbrett.__repr__().split('\n'):
if 'class:' not in line and 'alphabet:' not in line:
content += '\n ' + line
if self._zusatzwalzen:
content += '\nZusatzwalzen:'
for walze in self._zusatzwalzen:
for line in walze.__repr__().split('\n'):
if 'class:' not in line and 'alphabet:' not in line:
content += '\n ' + line
return content
[docs]class Tagesschluessel(object):
"""Represents the Tagesschluessel of an Enigma
:param enigma: model of the Enigma (required)
:param umkehrwalze: 'Umkehrwalze' (if undefined, randomly selected)
:param walzen: List of type 'Walze' (if undefined, randomly selecte(if undefined, randomly selected)
:param zusatzwalze: 'Zusatzwalze' (if undefined, randomly selected)
:param blank: replacement character for a blank
:param notify: notification function (e.g. print)
"""
def __init__(self,
enigma : Optional[Enigma] = None,
umkehrwalze : Optional[MzEnigma.Umkehrwalze] = None,
walzen : Optional[List[MzEnigma.Walze]] = None,
tagesWalzenStellungen : Optional[str] = None,
steckerbrett : Optional[MzEnigma.Steckerbrett] = None,
zusatzwalze : Optional[MzEnigma.Zusatzwalze] = None,
blank : str = '',
notify : Optional[Callable[[str], None]] = None) -> None:
self.notify = notify
assert enigma, '{}: Enigma required'.format(self.__class__.__name__)
self.enigma =enigma
self.blank = blank
alphabet = self.enigma.alphabet
if steckerbrett is None and enigma.steckerbrett:
self.steckerbrett = copy.deepcopy(enigma.steckerbrett)
self.steckerbrett.notify = self.notify
else:
self.steckerbrett = steckerbrett
lastWalze = self.steckerbrett
if not walzen:
walzen = random.sample(enigma.walzen, enigma.numberOfWalzen)
assert all(v.alphabet == alphabet for v in walzen), '{}: Alphabet in all walzen must match enigma'.format(self.__class__.__name__)
self.walzen = list()
for _walze in walzen:
walze = copy.deepcopy(_walze)
walze.notify = self.notify
self.walzen.append(walze)
if lastWalze:
lastWalze.nextComponent = walze
walze.prevComponent = lastWalze
lastWalze = walze
if self.steckerbrett:
self.firstComponent = self.steckerbrett
else:
self.firstComponent = self.walzen[0]
if tagesWalzenStellungen:
assert len(tagesWalzenStellungen) == len(walzen), '{}: Walzenstellung, number of characters must match the number of walzen'.format(self.__class__.__name__)
assert all(v in alphabet for v in tagesWalzenStellungen), '{}: Walzenstellung, all characters must be in alphabet'.format(self.__class__.__name__)
else:
tagesWalzenStellungen = ''.join(random.sample(alphabet, enigma.numberOfWalzen))
self.tagesWalzenStellungen = tagesWalzenStellungen
if zusatzwalze:
assert zusatzwalze.alphabet == alphabet, '{}: Alphabet in zusatzwalze must match enigma'.format(self.__class__.__name__)
elif enigma.zusatzwalzen is not None:
zusatzwalze = random.sample(enigma.zusatzwalzen, 1)[0]
if zusatzwalze:
self.zusatzwalze = copy.deepcopy(zusatzwalze)
self.zusatzwalze.notify = self.notify
lastWalze.nextComponent = self.zusatzwalze
self.zusatzwalze.prevComponent = lastWalze
lastWalze = self.zusatzwalze
else:
self.zusatzwalze = None
if umkehrwalze:
assert umkehrwalze.alphabet == alphabet, '{}: Alphabet in umkehrwalze must match enigma alphabet'.format(self.__class__.__name__)
elif len(enigma.umkehrwalzen) > 0:
umkehrwalze = random.sample(enigma.umkehrwalzen, 1)[0]
else:
self.umkehrwalze = None
if umkehrwalze:
self.umkehrwalze = copy.deepcopy(umkehrwalze)
self.umkehrwalze.notify = self.notify
lastWalze.nextComponent = self.umkehrwalze
self.umkehrwalze.nextComponent = lastWalze
[docs] @classmethod
def changeWalzen(cls,
currentTagesschluessel : Tagesschluessel,
umkehrwalze : Optional[MzEnigma.Umkehrwalze] = None,
walzen : List[MzEnigma.Walze] = None,
tagesWalzenStellungen : Optional[str] = None,
zusatzwalze : Optional[MzEnigma.Zusatzwalze] = None) -> None:
"""Changes certain elements of a Tagesschluessel
:param currentTagesschluessel: current Tagesschluessel (required)
:param tagesWalzenStellungen: changed Tageswalzenstellungen to be used (if undefined, randomly selected)
:param umkehrwalze: 'Umkehrwalze' (if undefined, used from currentTagesschluessel)
:param walzen: List of type 'Walze' (if undefined, used from currentTagesschluessel)
:param zusatzwalze: 'Zusatzwalze' (if undefined, used from currentTagesschluessel)
:returns: Tagesschluessel object
"""
if umkehrwalze is None:
umkehrwalze = currentTagesschluessel.umkehrwalze
if walzen is None:
walzen = currentTagesschluessel.walzen
if zusatzwalze is None:
zusatzwalze = currentTagesschluessel.zusatzwalze
return cls(
enigma = currentTagesschluessel.enigma,
umkehrwalze = umkehrwalze,
steckerbrett = currentTagesschluessel.steckerbrett,
walzen = walzen,
tagesWalzenStellungen = tagesWalzenStellungen,
zusatzwalze = zusatzwalze,
blank = currentTagesschluessel.blank,
notify = currentTagesschluessel.notify)
[docs] def findPatterns(self, substringList : List[str] = [''], encodedSpruchList : List[str] = []) -> Dict[str, Set[str]]:
"""Find a list of substringList for any tagesWalzenStellungen at any positions
:param substringList: list of substrings
:param encodedSpruchList: list of messages to be searched for substrings
:return: dict(substring, set of tagesWalzenStellungen)
"""
assert len(encodedSpruchList) > 0
maxSpruchlength = max(encodedSpruchList, key=len)
charSet = set()
for substring in substringList:
for c in substring:
assert c in self.alphabet, '{}: Character {} not in {}'.format(self.__class__.__name__, c, self.enigma.alphabet)
charSet.add(c)
savedTagesWalzenStellungen = self.tagesWalzenStellungen
patternDict : Dict[str, Set[str]] = dict()
for tagesWalzenStellungenTuple in itertools.permutations(self.alphabet, len(savedTagesWalzenStellungen)):
self.tagesWalzenStellungen = ''.join(tagesWalzenStellungenTuple)
charEncodingDict = dict()
for c in charSet:
charEncodingDict[c] = self.encode(maxSpruchlength * c)
for encodedSpruch in encodedSpruchList:
lSpruch = len(encodedSpruch)
for substring in substringList:
isInSpruch = False
ls = lSpruch - len(substring)
for n, (c0, cExp) in enumerate(zip(encodedSpruch[:ls], charEncodingDict[substring[0]][:ls])):
if c0 == cExp:
for m, cm in enumerate(substring[1:]):
nm = n + m + 1
if cm == charEncodingDict[substring[m]][nm]:
if m == len(substring) - 2:
isInSpruch = True
break
if isInSpruch:
if substring not in patternDict:
patternDict[substring] = list()
patternDict[substring].add(self.tagesWalzenStellungen)
return patternDict
[docs] def findDoublets(self, first : int = 0, second : int = 3, all : bool = True) -> Union[List[List[str]], List[str]]:
"""Find doublets of any character at fixed positions
:param first: first position
:param second: second position (> first)
:return: IF ALL it returns a list of doublets for every position ELSE it returns a list of doublets
"""
assert first >= 0 and second > first
allDoubletsList = list()
if all:
shift = second - first
else:
shift = 1
ecList = list()
for nc, c in enumerate(self.alphabet):
ecList.append(list())
for cs in self.encode((second + shift) * c):
ecList[-1].append(cs)
for ns in range(shift):
doubletsList = list()
for n, c in enumerate(self.alphabet):
doubletsList.append(ecList[n][first+ns] + ecList[n][second+ns])
allDoubletsList.append(doubletsList)
if not all:
return doubletsList
return allDoubletsList
def findTagesWalzenStellungen(self, rejewskiList : List[Dict[str, Set[str]]], twiceEncodedSpruchWalzenStellungen : Union[List[str], str] = '') -> List[str]:
if isinstance(twiceEncodedSpruchWalzenStellungen, str):
twiceEncodedSpruchWalzenStellungen = [twiceEncodedSpruchWalzenStellungen]
tagesWalzenStellungen = None
for tesws in twiceEncodedSpruchWalzenStellungen:
shift = len(tesws) // 2
assert shift == len(rejewskiList), '{}.findTagesWalzenStellungen: len(twiceEncodedSpruchWalzenStellungen) = {} == 2*len(rejewskiList) = {}'.format(self.__class__.__name__, shift, 2*len(rejewskiList))
for n, doubletsDict in enumerate(rejewskiList):
pair = tesws[n] + tesws[n+shift]
assert pair in rejewskiList[n].keys(), '{}.findTagesWalzenStellungen: {} not a key of rejewskiList[{}]'.format(self.__class__.__name__, pair, n)
if tagesWalzenStellungen is None:
tagesWalzenStellungen = rejewskiList[n][pair]
else:
tagesWalzenStellungen &= rejewskiList[n][pair]
return sorted(tagesWalzenStellungen)
[docs] def encode(self, spruch : str, spruchWalzenStellungen : Optional[str] = None) -> str:
"""Runs forward through the enigma
If the spruchWalzenStellungen is defined,
- it is encoded twice using the tagesWalzenStellungen
- the spruch is then encoded using the spruchWalzenStellungen
otherwise the spruch is encoded using the tagesWalzenStellungen
:param spruch: message to be encoded
:param spruchWalzenStellungen: see above
:returns: encoded spruch
"""
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
walze.ringstellung = ringstellung
out = str()
if spruchWalzenStellungen:
assert len(spruchWalzenStellungen) == len(self.tagesWalzenStellungen), '{}.encode: Walzenstellung, number of characters must match the number of walzen'.format(self.__class__.__name__)
assert all(v in self.alphabet for v in spruchWalzenStellungen), '{}.encode: Walzenstellung, all characters must be in alphabet'.format(self.__class__.__name__)
for n in range(2):
for c in spruchWalzenStellungen:
out += self._encodeLetter(c)
for walze, ringstellung in zip(self.walzen, spruchWalzenStellungen):
walze.ringstellung = ringstellung
if self.notify is not None:
self.notify('{}.encode/spruchWalzenStellungen: {} => {}'.format(self.__class__.__name__, spruchWalzenStellungen, out[:len(spruchWalzenStellungen)]))
for c in spruch.upper():
if c == ' ':
c = self.blank
if len(c) == 1:
out += self._encodeLetter(c)
if self.notify is not None:
self.notify('{}.encode: {} => {}'.format(self.__class__.__name__, spruch, out))
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
walze.ringstellung = ringstellung
return out
[docs] def encodeMatrix(self, msgLen : int) -> List[List[str]]:
"""Runs forward through the enigma using all characters of the alphabet
:param msgLen: length of messages
:returns: list of encoded messages
"""
ecList = list()
for nc, c in enumerate(self.enigma.alphabet):
ecList.append(list())
for cs in self.encode(msgLen * c):
ecList[-1].append(self.enigma.alphabet.index(cs))
return ecList
[docs] def decode(self, encodedSpruch : str, useSpruchWalzenStellungen : bool = False) -> str:
"""Runs backward through the enigma
If useSpruchWalzenStellungen,
- the spruchWalzenStellung is expected twice at the beginning
- the spruch is then decoded using the spruchWalzenStellungen
otherwise the spruch is decoded using the tagesWalzenStellungen
:param encodedSpruch: message to be decoded
:param useSpruchWalzenStellungen: see above
:returns: decoded spruch
"""
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
walze.ringstellung = ringstellung
if useSpruchWalzenStellungen:
spruchWalzenStellungen = str()
ls = len(self.tagesWalzenStellungen)
for c in encodedSpruch[:2*ls]:
spruchWalzenStellungen += self._encodeLetter(c)
assert spruchWalzenStellungen[:ls] == spruchWalzenStellungen[ls:], '{}.decode: Inconsistent Spruchschlüssel {}'.format(self.__class__.__name__, spruchWalzenStellungen)
if self.notify is not None:
self.notify('{}.decode/spruchWalzenStellungen: {} => {}'.format(self.__class__.__name__, encodedSpruch[:ls], spruchWalzenStellungen[:ls]))
for walze, ringstellung in zip(self.walzen, spruchWalzenStellungen[:ls]):
walze.ringstellung = ringstellung
encodedSpruch = encodedSpruch[2*ls:]
out = str()
for c in encodedSpruch:
out += self._encodeLetter(c)
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
walze.ringstellung = ringstellung
return out
def _encodeLetter(self, c : str) -> str:
"""Encodes a single character and steps the first Walze
:param c: single character
:returns: encoded character
"""
assert len(c) == 1, '{}.encodeLetter {}: len({}) != 1'.format(self.__class__.__name__, self.name, c)
assert c in self.enigma.alphabet, '{}.encodeLetter: letter ({}) not in alphabet'.format(self.__class__.__name__, c)
self.walzen[0].step()
return self.firstComponent.encode(c, forward = True)
[docs] def chain(self) -> List[MzEnigma.Component]:
"""Creates a chain of visited components
:returns: chain list
"""
chainList = self.firstComponent.chain(forward = True)
return chainList
[docs] def debugEncode(self, c : str ) -> str:
"""Shows the transformation of a character after each component
:param c: single character
:return: text built up from the characters after each component
"""
assert len(c) == 1, '{}.debugEncode: len({}) != 1'.format(self.__class__.__name__, c)
assert c in self.enigma.alphabet, '{}.debugEncode: Character {} out of range'.format(self.__class__.__name__, c)
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
walze.ringstellung = ringstellung
output = ''
actC = c
for component, forward in self.chain():
actC = component.encode(actC, forward, componentOnly = True)
output += actC
return output
[docs] def frequencyDict(self, spruch : str, decode : bool = True) -> Dict[int, Set[Tuple[str, Set[str]]]]:
"""Creates a frequency dictionary of a message
:param spruch: message to be analysed (required)
:param decode: use decoced message instead of the original message
:return: Dict[frequency, Set[Tuple[char, Tuple[occurrency]]
"""
if decode:
spruch = self.decode(spruch)
assert len(spruch) > 0
c2sDict = dict()
for n, sc in enumerate(spruch):
if sc not in c2sDict:
c2sDict[sc] = set()
c2sDict[sc].add(n)
l2csDict = dict()
for c, nSet in c2sDict.items():
l = len(nSet)
if l not in l2csDict:
l2csDict[l] = set()
l2csDict[l].add((c, tuple(sorted(nSet))))
return l2csDict
@property
def alphabet(self) -> str:
"""
:getter: Returns the alphabet of the engine
:setter: None
"""
return self.enigma.alphabet
def __eq__(self, tagesschluessel : Tagesschluessel) -> bool:
return self.enigma == tagesschluessel.enigma \
and self.tagesWalzenStellungen == tagesschluessel.tagesWalzenStellungen \
and self.umkehrwalze == tagesschluessel.umkehrwalze \
and self.walzen == tagesschluessel.walzen \
and self.steckerbrett == tagesschluessel.steckerbrett \
and self.zusatzwalze == tagesschluessel.zusatzwalze
def __repr__(self) -> str:
content = 'class: {}\nmodel: {}\nalphabet: {}'.format(self.__class__.__name__, self.enigma.model, self.alphabet)
for walze, ringstellung in zip(self.walzen, self.tagesWalzenStellungen):
content += '\nWalze {}, Stellung: {}, wiring: {}, notches: {}'.format(walze.name, ringstellung, walze.wiring, walze.notches)
content += '\nUmkehrwalze: {}, wiring: {}'.format(self.umkehrwalze.name, self.umkehrwalze.wiring)
if self.steckerbrett:
content += '\nSteckerbrett {}, wiring: {}'.format(self.steckerbrett.name, self.steckerbrett.wiring)
if self.zusatzwalze:
content += '\nZusatzwalze: {}, wiring: {}'.format(self.zusatzwalze.name, self.zusatzwalze.wiring)
return content