Coverage for kryptic_cypher/cypher/bacons.py: 93%
69 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-20 18:52 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-20 18:52 +0000
1import string
3from kryptic_cypher.cypher.base import CypherResult, CypherWithKey, ValidationResult
5characters = {
6 "A": 0,
7 "B": 1,
8 "C": 2,
9 "D": 3,
10 "E": 4,
11 "F": 5,
12 "G": 6,
13 "H": 7,
14 "I": 8,
15 "J": 8,
16 "K": 9,
17 "L": 10,
18 "M": 11,
19 "N": 12,
20 "O": 13,
21 "P": 14,
22 "Q": 15,
23 "R": 16,
24 "S": 17,
25 "T": 18,
26 "U": 19,
27 "V": 19,
28 "W": 20,
29 "X": 21,
30 "Y": 22,
31 "Z": 23,
32}
35def sanitize_message(message: str) -> str:
36 new_message = ""
37 for letter in message.upper():
38 if string.ascii_letters.find(letter) != -1:
39 new_message += letter
40 return new_message
43class BaconsCypher(CypherWithKey):
44 """
45 A keyed cypher that encodes only text using a key to encode the binary value of a letter with a 5 bit value.
46 The key's casing is changed to upper or lower depending on the binary value i.e. upper = 1, lower = 0.
48 The following table defines what each binary 5 bit value means for each letter:
50 | Letter | Code | Int Value |
51 | ------ | ---- | --------- |
52 | A | 00000 | 0 |
53 | B | 00001 | 1 |
54 | C | 00010 | 2 |
55 | D | 00011 | 3 |
56 | E | 00100 | 4 |
57 | F | 00101 | 5 |
58 | G | 00110 | 6 |
59 | H | 00111 | 7 |
60 | I,J | 01000 | 8 |
61 | K | 01001 | 9 |
62 | L | 01010 | 10 |
63 | M | 01011 | 11 |
64 | N | 01100 | 12 |
65 | O | 01101 | 13 |
66 | P | 01110 | 14 |
67 | Q | 01111 | 15 |
68 | R | 10000 | 16 |
69 | S | 10001 | 17 |
70 | T | 10010 | 18 |
71 | U,V | 10011 | 19 |
72 | W | 10100 | 20 |
73 | X | 10101 | 21 |
74 | Y | 10110 | 22 |
75 | Z | 10111 | 23 |
77 **Examples**
79 'Hello World', 'Jared' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'jaRED jaRed jArEd jArEd jAReD JaRed jAReD Jared jArEd jarED'
80 'Hello World', 'sm' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'sm SM Sm sM sm sM sM'
81 'Hello World', 'supercoolkeythatwillbeawesome' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'suPERcoOlkeYtHatWiLlbEAwESoMe suPErCOolkeyThAtwilLB'
82 """
84 @staticmethod
85 def get_name() -> str:
86 return __name__.split(".")[-1]
88 def validate_key(self, key: str) -> ValidationResult:
89 if key is None or len(key) == 0:
90 return ValidationResult.fail("Key cannot be empty.")
92 if len([c for c in key if c in string.ascii_letters]) < 2:
93 return ValidationResult.fail(
94 "Key must be at least 2 characters long with proper ascii letters."
95 )
96 return ValidationResult.ok()
98 def encode(self, text: str, key: str) -> CypherResult:
99 # Remove unsupported characters
100 sanitized = sanitize_message(text)
102 # Is there anything to encode?
103 if len(sanitized) == 0:
104 return CypherResult.fail(sanitized, "Message cannot be empty.")
106 key_index = 0
107 encoded = ""
109 # Encode each character
110 for character in sanitized:
111 value = characters[character]
113 # Turn the value into a 5 bit string
114 for c in f"{value:05b}":
116 # Check to see if we've reached the end of the key
117 if key_index >= len(key):
118 key_index = 0
119 encoded += " "
121 # Get the character from the key
122 visual = key[key_index]
123 while visual not in string.ascii_letters:
124 if visual in string.whitespace:
125 encoded += visual
126 key_index += 1
127 if key_index >= len(key):
128 key_index = 0
129 encoded += " "
130 visual = key[key_index]
131 key_index += 1
133 # Determine casing
134 if c == "1":
135 encoded += visual.upper()
136 else:
137 encoded += visual.lower()
139 return CypherResult.ok(
140 sanitized,
141 encoded,
142 )
144 def decode(self, text: str, key: str) -> CypherResult:
145 pieces = text.split(" ")
146 # Check to make sure the text is encoded with the given key
147 if set(c.lower() for c in key if c in string.ascii_letters) != set(
148 "".join(pieces).lower()
149 ):
150 return CypherResult.fail(
151 text,
152 "Text is not encoded with the given key, cannot trust the decoded text.",
153 )
154 joined = "".join(pieces)
156 # Check that the text is a multiple of 5 since we encode 5 bits at a time
157 if len(joined) % 5 != 0:
158 return CypherResult.fail(
159 text,
160 "Text is not properly encoded into 5 bit values, cannot trust the decoded text.",
161 )
163 message = ""
164 for i in range(0, len(joined), 5):
165 section = joined[i : i + 5]
166 # Turn section into binary
167 binary = ""
168 for character in section:
169 if character.isupper():
170 binary += "1"
171 else:
172 binary += "0"
173 true_value = int(binary, 2)
175 # Deduce letter from binary and make sure to account for collisions
176 possible_keys = []
177 for key in characters.keys():
178 if characters[key] == true_value:
179 possible_keys.append(key)
181 # Add the letter to the output message
182 if len(possible_keys) == 1:
183 message += possible_keys[0]
184 else:
185 message += f"({', '.join(possible_keys)})"
187 # Return the final assmbled message
188 return CypherResult.ok(text, message)