refactoring II

This commit is contained in:
relikd
2021-02-12 00:36:01 +01:00
parent 6d01aa4424
commit a9d4085a4b
25 changed files with 1080 additions and 1017 deletions

21
LP/Alphabet.py Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
white_rune = {'': ' ', '': '.', '': ',', '': ';', '': '#'}
white_text = {v: k for k, v in white_rune.items()}
alphabet = [ # Using last value for display. Custom added: V
(2, '', ['F']), (3, '', ['V', 'U']), (5, '', ['TH']), (7, '', ['O']),
(11, '', ['R']), (13, '', ['K', 'C']), (17, '', ['G']),
(19, '', ['W']), (23, '', ['H']), (29, '', ['N']), (31, '', ['I']),
(37, '', ['J']), (41, '', ['EO']), (43, '', ['P']), (47, '', ['X']),
(53, '', ['Z', 'S']), (59, '', ['T']), (61, '', ['B']),
(67, '', ['E']), (71, '', ['M']), (73, '', ['L']),
(79, '', ['ING', 'NG']), (83, '', ['OE']), (89, '', ['D']),
(97, '', ['A']), (101, '', ['AE']), (103, '', ['Y']),
(107, '', ['IO', 'IA']), (109, '', ['EA'])
]
text_map = {t: r for _, r, ta in alphabet for t in ta}
rune_map = {r: t for _, r, ta in alphabet for t in ta}
primes_map = {r: p for p, r, _ in alphabet}
RUNES = [r for _, r, _ in alphabet] # array already sorted
# del alphabet # used in playground for GP display

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env python3
from RuneText import rune_map, RuneText
# -*- coding: UTF-8 -*-
from Alphabet import rune_map
from RuneText import RuneText
from NGrams import NGrams
@@ -57,5 +59,6 @@ class NGramShifter(object):
print()
# NGramShifter().guess('ᛈᚢᛟᚫᛈᚠᛖᚱᛋᛈᛈᚦᛗᚾᚪᚱᛚᚹᛈᛖᚩᛈᚢᛠᛁᛁᚻᛞᛚᛟᛠ', 'ᛟ')
# NGramShifter().guess([1, 2, 4, 5, 7, 9, 0, 12], 'ᛟ')
if __name__ == '__main__':
NGramShifter().guess('ᛈᚢᛟᚫᛈᚠᛖᚱᛋᛈᛈᚦᛗᚾᚪᚱᛚᚹᛈᛖᚩᛈᚢᛠᛁᛁᚻᛞᛚᛟᛠ', '')
NGramShifter().guess([1, 2, 4, 5, 7, 9, 0, 12], '')

51
LP/IOReader.py Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re # load_indices
from Alphabet import RUNES
from RuneText import RuneText
re_norune = re.compile('[^' + ''.join(RUNES) + ']')
#########################################
# load page and convert to indices for faster access
#########################################
def load_indices(fname, interrupt, maxinterrupt=None, minlen=None, limit=None):
with open(fname, 'r') as f:
data = RuneText(re_norune.sub('', f.read())).index_no_white[:limit]
if maxinterrupt is not None:
# incl. everything up to but not including next interrupt
# e.g., maxinterrupt = 0 will return text until first interrupt
for i, x in enumerate(data):
if x != interrupt:
continue
if maxinterrupt == 0:
if minlen and i < minlen:
continue
return data[:i]
maxinterrupt -= 1
return data
#########################################
# find the longest chunk in a list of indices, which does not include an irp
#########################################
def longest_no_interrupt(data, interrupt, irpmax=0):
def add(i):
nonlocal ret, prev
idx = prev.pop(0)
if idx == 0:
ret = []
ret.append((i - idx, idx))
prev = [0] * (irpmax + 1)
ret = []
for i, x in enumerate(data):
if x == interrupt:
prev.append(i + 1)
add(i)
add(i + 1)
length, pos = max(ret)
return pos, length

