diff --git a/README.md b/README.md index b8a0fbf..200fc09 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ If the output is too long, you can limit (the already loaded data) with `xl 180` `Rune.kind` can be one of `r n s l w` – meaning (r)une, (n)umber, (s)entence, (l)ine, or (w)hitespace. A line is what you see in the source file (which is equivalent to a line in the original jpg page). A sentence is one that ends with a period (`⁘`). -`Rune` as well as `RuneText` both support simple arithmetic operations: `Rune(i=2) - 2` will yield a `ᚠ` rune. For example, you can invert a text with `28 - RuneText('test me')`. +`Rune` as well as `RuneText` both support simple arithmetic operations: `Rune(i=2) - 2` will yield a `ᚠ` rune. For example, you can invert a text with `28 - RuneText('test me')` or simply `~RuneText('inverted')`. __Note:__ Always initialize a rune with its rune character or its index, never ASCII or its prime value. @@ -313,7 +313,7 @@ As an optimization, smaller look ahead levels are tried first. E.g., if you spec The complexity is not linear and depends on whether “there was just another better solution”. With the default look ahead of 3, which can flip 3 bits simultaneously, each step performs 66!/(3!(66-3)!) + 66!/(2!(66-2)!) + 66 operations or __4.8*10^4__. Usually it takes no more than 2–3 steps. - + ### InterruptDB.py Calculating the best interrupt position takes quite long, so we can optimize our program by pre-calculating the IoC's. That is what `InterruptDB.py` is for. The class will search for the best interrupts and store the IoC score as well as the set of interrupts in a file. Later queries just need to process this file instead. diff --git a/RuneSolver.py b/RuneSolver.py index b17f459..f2b6385 100755 --- a/RuneSolver.py +++ b/RuneSolver.py @@ -20,6 +20,12 @@ class RuneSolver(RuneRunner): def highlight_interrupt(self): return self.highlight_rune(self.INTERRUPT, self.INTERRUPT_POS) + def substitute_get(self, pos, keylen, search_term, found_term): + return found_term.zip_sub(search_term).description(count=True) + + def substitute_supports_keylen(self): + return False + def run(self, data=None): if data: self.input.load(data=data) @@ -120,7 +126,9 @@ class RunningKeySolver(RuneSolver): if self.KEY_INVERT: r_idx = 28 - r_idx pos = self.active_key_pos() - if pos != -1: + if pos == -1: + self.copy_unmodified(r_idx) + else: i = (pos + self.KEY_SHIFT) % self.k_len r_idx = (self.decrypt(r_idx, i) - self.KEY_ROTATE) % 29 # rotate_key @@ -128,8 +136,11 @@ class RunningKeySolver(RuneSolver): self.k_current_pos = (self.k_current_pos + 1) % self.k_full_len return Rune(i=r_idx) - def decrypt(self, rune_index, key_index): - raise NotImplementedError # must subclass + def decrypt(self, rune_index, key_index): # must subclass + raise NotImplementedError + + def copy_unmodified(self, rune_index): # subclass if needed + pass def key__str__(self): return self.KEY_DATA # you should override this @@ -156,6 +167,15 @@ class VigenereSolver(RunningKeySolver): def decrypt(self, rune_index, key_index): return rune_index - self.KEY_DATA[key_index] + def substitute_supports_keylen(self): + return True + + def substitute_get(self, pos, keylen, search_term, found_term): + ret = [Rune(r='⁚')] * keylen + for i, r in enumerate(found_term.zip_sub(search_term)): + ret[(pos + i) % keylen] = r + return RuneText(ret).description(count=True, index=False) + def key__str__(self): return self.key__str__basic_runes() @@ -175,13 +195,35 @@ class AffineSolver(RunningKeySolver): class AutokeySolver(RunningKeySolver): def run(self, data=None): - self.running_key = self.KEY_DATA[:] + key = self.KEY_DATA[self.KEY_SHIFT:] + self.KEY_DATA[:self.KEY_SHIFT] + key = [29] * self.KEY_OFFSET + key + [29] * self.KEY_POST_PAD + self.running_key = key super().run(data=data) - def decrypt(self, rune_index, key_index): + def decrypt(self, rune_index, _): rune_index = (rune_index - self.running_key.pop(0)) % 29 self.running_key.append(rune_index) return rune_index + def copy_unmodified(self, rune_index): + if self.k_len > 0: + self.running_key.pop(0) + self.running_key.append(rune_index) + + def substitute_supports_keylen(self): + return True + + def substitute_get(self, pos, keylen, search_term, found_term): + data = self.input.runes_no_whitespace() + ret = [Rune(r='⁚')] * keylen + for o in range(len(search_term)): + plain = search_term[o] + i = pos + o + while i >= 0: + plain = data[i] - plain + i -= keylen + ret[i + keylen] = plain + return RuneText(ret).description(count=True, index=False) + def key__str__(self): return self.key__str__basic_runes() diff --git a/RuneText.py b/RuneText.py index d30ac12..f028f37 100755 --- a/RuneText.py +++ b/RuneText.py @@ -101,6 +101,9 @@ class Rune(object): def __rsub__(self, o): return self if self.index == 29 else Rune(i=(o - self.index) % 29) + def __invert__(self): + return self if self.index == 29 else Rune(i=28 - self.index) + ######################################### # RuneText : Stores multiple Rune objects. Allows arithmetic operations @@ -196,6 +199,11 @@ class RuneText(object): d['i'] = [x for x in d['i'] if x != 29] return fmt.format(d['r'], len(d['r']), d['t'], len(d['t']), d['i']) + def zip_sub(self, other): + if len(self) != len(other): + raise IndexError('RuneText length mismatch') + return RuneText([x - y for x, y in zip(self._data, other._data)]) + def __getitem__(self, key): if isinstance(key, str): return [getattr(x, key) for x in self._data] @@ -206,20 +214,10 @@ class RuneText(object): self._data[key] = value def __add__(self, other): - if isinstance(other, RuneText): - if len(self) != len(other): - raise IndexError('RuneText length mismatch') - return RuneText([x + y for x, y in zip(self._data, other._data)]) - else: - return RuneText([x + other for x in self._data]) + return RuneText([x + other for x in self._data]) def __sub__(self, other): - if isinstance(other, RuneText): - if len(self) != len(other): - raise IndexError('RuneText length mismatch') - return RuneText([x - y for x, y in zip(self._data, other._data)]) - else: - return RuneText([x - other for x in self._data]) + return RuneText([x - other for x in self._data]) def __radd__(self, other): return RuneText([other + x for x in self._data]) @@ -227,5 +225,8 @@ class RuneText(object): def __rsub__(self, other): return RuneText([other - x for x in self._data]) + def __invert__(self): + return RuneText([~x for x in self._data]) + def __repr__(self): return f'RuneText<{len(self._data)}>' diff --git a/playground.py b/playground.py index b1deeea..2e50b44 100755 --- a/playground.py +++ b/playground.py @@ -85,8 +85,8 @@ def command_a(cmd, args): # [a]ll variations root = RuneText(args) inclIndex = 'q' not in cmd if 'i' in cmd: - root = 28 - root - for i in range(0, 29): + root = ~root + for i in range(29): print('{:02d}: {}'.format(i, (root + i).description(index=inclIndex))) @@ -110,7 +110,7 @@ def command_d(cmd, args): # [d]ecrypt or single substitution print('Error: key length mismatch') else: print('Substition:') - print((enc - plain).description()) + print((enc.zip_sub(plain)).description()) ######################################### @@ -134,13 +134,26 @@ def command_f(cmd, args): # (f)ind word print() print('Found:') for _, _, pos, _, w in cur_words: - print('{:04}: {}'.format(pos, w.description(count=True))) + print(f'{pos:04}: {w.description(count=True)}') if search_term: print() + keylen = [len(search_term)] + if SOLVER.substitute_supports_keylen(): + try: + inp = input('What is the key length? (num or [a]ll): ').strip() + if inp: + if inp[0] == 'a': + keylen = range(len(search_term), 24) + else: + keylen = [int(inp)] + except ValueError: + raise ValueError('not a number.') + print() print('Available substition:') for _, _, pos, _, w in cur_words: - word = search_term - w - print('{:04}: {}'.format(pos, word.description(count=True))) + for kl in keylen: + res = SOLVER.substitute_get(pos, kl, search_term, w) + print(f'{pos:04}: {res}') ######################################### @@ -253,17 +266,16 @@ def command_t(cmd, args): # (t)ranslate if cmd != 't': return False word = RuneText(args) - rev_word = 28 - word - word = word.as_dict() - psum = sum(word['p']) + x = word.as_dict() + psum = sum(x['p']) sffx = '*' if LIB.is_prime(psum) else '' if LIB.is_prime(LIB.rev(psum)): sffx += '√' - print('runes({}): {}'.format(len(word['r']), word['r'])) - print('plain({}): {}'.format(len(word['t']), word['t'])) - print('reversed: {}'.format(''.join([x.rune for x in rev_word]))) - print('indices: {}'.format(word['i'])) - print('prime({}{}): {}'.format(psum, sffx, word['p'])) + print('runes({}): {}'.format(len(x['r']), x['r'])) + print('plain({}): {}'.format(len(x['t']), x['t'])) + print('reversed: {}'.format(''.join([x.rune for x in ~word]))) + print('indices: {}'.format(x['i'])) + print('prime({}{}): {}'.format(psum, sffx, x['p'])) #########################################