# HG changeset patch # User Brian Neal # Date 1385598708 21600 # Node ID 9e44ad188f4aaae04dd3deb42665b7f48f66f7d9 # Parent 9e43a627ba5d3681476f04d8d969f7874bc21aa5 Started work on the Purple97 machine class. diff -r 9e43a627ba5d -r 9e44ad188f4a purple/machine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purple/machine.py Wed Nov 27 18:31:48 2013 -0600 @@ -0,0 +1,119 @@ +# Copyright (C) 2013 by Brian Neal. +# This file is part of purple, the PURPLE (Cipher Machine 97) simulation. +# purple is released under the MIT License (see LICENSE.txt). + +"""This module contains the Purple97 class, the top level class in the PURPLE +simulation. + +""" +from collections import Counter +import string + +import purple.switch as switch + + +class Purple97Error(Exception): + """Exception class for all Purple97 errors""" + + +class Purple97: + """This class simulates the top-level behavior of the PURPLE cipher + machine. + + """ + def __init__(self, switches_pos=None, fast_switch=1, middle_switch=2, + alphabet=None): + """Build a PURPLE (Cipher Machine 97) instance. Initial settings can be + optionally supplied. + + switches_pos: If not None, must be a 4-element list or tuple of integer + starting switch positions. Each position must be in the range of 0-24, + inclusive. If None, a list of all 0's is assumed. The first element in + the list is the starting position for the sixes switch. The second + through fourth elements are the starting positions for the three + twenties switches, 1 through 3. + + fast_switch: this integer parameter names which twenties switch (1-3) is + assuming the role of the fast switch. + + middle_switch: this integer parameter names which twenties switch (1-3) + has been designated as the middle switch. + + Passing in the same value for both the fast and medium switches will + raise a Purple97Error exception. The slow switch is assumed to be the + remaining twenties switch that was not named. + + alphabet: this parameter must be a 26-letter sequence that represents + the daily alphabet setting. It describes how the typewriters are wired + to the plugboard. The first six characters are the mapping for the sixes + switch, and the remaining 20 are for the input wiring for the first + stage of the twenties switches. If None is supplied, a straight through + mapping is assumed; i.e. "AEIOUYBCDFGHJKLMNPQRSTVWXZ". + + The alphabet parameter will accept either upper or lowercase letters. + All 26 distinct letters must be present or else a Purple97Error + exception will be raised. + + """ + # If no switch positions are supplied, default to all 0's + if switches_pos is None: + switches_pos = (0, 0, 0, 0) + + # Validate switch positions + try: + n = len(switches_pos) + except TypeError: + raise Purple97Error("switches_pos must be a sequence") + if n != 4: + raise Purple97Error("switches_pos must have length of 4") + + # Create switches with correct starting positions + self.sixes = switch.create_switch(switch.SIXES, switches_pos[0]) + self.twenties = [ + switch.create_switch(switch.TWENTIES_1, switches_pos[1]), + switch.create_switch(switch.TWENTIES_2, switches_pos[2]), + switch.create_switch(switch.TWENTIES_3, switches_pos[3]), + ] + + # Validate fast & middle switch parameters + if not (1 <= fast_switch <= 3): + raise Purple97Error("fast_switch out of range (1-3)") + if not (1 <= middle_switch <= 3): + raise Purple97Error("middle_switch out of range (1-3)") + if fast_switch == middle_switch: + raise Purple97Error("fast & middle switches cannot be the same") + + # Store references to the fast, middle, and slow switches + self.fast_switch = self.twenties[fast_switch - 1] + self.middle_switch = self.twenties[middle_switch - 1] + + # Pick the remaining switch as the slow switch + switches = [1, 2, 3] + switches.remove(fast_switch) + switches.remove(middle_switch) + self.slow_switch = self.twenties[switches[0] - 1] + + # Validate the alphabet + if alphabet is None: + alphabet = 'AEIOUYBCDFGHJKLMNPQRSTVWXZ' + + alphabet = alphabet.upper() + if len(alphabet) != 26: + raise Purple97Error("invalid alphabet length") + + # Count valid letters + ctr = Counter(string.ascii_uppercase) + for c in alphabet: + if c in ctr: + ctr[c] += 1 + + # At this point if alphabet is legal, all keys in ctr must have a value + # of 2. If any are 1, then alphabet was missing some letters. If any are + # greater than 2, we had duplicate letters. + + if not all(v == 2 for v in ctr.values()): + raise Purple97Error("invalid alphabet") + + self.alphabet = alphabet + + diff -r 9e43a627ba5d -r 9e44ad188f4a purple/tests/test_machine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purple/tests/test_machine.py Wed Nov 27 18:31:48 2013 -0600 @@ -0,0 +1,66 @@ +# Copyright (C) 2013 by Brian Neal. +# This file is part of purple, the PURPLE (Cipher Machine 97) simulation. +# purple is released under the MIT License (see LICENSE.txt). + +import string +import unittest + +from purple.machine import Purple97, Purple97Error +from purple.switch import SteppingSwitchError + + +class Purple97TestCase(unittest.TestCase): + + def test_construction(self): + + Purple97() + Purple97([0, 1, 2, 3]) + Purple97([0, 1, 2, 3], 1) + Purple97([0, 1, 2, 3], 2, 1) + Purple97((0, 1, 2, 3), 2, 1, string.ascii_uppercase) + Purple97((0, 1, 2, 3), 2, 1, string.ascii_lowercase) + Purple97(alphabet=string.ascii_lowercase) + Purple97(fast_switch=3, middle_switch=1) + + def test_construct_bad_positions(self): + + self.assertRaises(Purple97Error, Purple97, []) + self.assertRaises(Purple97Error, Purple97, [1]) + self.assertRaises(Purple97Error, Purple97, [1, 1, 1, 1, 1]) + self.assertRaises(SteppingSwitchError, Purple97, [1, 1, 1, 100]) + + def test_construct_bad_switches(self): + + self.assertRaises(Purple97Error, Purple97, fast_switch=0) + self.assertRaises(Purple97Error, Purple97, fast_switch=4) + self.assertRaises(Purple97Error, Purple97, fast_switch=-1) + + self.assertRaises(Purple97Error, Purple97, middle_switch=0) + self.assertRaises(Purple97Error, Purple97, middle_switch=4) + self.assertRaises(Purple97Error, Purple97, middle_switch=-1) + + self.assertRaises(Purple97Error, Purple97, fast_switch=2) + self.assertRaises(Purple97Error, Purple97, fast_switch=1, middle_switch=1) + self.assertRaises(Purple97Error, Purple97, fast_switch=2, middle_switch=2) + self.assertRaises(Purple97Error, Purple97, fast_switch=3, middle_switch=3) + + self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=1) + self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=0) + self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=4) + + def test_construct_bad_alphabet(self): + + alpha = '' + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = '1' + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = '!' * 26 + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = string.ascii_uppercase[3:] + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = string.ascii_uppercase + string.ascii_uppercase[4:10] + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = string.ascii_uppercase[:13] + string.ascii_uppercase[:13] + self.assertRaises(Purple97Error, Purple97, alpha) + alpha = 'M' * 26 + self.assertRaises(Purple97Error, Purple97, alpha)