148
LP/IOWriter.py Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import sys
from RuneText import RuneText
import utils
#########################################
# IOWriter : handle std output with highlight etc.
#########################################
class IOWriter(object):
def __init__(self):
self.BREAK_MODE = None
self.VERBOSE = '-v' in sys.argv
self.QUIET = '-q' in sys.argv
self.COLORS = True # sys.stdout.isatty() doesnt matter if no highlight
self.cur_color = None
self.file_output = None
def clear(self):
self.linesum = 0
self.out_r = ''
self.out_t = ''
self.out_p = ''
self.out_ps = ''
def mark(self, color=None):
if self.COLORS:
m = f'\x1b[{color}' if color else '\x1b[0m'
self.cur_color = color
self.out_r += m
self.out_t += m
self.out_p += m
# self.out_ps += m # No. Because a word may be split-up
def run(self, data, highlight=None): # make sure sorted, non-overlapping
break_on = self.BREAK_MODE # set by user
if break_on is None: # check None specifically, to allow '' as value
break_on = 'l' if self.VERBOSE else 's' # dynamically adapt mode
wsum = 0
self.clear()
if not highlight:
highlight = []
highlight.append((len(data), len(data)))
for i in range(len(data)):
# Handle color highlight
if i == highlight[0][0]:
try:
color = highlight[0][2] # e.g. 1;30m for bold black
except IndexError:
color = '1;31m' # fallback to bold red
self.mark(color)
elif i >= highlight[0][1]:
self.mark()
highlight.pop(0)
cur = data[i]
eow = i + 1 == len(data) or data[i + 1].kind not in 'rl'
# Output current rune
if cur.kind == 'l':
if cur.kind == break_on:
self.write()
continue # ignore all \n,\r if not forced explicitly
self.out_r += cur.rune
self.out_t += cur.text
if cur.kind != 'r':
if self.VERBOSE:
self.out_p += ' '
if cur.kind == break_on:
self.write()
continue
# Special case when printing numbers.
# Keep both lines (text + numbers) in sync.
if self.VERBOSE:
b = f'{cur.prime}' # TODO: option for indices instead
fillup = len(b) - len(cur.text)
self.out_t += ' ' * fillup
self.out_p += b
if not eow:
if fillup >= 0:
self.out_t += ' '
self.out_p += '+'
# Mark prime words
wsum += cur.prime
if eow and wsum > 0:
self.linesum += wsum
if self.VERBOSE:
if self.out_ps:
self.out_ps += ' + '
self.out_ps += str(wsum)
if utils.is_prime(wsum):
if self.VERBOSE:
self.out_ps += '*'
elif not self.QUIET: # and wsum > 109
self.out_t += '__'
wsum = 0
self.write()
def write(self):
def print_f(x=''):
if self.file_output:
with open(self.file_output, 'a') as f:
f.write(x + '\n')
else:
print(x)
if not self.out_t:
return
prev_color = self.cur_color
if prev_color:
self.mark()
sffx = ' = {}'.format(self.linesum)
if utils.is_prime(self.linesum):
sffx += '*'
if utils.is_emirp(self.linesum):
sffx += ''
if not self.QUIET or self.VERBOSE:
print_f()
if not self.QUIET:
print_f(self.out_r)
if not (self.QUIET or self.VERBOSE):
self.out_t += sffx
print_f(self.out_t)
if self.VERBOSE:
self.out_ps += sffx
print_f(self.out_p)
print_f(self.out_ps)
self.clear()
if prev_color:
self.mark(prev_color)
if __name__ == '__main__':
txt = RuneText('Hi there. And welc\nome, to my world; "manatee"')
io = IOWriter()
io.BREAK_MODE = 's' # 'l'
# io.VERBOSE = True
# io.QUIET = True
io.run(txt, [(4, 12), (13, 27)])

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
from HeuristicSearch import SearchInterrupt
from HeuristicLib import Probability
from RuneText import RUNES, load_indices
from InterruptSearch import InterruptSearch
from Probability import Probability
from IOReader import load_indices
from LPath import FILES_ALL, FILES_UNSOLVED, LPath
@@ -13,20 +14,14 @@ from LPath import FILES_ALL, FILES_UNSOLVED, LPath
class InterruptDB(object):
def __init__(self, data, interrupt):
self.irp = interrupt
self.iguess = SearchInterrupt(data, interrupt)
self.iguess = InterruptSearch(data, irp=interrupt)
self.irp_count = len(self.iguess.stops)
def make(self, dbname, name, keylen):
def fn(x):
return Probability.target_diff(x, keylen) # used in db_norm
# return Probability.IC_w_keylen(x, keylen) # used in db_high
if keylen == 0:
keylen = 1
score, skips = fn(self.iguess.join()), [[]] # without interrupts
def make(self, dbname, name, keylen, fn_score):
if keylen == 0: # without interrupts
score, skips = fn_score(self.iguess.join(), 1), [[]]
else:
score, skips = self.iguess.sequential(fn, startAt=0, maxdepth=99)
# score, skips = self.iguess.genetic(fn, topDown=False, maxdepth=4)
score, skips = self.iguess.all(keylen, fn_score)
for i, interrupts in enumerate(skips):
skips[i] = self.iguess.to_occurrence_index(interrupts)
@@ -35,18 +30,17 @@ class InterruptDB(object):
name, score, self.irp, self.irp_count, keylen, nums, dbname)
return score, skips
def make_secondary(self, dbname, name, keylen, threshold):
def make_secondary(self, dbname, name, keylen, fn_score, threshold):
scores = []
def fn(x):
score = Probability.target_diff(x, keylen) # used in db_norm
# score = Probability.IC_w_keylen(x, keylen) # used in db_high
def fn(x, kl):
score = fn_score(x, kl)
if score >= threshold:
scores.append(score)
return 1
return -1
_, skips = self.iguess.sequential(fn, startAt=0, maxdepth=99)
_, skips = self.iguess.all(keylen, fn)
for i, interrupts in enumerate(skips):
skips[i] = self.iguess.to_occurrence_index(interrupts)
ret = list(zip(scores, skips))
@@ -58,25 +52,6 @@ class InterruptDB(object):
name, score, self.irp, self.irp_count, keylen, nums, dbname)
return len(filtered)
@staticmethod
def longest_no_interrupt(data, interrupt, irpmax=0):
def add(i):
nonlocal ret, prev
idx = prev.pop(0)
if idx == 0:
ret = []
ret.append((i - idx, idx))
prev = [0] * (irpmax + 1)
ret = []
for i, x in enumerate(data):
if x == interrupt:
prev.append(i + 1)
add(i)
add(i + 1)
length, pos = max(ret)
return pos, length
@staticmethod
def load(dbname):
if not os.path.isfile(LPath.InterruptDB(dbname)):
@@ -103,171 +78,12 @@ class InterruptDB(object):
f.write(f'{name}|{irpmax}|{score:.5f}|{irp}|{keylen}|{nums}\n')
#########################################
# InterruptIndices : Read chapters and extract indices (cluster by runes)
#########################################
class InterruptIndices(object):
def __init__(self):
self.pos = InterruptIndices.read()
def consider(self, name, irp, limit):
nums = self.pos[name]['pos'][irp]
if len(nums) <= limit:
return self.pos[name]['total']
return nums[limit] # number of runes, which is not last index
def total(self, name):
return self.pos[name]['total']
def longest_no_interrupt(self, name, irp, irpmax=0):
irpmax += 1
nums = self.pos[name]['pos'][irp] + [self.pos[name]['total']] * irpmax
ret = [(y - x, x) for x, y in zip(nums, nums[irpmax:])]
return sorted(ret, reverse=True)
@staticmethod
def write(dbname='db_indices'):
with open(LPath.InterruptDB(dbname), 'w') as f:
f.write('# file | total runes in file | interrupt | indices\n')
for name in FILES_ALL:
fname = f'pages/{name}.txt'
data = load_indices(fname, 0)
total = len(data)
nums = [[] for x in range(29)]
for idx, rune in enumerate(data):
nums[rune].append(idx)
for irp, pos in enumerate(nums):
f.write('{}|{}|{}|{}\n'.format(
name, total, irp, ','.join(map(str, pos))))
@staticmethod
def read(dbname='db_indices'):
with open(LPath.InterruptDB(dbname), 'r') as f:
ret = {}
for line in f.readlines():
if line.startswith('#'):
continue
line = line.strip()
name, total, irp, nums = line.split('|')
if name not in ret:
ret[name] = {'total': int(total),
'pos': [[] for _ in range(29)]}
pos = ret[name]['pos']
pos[int(irp)] = list(map(int, nums.split(','))) if nums else []
return ret
#########################################
# InterruptToWeb : Read interrupt DB and create html graphic / matrix
#########################################
class InterruptToWeb(object):
def __init__(self, dbname, template='template.html'):
with open(LPath.results(template), 'r') as f:
self.template = f.read()
self.indices = InterruptIndices()
self.scores = {}
db = InterruptDB.load(dbname)
for k, v in db.items():
for irpc, score, irp, kl, nums in v:
if k not in self.scores:
self.scores[k] = [[] for _ in range(29)]
part = self.scores[k][irp]
while kl >= len(part):
part.append((0, 0)) # (score, irpc)
oldc = part[kl][1]
if irpc > oldc or (irpc == oldc and score > part[kl][0]):
part[kl] = (score, irpc)
def cls(self, x, low=0, high=1):
if x <= low:
return ' class="m0"'
return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"'
def table_reliable(self):
trh = '<tr class="rotate"><th></th>'
trtotal = '<tr class="small"><th>Total</th>'
trd = [f'<tr><th>{x}</th>' for x in RUNES]
del_row = [True] * 29
for name in FILES_ALL:
if name not in self.scores:
continue
total = self.indices.total(name)
trh += f'<th><div>{name}</div></th>'
trtotal += f'<td>{total}</td>'
for i in range(29):
scrs = self.scores[name][i][1:]
if not scrs:
trd[i] += '<td></td>'
continue
del_row[i] = False
worst_irpc = min([x[1] for x in scrs])
if worst_irpc == 0:
if max([x[1] for x in scrs]) != 0:
trd[i] += '<td>?</td>'
continue
num = self.indices.consider(name, i, worst_irpc)
trd[i] += f'<td{self.cls(num, 384, 812)}>{num}</td>'
trh += '</tr>\n'
trtotal += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
if del_row[i]:
trd[i] = ''
return f'<table>{trh}{"".join(trd)}{trtotal}</table>'
def table_interrupt(self, irp, pmin=1.25, pmax=1.65):
maxkl = max(len(x[irp]) for x in self.scores.values())
trh = '<tr class="rotate"><th></th>'
trbest = '<tr class="small"><th>best</th>'
trd = [f'<tr><th>{x}</th>' for x in range(maxkl)]
for name in FILES_ALL:
maxscore = 0
bestkl = -1
try:
klarr = self.scores[name][irp]
except KeyError:
continue
trh += f'<th><div>{name}</div></th>'
for kl, (score, _) in enumerate(klarr):
if score < 0:
trd[kl] += f'<td{self.cls(0)}></td>'
else:
trd[kl] += f'<td{self.cls(score, pmin, pmax)}>{score:.2f}</td>'
if score > maxscore:
maxscore = score
bestkl = kl
trbest += f'<td>{bestkl}</td>'
trh += '</tr>\n'
trbest += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
return f'<table>{trh}{"".join(trd[1:])}{trbest}</table>'
def make(self, outfile, pmin=1.25, pmax=1.65):
nav = ''
txt = ''
for i in range(29):
has_entries = any(True for x in self.scores.values() if x[i])
if not has_entries:
continue
nav += f'<a href="#tb-i{i}">{RUNES[i]}</a>\n'
txt += f'<h3 id="tb-i{i}">Interrupt {i}: <b>{RUNES[i]}</b></h3>'
txt += self.table_interrupt(i, pmin, pmax)
html = self.template.replace('__NAVIGATION__', nav)
html = html.replace('__TAB_RELIABLE__', self.table_reliable())
html = html.replace('__INTERRUPT_TABLES__', txt)
with open(LPath.results(outfile), 'w') as f:
f.write(html)
#########################################
# helper functions
#########################################
def create_initial_db(dbname, minkl=1, maxkl=32, max_irp=20, irpset=range(29)):
def create_initial_db(dbname, fn_score, klset=range(1, 33),
max_irp=20, irpset=range(29)):
oldDB = InterruptDB.load(dbname)
oldValues = {k: set((a, b, c) for a, _, b, c, _ in v)
for k, v in oldDB.items()}
@@ -276,15 +92,16 @@ def create_initial_db(dbname, minkl=1, maxkl=32, max_irp=20, irpset=range(29)):
data = load_indices(LPath.page(name), irp, maxinterrupt=max_irp)
db = InterruptDB(data, irp)
print('load:', name, 'interrupt:', irp, 'count:', db.irp_count)
for keylen in range(minkl, maxkl + 1): # key length
for keylen in klset: # key length
if (db.irp_count, irp, keylen) in oldValues.get(name, []):
print(f'{keylen}: skipped.')
continue
score, interrupts = db.make(dbname, name, keylen)
score, interrupts = db.make(dbname, name, keylen, fn_score)
print(f'{keylen}: {score:.4f}, solutions: {len(interrupts)}')
def find_secondary_solutions(db_in, db_out, threshold=0.75, max_irp=20):
def find_secondary_solutions(db_in, db_out, fn_score,
threshold=0.75, max_irp=20):
oldDB = InterruptDB.load(db_in)
search_set = set()
for name, arr in oldDB.items():
@@ -299,13 +116,14 @@ def find_secondary_solutions(db_in, db_out, threshold=0.75, max_irp=20):
print('load:', name, 'interrupt:', irp, 'keylen:', kl)
data = load_indices(LPath.page(name), irp, maxinterrupt=max_irp)
db = InterruptDB(data, irp)
c = db.make_secondary(db_out, name, kl, threshold)
c = db.make_secondary(db_out, name, kl, fn_score, threshold)
print('found', c, 'additional solutions')
if __name__ == '__main__':
# find_secondary_solutions('db_high', 'db_high_secondary', threshold=1.4)
# find_secondary_solutions('db_norm', 'db_norm_secondary', threshold=0.55)
# create_initial_db('db_norm', minkl=1, maxkl=32, max_irp=20)
# InterruptToWeb('db_high').make('index_high.html')
InterruptToWeb('db_norm').make('index_norm.html', pmin=0.40, pmax=0.98)
create_initial_db('db_high', Probability.IC_w_keylen, max_irp=20)
create_initial_db('db_norm', Probability.target_diff, max_irp=20)
# find_secondary_solutions('db_high', 'db_high_secondary',
# Probability.IC_w_keylen, threshold=1.4)
# find_secondary_solutions('db_norm', 'db_norm_secondary',
# Probability.target_diff, threshold=0.55)

64
LP/InterruptIndices.py Executable file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from IOReader import load_indices
from LPath import FILES_ALL, LPath
#########################################
# InterruptIndices : Read chapters and extract indices (cluster by runes)
#########################################
class InterruptIndices(object):
def __init__(self):
self.pos = InterruptIndices.load()
def consider(self, name, irp, limit):
nums = self.pos[name]['pos'][irp]
total = self.pos[name]['total'] if len(nums) <= limit else nums[limit]
return nums[:limit], total
def total(self, name):
return self.pos[name]['total']
def longest_no_interrupt(self, name, irp, irpmax=0):
irpmax += 1
nums = self.pos[name]['pos'][irp] + [self.pos[name]['total']] * irpmax
ret = [(y - x, x) for x, y in zip(nums, nums[irpmax:])]
return sorted(ret, reverse=True)
@staticmethod
def write(dbname='db_indices'):
with open(LPath.InterruptDB(dbname), 'w') as f:
f.write('# file | total runes in file | interrupt | indices\n')
for name in FILES_ALL:
data = load_indices(LPath.page(name), 0)
total = len(data)
nums = [[] for x in range(29)]
for idx, rune in enumerate(data):
nums[rune].append(idx)
for irp, pos in enumerate(nums):
f.write('{}|{}|{}|{}\n'.format(
name, total, irp, ','.join(map(str, pos))))
@staticmethod
def load(dbname='db_indices'):
with open(LPath.InterruptDB(dbname), 'r') as f:
ret = {}
for line in f.readlines():
if line.startswith('#'):
continue
line = line.strip()
name, total, irp, nums = line.split('|')
if name not in ret:
ret[name] = {'total': int(total),
'pos': [[] for _ in range(29)]}
pos = ret[name]['pos']
pos[int(irp)] = list(map(int, nums.split(','))) if nums else []
return ret
if __name__ == '__main__':
# InterruptIndices.write()
for name, val in InterruptIndices.load().items():
print(name, 'total:', val['total'])
print(' ', [len(x) for x in val['pos']])

View File

@@ -1,114 +1,18 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import itertools # product, compress, combinations
import bisect # bisect_left, insort
from lib import affine_decrypt
#########################################
# GuessVigenere : Shift values around with a given keylength
# InterruptSearch : Hill climbing algorithm for interrupt detection
#########################################
class GuessVigenere(object):
def __init__(self, nums):
self.nums = nums
def guess(self, keylength, score_fn): # minimize score_fn
found = []
avg_score = 0
for offset in range(keylength):
bi = -1
bs = 9999999
for i in range(29):
shifted = [(x - i) % 29 for x in self.nums[offset::keylength]]
score = score_fn(shifted)
if score < bs:
bs = score
bi = i
avg_score += bs
found.append(bi)
return avg_score / keylength, found
#########################################
# GuessAffine : Find greatest common affine key
#########################################
class GuessAffine(object):
def __init__(self, nums):
self.nums = nums
def guess(self, keylength, score_fn): # minimize score_fn
found = []
avg_score = 0
for offset in range(keylength):
candidate = (None, None)
best = 9999999
for s in range(29):
for t in range(29):
shifted = [affine_decrypt(x, (s, t))
for x in self.nums[offset::keylength]]
score = score_fn(shifted)
if score < best:
best = score
candidate = (s, t)
avg_score += best
found.append(candidate)
return avg_score / keylength, found
#########################################
# GuessPattern : Find a key that is rotated ABC BCA CAB, or ABC CAB BCA
#########################################
class GuessPattern(object):
def __init__(self, nums):
self.nums = nums
@staticmethod
def pattern(keylen, fn_pattern):
mask = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]
return fn_pattern(mask, keylen)
def split(self, keylen, mask, offset=0):
ret = {}
for _ in range(offset):
next(mask)
ret = {k: [] for k in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]}
for n, k in zip(self.nums, mask):
ret[k].append(n)
return ret.values()
def zip(self, key_mask, offset=0):
for _ in range(offset):
next(key_mask)
return [(n - k) % 29 for n, k in zip(self.nums, key_mask)]
@staticmethod
def guess(parts, score_fn): # minimize score_fn
found = []
avg_score = 0
for nums in parts:
best = 9999999
candidate = 0
for i in range(29):
score = score_fn([(x - i) % 29 for x in nums])
if score < best:
best = score
candidate = i
avg_score += best
found.append(candidate)
return avg_score / len(parts), found
#########################################
# SearchInterrupt : Hill climbing algorithm for interrupt detection
#########################################
class SearchInterrupt(object):
def __init__(self, arr, interrupt_chr): # remove all whitespace in arr
class InterruptSearch(object):
def __init__(self, arr, irp): # remove all whitespace in arr
self.single_result = False # if False, return list of equal likelihood
self.full = arr
self.stops = [i for i, n in enumerate(arr) if n == interrupt_chr]
self.stops = [i for i, n in enumerate(arr) if n == irp]
def to_occurrence_index(self, interrupts):
return [self.stops.index(x) + 1 for x in interrupts]
@@ -124,11 +28,27 @@ class SearchInterrupt(object):
i = x
return ret + self.full[i + 1:]
# Just enumerate all possibilities.
# If you need to limit the options, trim the data before computation
def all(self, keylen, score_fn):
best_s = -8
found = [] # [match, match, ...]
for x in itertools.product([False, True], repeat=len(self.stops)):
part = list(itertools.compress(self.stops, x))
score = score_fn(self.join(part), keylen)
if score >= best_s:
if score > best_s or self.single_result:
best_s = score
found = [part]
else:
found.append(part)
return best_s, found
# Go over the full string but only look at the first {maxdepth} interrupts.
# Enumerate all possibilities and choose the one with the highest score.
# If first interrupt is set, add it to the resulting set. If not, ignore it
# Every iteration will add a single interrupt only, not the full set.
def sequential(self, score_fn, startAt=0, maxdepth=9):
def sequential(self, keylen, score_fn, startAt=0, maxdepth=9):
found = [[]]
def best_in_one(i, depth, prefix=[]):
@@ -137,7 +57,7 @@ class SearchInterrupt(object):
irp = self.stops[i:i + depth]
for x in itertools.product([False, True], repeat=depth):
part = list(itertools.compress(irp, x))
score = score_fn(self.join(prefix + part))
score = score_fn(self.join(prefix + part), keylen)
if score >= best_s:
if score > best_s or self.single_result:
best_s = score
@@ -162,7 +82,7 @@ class SearchInterrupt(object):
# first step: move maxdepth-sized window over data
i = startAt - 1 # in case loop isnt called
for i in range(startAt, len(self.stops) - maxdepth):
# print('.', end='')
print('.', end='')
parts, _ = best_in_all(i, maxdepth)
found = []
search = self.stops[i]
@@ -180,7 +100,7 @@ class SearchInterrupt(object):
found.append(prfx + [search])
if bitNotSet:
found.append(prfx)
# print('.')
print('.')
# last step: all permutations for the remaining (< maxdepth) bits
i += 1
remaining, score = best_in_all(i, min(maxdepth, len(self.stops) - i))
@@ -191,10 +111,12 @@ class SearchInterrupt(object):
# Choose the bitset with the highest score and repeat.
# If no better score found, increment number of testing bits and repeat.
# Either start with all interrupts set (topDown) or none set.
def genetic(self, score_fn, topDown=False, maxdepth=3):
def genetic(self, keylen, score_fn, topDown=False, maxdepth=3):
current = self.stops if topDown else []
def evolve(lvl):
if lvl > 0:
yield from evolve(lvl - 1)
for x in itertools.combinations(self.stops, lvl + 1):
tmp = current[:]
for y in x:
@@ -202,9 +124,9 @@ class SearchInterrupt(object):
tmp.pop(bisect.bisect_left(tmp, y))
else:
bisect.insort(tmp, y)
yield tmp, score_fn(self.join(tmp))
yield tmp, score_fn(self.join(tmp), keylen)
best = score_fn(self.join())
best = score_fn(self.join(), keylen)
level = 0 # or start directly with maxdepth - 1
while level < maxdepth:
print('.', end='')
@@ -227,7 +149,10 @@ class SearchInterrupt(object):
return best, all_of_them
# a = GuessInterrupt([2, 0, 1, 0, 14, 15, 0, 13, 24, 25, 25, 25], 0)
# print(a.sequential(lambda x: (1.2 if len(x) == 11 else 0.1)))
# print(a.sequential(lambda x: (1.1 if len(x) == 10 else 0.1)))
# print(a.sequential(lambda x: (1.3 if len(x) == 9 else 0.1)))
if __name__ == '__main__':
a = InterruptSearch([2, 0, 1, 0, 14, 15, 0, 13, 24, 25, 25, 25], irp=0)
print(a.sequential(1, lambda x, k: (1.2 if len(x) == 11 else 0.1)))
print(a.sequential(1, lambda x, k: (1.1 if len(x) == 10 else 0.1)))
print(a.sequential(1, lambda x, k: (1.3 if len(x) == 9 else 0.1)))
print(a.genetic(1, lambda x, k: (1.5 if len(x) == 10 else 0.1)))
print(a.all(1, lambda x, k: (1.4 if len(x) == 11 else 0.1)))

116
LP/InterruptToWeb.py Executable file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from Alphabet import RUNES
from LPath import FILES_ALL, LPath
from InterruptDB import InterruptDB
from InterruptIndices import InterruptIndices
#########################################
# InterruptToWeb : Read interrupt DB and create html graphic / matrix
#########################################
class InterruptToWeb(object):
def __init__(self, dbname, template='template.html'):
with open(LPath.results(template), 'r') as f:
self.template = f.read()
self.indices = InterruptIndices()
self.scores = {}
db = InterruptDB.load(dbname)
for k, v in db.items():
for irpc, score, irp, kl, nums in v:
if k not in self.scores:
self.scores[k] = [[] for _ in range(29)]
part = self.scores[k][irp]
while kl >= len(part):
part.append((0, 0)) # (score, irpc)
oldc = part[kl][1]
if irpc > oldc or (irpc == oldc and score > part[kl][0]):
part[kl] = (score, irpc)
def cls(self, x, low=0, high=1):
if x <= low:
return ' class="m0"'
return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"'
def table_reliable(self):
trh = '<tr class="rotate"><th></th>'
trtotal = '<tr class="small"><th>Total</th>'
trd = [f'<tr><th>{x}</th>' for x in RUNES]
del_row = [True] * 29
for name in FILES_ALL:
if name not in self.scores:
continue
total = self.indices.total(name)
trh += f'<th><div>{name}</div></th>'
trtotal += f'<td>{total}</td>'
for i in range(29):
scrs = self.scores[name][i][1:]
if not scrs:
trd[i] += '<td></td>'
continue
del_row[i] = False
worst_irpc = min([x[1] for x in scrs])
if worst_irpc == 0:
if max([x[1] for x in scrs]) != 0:
trd[i] += '<td>?</td>'
continue
_, num = self.indices.consider(name, i, worst_irpc)
trd[i] += f'<td{self.cls(num, 384, 812)}>{num}</td>'
trh += '</tr>\n'
trtotal += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
if del_row[i]:
trd[i] = ''
return f'<table>{trh}{"".join(trd)}{trtotal}</table>'
def table_interrupt(self, irp, pmin=1.25, pmax=1.65):
maxkl = max(len(x[irp]) for x in self.scores.values())
trh = '<tr class="rotate"><th></th>'
trbest = '<tr class="small"><th>best</th>'
trd = [f'<tr><th>{x}</th>' for x in range(maxkl)]
for name in FILES_ALL:
maxscore = 0
bestkl = -1
try:
klarr = self.scores[name][irp]
except KeyError:
continue
trh += f'<th><div>{name}</div></th>'
for kl, (score, _) in enumerate(klarr):
if score < 0:
trd[kl] += f'<td{self.cls(0)}></td>'
else:
trd[kl] += f'<td{self.cls(score, pmin, pmax)}>{score:.2f}</td>'
if score > maxscore:
maxscore = score
bestkl = kl
trbest += f'<td>{bestkl}</td>'
trh += '</tr>\n'
trbest += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
return f'<table>{trh}{"".join(trd[1:])}{trbest}</table>'
def make(self, outfile, pmin=1.25, pmax=1.65):
nav = ''
txt = ''
for i in range(29):
has_entries = any(True for x in self.scores.values() if x[i])
if not has_entries:
continue
nav += f'<a href="#tb-i{i}">{RUNES[i]}</a>\n'
txt += f'<h3 id="tb-i{i}">Interrupt {i}: <b>{RUNES[i]}</b></h3>'
txt += self.table_interrupt(i, pmin, pmax)
html = self.template.replace('__NAVIGATION__', nav)
html = html.replace('__TAB_RELIABLE__', self.table_reliable())
html = html.replace('__INTERRUPT_TABLES__', txt)
with open(LPath.results(outfile), 'w') as f:
f.write(html)
if __name__ == '__main__':
InterruptToWeb('db_high').make('index_high.html')
InterruptToWeb('db_norm').make('index_norm.html', pmin=0.40, pmax=0.98)

99
LP/KeySearch.py Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from utils import affine_decrypt
#########################################
# GuessVigenere : Shift values around with a given keylength
#########################################
class GuessVigenere(object):
def __init__(self, nums):
self.nums = nums
def guess(self, keylength, score_fn): # minimize score_fn
found = []
avg_score = 0
for offset in range(keylength):
bi = -1
bs = 9999999
for i in range(29):
shifted = [(x - i) % 29 for x in self.nums[offset::keylength]]
score = score_fn(shifted)
if score < bs:
bs = score
bi = i
avg_score += bs
found.append(bi)
return avg_score / keylength, found
#########################################
# GuessAffine : Find greatest common affine key
#########################################
class GuessAffine(object):
def __init__(self, nums):
self.nums = nums
def guess(self, keylength, score_fn): # minimize score_fn
found = []
avg_score = 0
for offset in range(keylength):
candidate = (None, None)
best = 9999999
for s in range(29):
for t in range(29):
shifted = [affine_decrypt(x, (s, t))
for x in self.nums[offset::keylength]]
score = score_fn(shifted)
if score < best:
best = score
candidate = (s, t)
avg_score += best
found.append(candidate)
return avg_score / keylength, found
#########################################
# GuessPattern : Find a key that is rotated ABC BCA CAB, or ABC CAB BCA
#########################################
class GuessPattern(object):
def __init__(self, nums):
self.nums = nums
@staticmethod
def pattern(keylen, fn_pattern):
mask = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]
return fn_pattern(mask, keylen)
def split(self, keylen, mask, offset=0):
ret = {}
for _ in range(offset):
next(mask)
ret = {k: [] for k in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]}
for n, k in zip(self.nums, mask):
ret[k].append(n)
return ret.values()
def zip(self, key_mask, offset=0):
for _ in range(offset):
next(key_mask)
return [(n - k) % 29 for n, k in zip(self.nums, key_mask)]
@staticmethod
def guess(parts, score_fn): # minimize score_fn
found = []
avg_score = 0
for nums in parts:
best = 9999999
candidate = 0
for i in range(29):
score = score_fn([(x - i) % 29 for x in nums])
if score < best:
best = score
candidate = i
avg_score += best
found.append(candidate)
return avg_score / len(parts), found

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os.path
FILES_SOLVED = ['0_warning', '0_welcome', '0_wisdom', '0_koan_1',

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
from RuneText import RUNES, re_norune, RuneText
from Alphabet import RUNES
from IOReader import re_norune
from RuneText import RuneText
from LPath import LPath
@@ -54,21 +57,20 @@ class NGrams(object):
return ret
def make_translation(stream=False): # if true, ignore spaces / word bounds
NGrams.translate(LPath.data('baseline-text'),
LPath.data('baseline-rune'), stream)
if __name__ == '__main__':
def make_translation(stream=False): # if true, ignore spaces / word bounds
NGrams.translate(LPath.data('baseline-text'),
LPath.data('baseline-rune'), stream)
def make_ngrams(max_ngram=1):
for i in range(1, max_ngram + 1):
print(f'generate {i}-gram file')
NGrams.make(i, infile=LPath.data('baseline-rune-words'),
outfile=LPath.data(f'p-{i}gram'))
NGrams.make(i, infile=LPath.root('_solved.txt'),
outfile=LPath.data(f'p-solved-{i}gram'))
NGrams.make(i, infile=LPath.data('baseline-rune-no-e'),
outfile=LPath.data(f'p-no-e-{i}gram'))
def make_ngrams(max_ngram=1):
for i in range(1, max_ngram + 1):
print(f'generate {i}-gram file')
NGrams.make(i, infile=LPath.data('baseline-rune-words'),
outfile=LPath.data(f'p-{i}gram'))
NGrams.make(i, infile=LPath.root('_solved.txt'),
outfile=LPath.data(f'p-solved-{i}gram'))
NGrams.make(i, infile=LPath.data('baseline-rune-no-e'),
outfile=LPath.data(f'p-no-e-{i}gram'))
# make_translation(stream=False)
# make_ngrams(5)
# make_translation(stream=False)
# make_ngrams(5)

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from NGrams import NGrams
from RuneText import RUNES
from Alphabet import RUNES
def normalized_probability(int_prob):

83
LP/Rune.py Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from Alphabet import RUNES, white_rune, rune_map, primes_map
#########################################
# Rune : Stores a single rune. Incl. text, prime, index, and kind
#########################################
class Rune(object):
def __init__(self, r=None, t=None, i=None, p=None):
self._rune = r
self._text = t
self._index = i
self._prime = p
self._kind = None # one of: r n s l w
def __repr__(self):
return f'<{self._rune}, {self._text}, {self._index}, {self._prime}>'
@property
def rune(self):
if self._rune is None:
self._rune = RUNES[self._index] if self._index < 29 else ''
return self._rune
@property
def text(self):
if self._text is None:
r = self.rune
try:
self._text = rune_map[self.rune]
except KeyError:
self._text = white_rune.get(r, r)
return self._text
@property
def index(self):
if self._index is None:
r = self._rune
self._index = RUNES.index(r) if r in RUNES else 29
return self._index
@property
def prime(self):
if self._prime is None:
self._prime = primes_map.get(self.rune, 0)
return self._prime
@property
def kind(self):
if self._kind is None:
x = self.rune
if x in rune_map:
self._kind = 'r' # rune
elif x == '':
self._kind = 's' # paragraph, but treat as sentence
elif x == '':
self._kind = 's' # sentence
elif x == '\n' or x == '\r':
self._kind = 'l' # line end
elif x in '1234567890':
self._kind = 'n' # number
else:
self._kind = 'w' # whitespace (explicitly not n or s)
return self._kind
def __add__(self, o):
if isinstance(o, Rune):
o = o.index
if self.index == 29 or o == 29:
return self
return Rune(i=(self.index + o) % 29)
def __sub__(self, o):
if isinstance(o, Rune):
o = o.index
if self.index == 29 or o == 29:
return self
return Rune(i=(self.index - o) % 29)
def __invert__(self):
return self if self.index == 29 else Rune(i=28 - self.index)

View File

@@ -1,245 +0,0 @@
#!/usr/bin/env python3
import sys
from RuneText import RuneText
import lib as utils
#########################################
# RuneWriter : handle std output with highlight etc.
#########################################
class RuneWriter(object):
MARKS = {'_': '\x1b[0m', 'r': '\x1b[31;01m', 'b': '\x1b[30;01m'}
def __init__(self):
self.COLORS = True
self.VERBOSE = '-v' in sys.argv
self.QUIET = '-q' in sys.argv
self.BREAK_MODE = None
self.file_output = None
self.clear()
def clear(self):
self.mark = False
self.alternate = False
self._marked = ['_'] * 4
self.txt = [''] * 4
def is_empty(self):
return not self.txt[0]
def line_break_mode(self):
if self.BREAK_MODE is not None: # if set by user
return self.BREAK_MODE
return 'l' if self.VERBOSE else 's' # dynamically adapt to mode
def write(self, r=None, t=None, n1=None, n2=None):
m = ('b' if self.alternate else 'r') if self.mark else '_'
for i, v in enumerate([r, t, n1, n2]):
if v is None:
continue
if self.COLORS and self._marked[i] != m and i != 3:
self._marked[i] = m
prfx = self.MARKS[m]
else:
prfx = ''
self.txt[i] += prfx + v
# def rm(self, r=0, t=0, n1=0, n2=0):
# for i, v in enumerate([r, t, n1, n2]):
# if v > 0:
# self.txt[i] = self.txt[i][:-v]
def stdout(self):
def print_f(x=''):
if self.file_output:
with open(self.file_output, 'a') as f:
f.write(x + '\n')
else:
print(x)
if self.is_empty():
return
m = self.mark
self.mark = False # flush closing color
self.write(r='', t='', n1='', n2='')
self.mark = m
if not self.QUIET or self.VERBOSE:
print_f()
if not self.QUIET:
print_f(self.txt[0])
print_f(self.txt[1])
if self.VERBOSE:
print_f(self.txt[2])
print_f(self.txt[3])
self.clear()
#########################################
# RuneReader : handles parsing of the file and line breaks etc.
#########################################
class RuneReader(object):
def __init__(self):
self.data = None
self.loaded_file = None
self.words = {x: [] for x in range(20)} # increase for longer words
def load(self, data=None, file=None, limit=None):
self.loaded_file = None
if not data:
with open(file, 'r') as f:
data = f.read()[:limit]
self.loaded_file = file
self.data = data if isinstance(data, RuneText) else RuneText(data)
self.generate_word_list()
def has_data(self):
if len(self.data) > 0:
return True
return False
def runes_no_whitespace(self):
return [x for x in self.data if x.kind == 'r']
def generate_word_list(self):
for x in self.words.values():
x.clear()
res = []
ai = 0
ari = 0
zri = 0
for zi, x in enumerate(self.data):
if x.kind == 'l':
continue
elif x.kind == 'r':
res.append(x)
zri += 1
else:
if len(res) > 0:
xt = RuneText(res)
self.words[len(xt)].append((ai, zi, ari, zri, xt))
res = []
ai = zi
ari = zri
# count_callback('c|w|l', count, is-first-flag)
def parse(self, rune_fn, count_fn, whitespace_fn, break_line_on='l'):
word_sum = 0
line_sum = 0
for i, x in enumerate(self.data):
if x.kind == 'r':
r = rune_fn(self.data, i, word_sum == 0)
count_fn('c', r.prime, word_sum == 0)
word_sum += r.prime
elif x.kind == 'l' and x.kind != break_line_on:
continue # ignore all \n,\r if not forced explicitly
else:
if word_sum > 0:
count_fn('w', word_sum, line_sum == 0)
line_sum += word_sum
word_sum = 0
if x.kind != 'l': # still ignore \n,\r
whitespace_fn(x)
if x.kind == break_line_on:
count_fn('l', line_sum, line_sum == 0)
line_sum = 0
if word_sum > 0:
count_fn('w', word_sum, line_sum == 0)
line_sum += word_sum
if line_sum > 0:
count_fn('l', line_sum, True)
#########################################
# RuneRunner : Merge RuneWriter and RuneReader and stay in sync
#########################################
class RuneRunner(object):
def __init__(self):
self.input = RuneReader()
self.output = RuneWriter()
self.marked_chars = []
self.mark_alternate = []
self.next_mark = False
self.fn_cipher = None
def highlight_words_with_len(self, search_length):
found = [x for x in self.input.words[search_length]]
self.marked_chars = set(x for fp in found for x in range(fp[0], fp[1]))
return found
def highlight_rune(self, rune, mark_occurrences=[]):
ip = 0
tp = 0
ret = []
for i, x in enumerate(self.input.data):
if x.kind == 'r':
if x.rune == rune:
ip += 1
ret.append((ip, tp, i, ip in mark_occurrences))
tp += 1
self.marked_chars = set(i for _, _, i, _ in ret)
self.mark_alternate = set(i for _, _, i, f in ret if not f)
return ret
def reset_highlight(self):
self.marked_chars = []
self.mark_alternate = []
def start(self, fn_cipher):
self.fn_cipher = fn_cipher
self.next_mark = False
self.input.parse(
self.rune_callback, self.count_callback, self.whitespace_callback,
self.output.line_break_mode())
def rune_callback(self, encrypted_data, index, is_first):
if self.output.VERBOSE:
fillup = len(self.output.txt[2]) - len(self.output.txt[1])
if not is_first:
fillup += 1 # +1 cause n1 will add a '+'
if fillup > 0:
self.output.write(t=' ' * fillup)
if self.marked_chars:
x = encrypted_data[index] # always search on original data
mt = index in self.marked_chars
mn = index + 1 in self.marked_chars
self.output.alternate = index in self.mark_alternate
else:
x, mt, mn = self.fn_cipher(encrypted_data, index)
self.output.mark = mt
self.output.write(r=x.rune, t=x.text)
self.next_mark = mn
return x
def count_callback(self, typ, num, is_first):
if typ == 'c': # char
if self.output.VERBOSE:
self.output.write(n1=('' if is_first else '+') + str(num))
return
prm = utils.is_prime(num)
if typ == 'w': # word
tt = ('' if is_first else ' + ') + str(num) + ('*' if prm else '')
self.output.write(n2=tt)
if prm and num > 109 and not (self.output.VERBOSE or self.output.QUIET):
self.output.write(t='__')
elif typ == 'l': # line end (ignoring \n if mode is set to 's')
self.output.mark = False
# if not is_first:
sffx = ' = {}'.format(num) + ('*' if prm else '')
if utils.is_emirp(num):
sffx += ''
if self.output.VERBOSE:
self.output.write(n2=sffx)
elif not self.output.QUIET:
self.output.write(t=sffx)
self.output.stdout()
def whitespace_callback(self, rune):
if not self.next_mark: # dont mark whitespace after selection
self.output.mark = False
self.output.write(r=rune.rune, t=rune.text)
if self.output.VERBOSE:
self.output.write(n1=' ')

View File

@@ -1,16 +1,15 @@
#!/usr/bin/env python3
from RuneRunner import RuneRunner
# -*- coding: UTF-8 -*-
from RuneText import Rune, RuneText
from lib import affine_decrypt
from utils import affine_decrypt
#########################################
# RuneSolver : Generic parent class handles interrupts and text highlight
#########################################
class RuneSolver(RuneRunner):
class RuneSolver(object):
def __init__(self):
super().__init__()
self.reset()
def reset(self):
@@ -20,43 +19,31 @@ 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)
self.interrupt_counter = 0
self.start(self.cipher_callback)
def substitute_get(self, pos, keylen, search_term, found_term, all_data):
return found_term.zip_sub(search_term).description(count=True)
def cipher_callback(self, encrypted_data, index):
obj = encrypted_data[index]
is_interrupt = obj.rune == self.INTERRUPT
if is_interrupt:
self.interrupt_counter += 1
skip = is_interrupt and self.interrupt_counter in self.INTERRUPT_POS
mark_this = self.mark_char_at(index)
if not skip:
obj = self.cipher(obj, (index, encrypted_data))
mark_next = self.mark_char_at(index)
return obj, mark_this, mark_next
def enum_data(self, data):
irp_i = 0
r_pos = -1
for i, obj in enumerate(data):
skip = obj.index == 29
if not skip:
r_pos += 1
is_interrupt = obj.rune == self.INTERRUPT
if is_interrupt:
irp_i += 1
skip = is_interrupt and irp_i in self.INTERRUPT_POS
yield obj, i, r_pos, skip
def cipher(self, rune, context):
raise NotImplementedError # must subclass
def mark_char_at(self, position):
return False
def run(self, data):
raise NotImplementedError('must subclass')
# return RuneText(), [(start-highlight, end-highlight), ...]
def __str__(self):
txt = f'DATA: {len(self.input.data) if self.input.data else 0} bytes'
if self.input.loaded_file:
txt += f' (file: {self.input.loaded_file})'
else:
txt += f' (manual input)'
return txt + f'\ninterrupt jumps: {self.INTERRUPT_POS}'
return f'interrupt: {self.INTERRUPT}, jumps: {self.INTERRUPT_POS}'
#########################################
@@ -66,22 +53,22 @@ class RuneSolver(RuneRunner):
class SequenceSolver(RuneSolver):
def __init__(self):
super().__init__()
self.seq_index = 0
self.reset()
def reset(self):
super().reset()
self.FN = None
def run(self, data=None):
self.seq_index = 0
def run(self, data):
assert(self.FN)
super().run(data=data)
def cipher(self, rune, context):
x = self.FN(self.seq_index, rune)
self.seq_index += 1
return x
seq_i = 0
ret = []
for rune, i, ri, skip in self.enum_data(data):
if not skip:
rune = self.FN(seq_i, rune)
seq_i += 1
ret.append(rune)
return RuneText(ret), []
def __str__(self):
return super().__str__() + f'\nf(x): {self.FN}'
@@ -99,59 +86,52 @@ class RunningKeySolver(RuneSolver):
def reset(self):
super().reset()
self.KEY_DATA = [] # the key material
self.KEY_INVERT = False # ABCD -> ZYXW
self.KEY_SHIFT = 0 # ABCD -> DABC
self.KEY_ROTATE = 0 # ABCD -> ZABC
self.KEY_OFFSET = 0 # ABCD -> __ABCD
self.KEY_POST_PAD = 0 # ABCD -> ABCD__
def run(self, data=None):
self.k_current_pos = 0
self.k_len = len(self.KEY_DATA)
self.k_full_len = self.KEY_OFFSET + self.k_len + self.KEY_POST_PAD
super().run(data=data)
def run(self, data):
k_len = len(self.KEY_DATA)
if k_len <= 0:
return data, []
k_full_len = self.KEY_OFFSET + k_len + self.KEY_POST_PAD
k_current_pos = 0
ret = []
highlight = [[0, 0]]
for rune, i, ri, skip in self.enum_data(data):
if not skip:
u = k_current_pos - self.KEY_OFFSET
if u < 0 or u >= k_len or self.KEY_DATA[u] == 29:
self.unmodified_callback(rune)
else:
key_i = (u + self.KEY_SHIFT) % k_len
decrypted = self.decrypt(rune.index, key_i)
rune = Rune(i=(decrypted - self.KEY_ROTATE) % 29)
if i == highlight[-1][1]:
highlight[-1][1] = i + 1
else:
highlight.append([i, i + 1])
# rotate_key
if k_full_len > 0: # e.g., for key invert without a key
k_current_pos = (k_current_pos + 1) % k_full_len
ret.append(rune)
if highlight[0][1] == 0:
highlight = highlight[1:]
return RuneText(ret), highlight
def mark_char_at(self, position):
return self.active_key_pos() != -1
def decrypt(self, rune_index, key_index):
raise NotImplementedError('must subclass')
def active_key_pos(self):
i = self.k_current_pos - self.KEY_OFFSET
if i >= 0 and i < self.k_len:
if self.KEY_DATA[i] != 29: # placeholder for unknown
return i
return -1
def unmodified_callback(self, rune_index):
pass # subclass if needed
def cipher(self, rune, context):
r_idx = rune.index
if self.KEY_INVERT:
r_idx = 28 - r_idx
pos = self.active_key_pos()
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
if self.k_full_len > 0: # e.g., for key invert without a key
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): # 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
def key__str__basic_runes(self):
def key__str__(self): # you should override this
return RuneText(self.KEY_DATA).description(indexWhitespace=True)
def __str__(self):
txt = super().__str__()
txt += f'\nkey: {self.key__str__()}'
txt += f'\nkey invert: {self.KEY_INVERT}'
txt += f'\nkey offset: {self.KEY_OFFSET} runes'
txt += f'\nkey post pad: {self.KEY_POST_PAD} runes'
txt += f'\nkey shift: {self.KEY_SHIFT} indices'
@@ -170,15 +150,12 @@ class VigenereSolver(RunningKeySolver):
def substitute_supports_keylen(self):
return True
def substitute_get(self, pos, keylen, search_term, found_term):
def substitute_get(self, pos, keylen, search_term, found_term, all_data):
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()
#########################################
# AffineSolver : Decrypt runes with an array of (s, t) affine keys
@@ -188,42 +165,55 @@ class AffineSolver(RunningKeySolver):
def decrypt(self, rune_index, key_index):
return affine_decrypt(rune_index, self.KEY_DATA[key_index])
def key__str__(self):
return self.KEY_DATA
#########################################
# AutokeySolver : Decrypts runes by using previously decrypted ones as input
#########################################
class AutokeySolver(RunningKeySolver):
def run(self, data=None):
def run(self, 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)
return super().run(data)
def decrypt(self, rune_index, _):
def decrypt(self, rune_index, key_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 unmodified_callback(self, rune_index):
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()
def substitute_get(self, pos, keylen, search_term, found_term, all_data):
data = all_data.index_no_white
ret = [Rune(r='')] * keylen
for o in range(len(search_term)):
plain = search_term[o]
plain = search_term[o].index
i = pos + o
while i >= 0:
plain = data[i] - plain
plain = (data[i] - plain) % 29
i -= keylen
ret[i + keylen] = plain
ret[i + keylen] = Rune(i=plain)
return RuneText(ret).description(count=True, index=False)
def key__str__(self):
return self.key__str__basic_runes()
if __name__ == '__main__':
slvr = VigenereSolver()
slvr.KEY_DATA = [1]
print(slvr)
txt = RuneText('hi there')
sol = slvr.run(txt)
print(sol[0].text)
sol, mark = slvr.run(txt)
print(sol.text)
slvr.KEY_DATA = [-1]
print(slvr.run(sol)[0].text)
print(mark)

View File

@@ -1,112 +1,7 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re # load_indices
white_rune = {'': ' ', '': '.', '': ',', '': ';', '': '#'}
white_text = {v: k for k, v in white_rune.items()}
alphabet = [ # Using last value for display. Custom added: V
(2, '', ['F']), (3, '', ['V', 'U']), (5, '', ['TH']), (7, '', ['O']),
(11, '', ['R']), (13, '', ['K', 'C']), (17, '', ['G']),
(19, '', ['W']), (23, '', ['H']), (29, '', ['N']), (31, '', ['I']),
(37, '', ['J']), (41, '', ['EO']), (43, '', ['P']), (47, '', ['X']),
(53, '', ['Z', 'S']), (59, '', ['T']), (61, '', ['B']),
(67, '', ['E']), (71, '', ['M']), (73, '', ['L']),
(79, '', ['ING', 'NG']), (83, '', ['OE']), (89, '', ['D']),
(97, '', ['A']), (101, '', ['AE']), (103, '', ['Y']),
(107, '', ['IO', 'IA']), (109, '', ['EA'])
]
text_map = {t: r for _, r, ta in alphabet for t in ta}
rune_map = {r: t for _, r, ta in alphabet for t in ta}
primes_map = {r: p for p, r, _ in alphabet}
RUNES = [r for _, r, _ in alphabet] # array already sorted
re_norune = re.compile('[^' + ''.join(RUNES) + ']')
# del alphabet # used in playground for GP display
#########################################
# Rune : Stores a single rune. Incl. text, prime, index, and kind
#########################################
class Rune(object):
def __init__(self, r=None, t=None, i=None, p=None):
self._rune = r
self._text = t
self._index = i
self._prime = p
self._kind = None # one of: r n s l w
def __repr__(self):
return f'<{self._rune}, {self._text}, {self._index}, {self._prime}>'
@property
def rune(self):
if self._rune is None:
self._rune = RUNES[self._index] if self._index < 29 else ''
return self._rune
@property
def text(self, sameWhitespace=False):
if self._text is None:
if sameWhitespace:
self._text = rune_map.get(self.rune, ' ')
else:
r = self.rune
self._text = rune_map.get(r, white_rune.get(r, r))
return self._text
@property
def index(self):
if self._index is None:
r = self._rune
self._index = RUNES.index(r) if r in RUNES else 29
return self._index
@property
def prime(self):
if self._prime is None:
self._prime = primes_map.get(self.rune, 0)
return self._prime
@property
def kind(self):
if self._kind is None:
x = self.rune
if x in rune_map:
self._kind = 'r' # rune
elif x == '':
self._kind = 's' # paragraph, but treat as sentence
elif x == '':
self._kind = 's' # sentence
elif x == '\n' or x == '\r':
self._kind = 'l' # line end
elif x in '1234567890':
self._kind = 'n' # number
else:
self._kind = 'w' # whitespace (explicitly not n or s)
return self._kind
def __add__(self, o):
if isinstance(o, Rune):
o = o.index
if self.index == 29 or o == 29:
return self
return Rune(i=(self.index + o) % 29)
def __sub__(self, o):
if isinstance(o, Rune):
o = o.index
if self.index == 29 or o == 29:
return self
return Rune(i=(self.index - o) % 29)
def __radd__(self, o):
return self if self.index == 29 else Rune(i=(o + self.index) % 29)
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)
from Alphabet import white_rune, white_text, rune_map, text_map
from Rune import Rune
#########################################
@@ -139,16 +34,6 @@ class RuneText(object):
self._data_len = len(self._data)
def __len__(self):
return self._data_len
def trim(self, maxlen):
if self._data_len > maxlen:
if self._rune_sum and self._rune_sum > 0:
self._rune_sum -= sum(x.prime for x in self._data[maxlen:])
self._data = self._data[:maxlen]
self._data_len = maxlen
@classmethod
def from_text(self, text):
res = []
@@ -186,17 +71,29 @@ class RuneText(object):
res.append(Rune(r=rune, t=char))
return res
def description(self, count=False, index=True, indexWhitespace=False):
return None if len(self) == 0 else \
self.rune + (f' ({len(self)})' if count else '') + ' - ' + \
self.text + (f' ({len(self.text)})' if count else '') + \
(f' - {self.index if indexWhitespace else self.index_rune_only}'
if index else '')
def __len__(self):
return self._data_len
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]
else:
return self._data[key]
# def __setitem__(self, key, value):
# self._data[key] = value
def __add__(self, other):
return RuneText([x + other for x in self._data])
def __sub__(self, other):
return RuneText([x - other for x in self._data])
def __invert__(self):
return RuneText([~x for x in self._data])
def __str__(self):
return f'RuneText<{len(self)}>'
@property
def text(self):
@@ -207,11 +104,11 @@ class RuneText(object):
return ''.join(x.rune for x in self._data)
@property
def index(self):
def index_no_newline(self):
return [x.index for x in self._data if x.kind != 'l']
@property
def index_rune_only(self):
def index_no_white(self):
return [x.index for x in self._data if x.index != 29]
@property
@@ -224,50 +121,82 @@ class RuneText(object):
self._rune_sum = sum(self.prime)
return self._rune_sum
def __getitem__(self, key):
if isinstance(key, str):
return [getattr(x, key) for x in self._data]
else:
return self._data[key]
@property
def data_clean(self):
return [x if x.kind == 'r' else Rune(i=29)
for x in self._data if x.kind != 'l']
def __setitem__(self, key, value):
self._data[key] = value
def description(self, count=False, index=True, indexWhitespace=False):
return None if len(self) == 0 else \
self.rune + (f' ({len(self)})' if count else '') + ' - ' + \
self.text + (f' ({len(self.text)})' if count else '') + \
(' - {}'.format(self.index_no_newline if indexWhitespace else
self.index_no_white)
if index else '')
def __add__(self, other):
return RuneText([x + other for x in self._data])
def trim(self, maxlen):
if self._data_len > maxlen:
if self._rune_sum and self._rune_sum > 0:
self._rune_sum -= sum(x.prime for x in self._data[maxlen:])
self._data = self._data[:maxlen]
self._data_len = maxlen
def __sub__(self, other):
return RuneText([x - other for x in self._data])
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 __radd__(self, other):
return RuneText([other + x for x in self._data])
# def equal(self, other):
# if len(self) != len(other):
# return False
# return all(x.index == y.index for x, y in zip(self, other))
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)}>'
#########################################
# load page and convert to indices for faster access
#########################################
def load_indices(fname, interrupt, maxinterrupt=None, minlen=None, limit=None):
with open(fname, 'r') as f:
data = RuneText(re_norune.sub('', f.read())).index_rune_only[:limit]
if maxinterrupt is not None:
# incl. everything up to but not including next interrupt
# e.g., maxinterrupt = 0 will return text until first interrupt
for i, x in enumerate(data):
if x != interrupt:
def enum_words(self): # [(start, end, len), ...] may include \n \r
start = 0
r_pos = 0
word = []
for i, x in enumerate(self._data):
if x.kind == 'r':
r_pos += 1
word.append(x)
elif x.kind == 'l':
continue
if maxinterrupt == 0:
if minlen and i < minlen:
continue
return data[:i]
maxinterrupt -= 1
return data
else:
if len(word) > 0:
yield start, i, r_pos - len(word), RuneText(word)
word = []
start = i + 1
class RuneTextFile(RuneText):
def __init__(self, file, limit=None):
with open(file, 'r') as f:
super().__init__(f.read()[:limit])
self.inverted = False
self.loaded_file = file
def reopen(self, limit=None):
ret = RuneTextFile(self.loaded_file, limit)
if self.inverted:
ret.invert()
return ret
def invert(self):
self.inverted = not self.inverted
self._rune_sum = None
self._data = [~x for x in self._data]
def __str__(self):
return '@file: {} ({} bytes), inverted: {}'.format(
self.loaded_file, len(self._data), self.inverted)
if __name__ == '__main__':
x = RuneText('Hi there. And welc\nome, to my "world";')
for a, z, r_pos, word in x.enum_words():
print((a, z), r_pos, word.text)
y = RuneTextFile(file='../_input.txt')
print(y.loaded_file)
print(y.prime_sum)
print(y)

View File

@@ -1,17 +1,24 @@
import sys
if True:
sys.path.append(__path__[0])
if __name__ != '__main__':
sys.path.insert(0, __path__[0])
import lib as utils
import utils
from LPath import FILES_ALL, FILES_UNSOLVED, FILES_SOLVED
from LPath import LPath as path
from RuneSolver import VigenereSolver, AffineSolver, AutokeySolver, SequenceSolver
from RuneText import Rune, RuneText
from RuneText import RUNES, alphabet, load_indices
from HeuristicSearch import GuessVigenere, GuessAffine, GuessPattern
from HeuristicSearch import SearchInterrupt
from HeuristicLib import Probability
from Alphabet import RUNES, alphabet
from Rune import Rune
from RuneText import RuneText, RuneTextFile
from IOReader import load_indices, longest_no_interrupt
from IOWriter import IOWriter
from RuneSolver import SequenceSolver, VigenereSolver, AffineSolver, AutokeySolver
from KeySearch import GuessVigenere, GuessAffine, GuessPattern
from Probability import Probability
from InterruptDB import InterruptDB
from InterruptIndices import InterruptIndices
from InterruptSearch import InterruptSearch
from FailedAttempts import NGramShifter

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import math
@@ -113,7 +114,9 @@ def autokey_reverse(data, keylen, pos, search_term):
ret[i + keylen] = plain
return ret
# alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
# cipher = 'YDIDWYASDDJVAPJMMBIASDTJVAMD'
# indices = [affine_decrypt(alphabet.index(x), (5, 9), 26) for x in cipher]
# print(''.join(alphabet[x] for x in indices))
if __name__ == '__main__':
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
cipher = 'YDIDWYASDDJVAPJMMBIASDTJVAMD'
indices = [affine_decrypt(alphabet.index(x), (5, 9), 26) for x in cipher]
print(''.join(alphabet[x] for x in indices))