codext 1.16.1__tar.gz → 1.16.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {codext-1.16.1 → codext-1.16.2}/.github/workflows/python-package.yml +1 -1
  2. {codext-1.16.1 → codext-1.16.2}/PKG-INFO +2 -1
  3. {codext-1.16.1 → codext-1.16.2}/README.md +1 -0
  4. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/crypto.md +19 -0
  5. codext-1.16.2/src/codext/VERSION.txt +1 -0
  6. {codext-1.16.1 → codext-1.16.2}/src/codext/base/_base.py +6 -1
  7. {codext-1.16.1 → codext-1.16.2}/src/codext/base/base45.py +5 -2
  8. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/__init__.py +1 -0
  9. codext-1.16.2/src/codext/crypto/phillips.py +128 -0
  10. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/PKG-INFO +2 -1
  11. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/SOURCES.txt +1 -0
  12. {codext-1.16.1 → codext-1.16.2}/tests/test_base.py +22 -1
  13. codext-1.16.1/src/codext/VERSION.txt +0 -1
  14. {codext-1.16.1 → codext-1.16.2}/.coveragerc +0 -0
  15. {codext-1.16.1 → codext-1.16.2}/.github/ISSUE_TEMPLATE/add-encoding.yml +0 -0
  16. {codext-1.16.1 → codext-1.16.2}/.github/copilot-instructions.md +0 -0
  17. {codext-1.16.1 → codext-1.16.2}/.github/prompts/add_codec.prompt.md +0 -0
  18. {codext-1.16.1 → codext-1.16.2}/.github/pull_request_template.md +0 -0
  19. {codext-1.16.1 → codext-1.16.2}/.gitignore +0 -0
  20. {codext-1.16.1 → codext-1.16.2}/.readthedocs.yml +0 -0
  21. {codext-1.16.1 → codext-1.16.2}/LICENSE +0 -0
  22. {codext-1.16.1 → codext-1.16.2}/docs/coverage.svg +0 -0
  23. {codext-1.16.1 → codext-1.16.2}/docs/mkdocs.yml +0 -0
  24. {codext-1.16.1 → codext-1.16.2}/docs/pages/cli.md +0 -0
  25. {codext-1.16.1 → codext-1.16.2}/docs/pages/css/extra.css +0 -0
  26. {codext-1.16.1 → codext-1.16.2}/docs/pages/demos/using-bases.gif +0 -0
  27. {codext-1.16.1 → codext-1.16.2}/docs/pages/demos/using-codext.gif +0 -0
  28. {codext-1.16.1 → codext-1.16.2}/docs/pages/demos/using-debase.gif +0 -0
  29. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/base.md +0 -0
  30. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/binary.md +0 -0
  31. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/common.md +0 -0
  32. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/compressions.md +0 -0
  33. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/hashing.md +0 -0
  34. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/languages.md +0 -0
  35. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/others.md +0 -0
  36. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/stegano.md +0 -0
  37. {codext-1.16.1 → codext-1.16.2}/docs/pages/enc/web.md +0 -0
  38. {codext-1.16.1 → codext-1.16.2}/docs/pages/features.md +0 -0
  39. {codext-1.16.1 → codext-1.16.2}/docs/pages/guessing.md +0 -0
  40. {codext-1.16.1 → codext-1.16.2}/docs/pages/howto.md +0 -0
  41. {codext-1.16.1 → codext-1.16.2}/docs/pages/img/banner.png +0 -0
  42. {codext-1.16.1 → codext-1.16.2}/docs/pages/img/icon.png +0 -0
  43. {codext-1.16.1 → codext-1.16.2}/docs/pages/img/logo.png +0 -0
  44. {codext-1.16.1 → codext-1.16.2}/docs/pages/index.md +0 -0
  45. {codext-1.16.1 → codext-1.16.2}/docs/pages/manipulations.md +0 -0
  46. {codext-1.16.1 → codext-1.16.2}/docs/requirements.txt +0 -0
  47. {codext-1.16.1 → codext-1.16.2}/pyproject.toml +0 -0
  48. {codext-1.16.1 → codext-1.16.2}/pytest.ini +0 -0
  49. {codext-1.16.1 → codext-1.16.2}/requirements.txt +0 -0
  50. {codext-1.16.1 → codext-1.16.2}/setup.cfg +0 -0
  51. {codext-1.16.1 → codext-1.16.2}/src/codext/__common__.py +0 -0
  52. {codext-1.16.1 → codext-1.16.2}/src/codext/__info__.py +0 -0
  53. {codext-1.16.1 → codext-1.16.2}/src/codext/__init__.py +0 -0
  54. {codext-1.16.1 → codext-1.16.2}/src/codext/base/__init__.py +0 -0
  55. {codext-1.16.1 → codext-1.16.2}/src/codext/base/_base2n.py +0 -0
  56. {codext-1.16.1 → codext-1.16.2}/src/codext/base/base100.py +0 -0
  57. {codext-1.16.1 → codext-1.16.2}/src/codext/base/base122.py +0 -0
  58. {codext-1.16.1 → codext-1.16.2}/src/codext/base/base85.py +0 -0
  59. {codext-1.16.1 → codext-1.16.2}/src/codext/base/base91.py +0 -0
  60. {codext-1.16.1 → codext-1.16.2}/src/codext/base/baseN.py +0 -0
  61. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/__init__.py +0 -0
  62. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/baudot.py +0 -0
  63. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/bcd.py +0 -0
  64. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/excess3.py +0 -0
  65. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/gray.py +0 -0
  66. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/manchester.py +0 -0
  67. {codext-1.16.1 → codext-1.16.2}/src/codext/binary/rotate.py +0 -0
  68. {codext-1.16.1 → codext-1.16.2}/src/codext/checksums/__init__.py +0 -0
  69. {codext-1.16.1 → codext-1.16.2}/src/codext/checksums/adler.py +0 -0
  70. {codext-1.16.1 → codext-1.16.2}/src/codext/checksums/crc.py +0 -0
  71. {codext-1.16.1 → codext-1.16.2}/src/codext/checksums/luhn.py +0 -0
  72. {codext-1.16.1 → codext-1.16.2}/src/codext/common/__init__.py +0 -0
  73. {codext-1.16.1 → codext-1.16.2}/src/codext/common/a1z26.py +0 -0
  74. {codext-1.16.1 → codext-1.16.2}/src/codext/common/cases.py +0 -0
  75. {codext-1.16.1 → codext-1.16.2}/src/codext/common/dummy.py +0 -0
  76. {codext-1.16.1 → codext-1.16.2}/src/codext/common/octal.py +0 -0
  77. {codext-1.16.1 → codext-1.16.2}/src/codext/common/ordinal.py +0 -0
  78. {codext-1.16.1 → codext-1.16.2}/src/codext/compressions/__init__.py +0 -0
  79. {codext-1.16.1 → codext-1.16.2}/src/codext/compressions/gzipp.py +0 -0
  80. {codext-1.16.1 → codext-1.16.2}/src/codext/compressions/lz77.py +0 -0
  81. {codext-1.16.1 → codext-1.16.2}/src/codext/compressions/lz78.py +0 -0
  82. {codext-1.16.1 → codext-1.16.2}/src/codext/compressions/pkzip.py +0 -0
  83. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/affine.py +0 -0
  84. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/atbash.py +0 -0
  85. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/bacon.py +0 -0
  86. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/barbie.py +0 -0
  87. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/citrix.py +0 -0
  88. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/polybius.py +0 -0
  89. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/railfence.py +0 -0
  90. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/rot.py +0 -0
  91. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/scytale.py +0 -0
  92. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/shift.py +0 -0
  93. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/vigenere.py +0 -0
  94. {codext-1.16.1 → codext-1.16.2}/src/codext/crypto/xor.py +0 -0
  95. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/__init__.py +0 -0
  96. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/blake.py +0 -0
  97. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/crypt.py +0 -0
  98. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/md.py +0 -0
  99. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/mmh3.py +0 -0
  100. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/sha.py +0 -0
  101. {codext-1.16.1 → codext-1.16.2}/src/codext/hashing/shake.py +0 -0
  102. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/__init__.py +0 -0
  103. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/braille.py +0 -0
  104. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/galactic.py +0 -0
  105. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/ipsum.py +0 -0
  106. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/leetspeak.py +0 -0
  107. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/morse.py +0 -0
  108. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/navajo.py +0 -0
  109. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/radio.py +0 -0
  110. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/southpark.py +0 -0
  111. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/tap.py +0 -0
  112. {codext-1.16.1 → codext-1.16.2}/src/codext/languages/tomtom.py +0 -0
  113. {codext-1.16.1 → codext-1.16.2}/src/codext/macros.json +0 -0
  114. {codext-1.16.1 → codext-1.16.2}/src/codext/others/__init__.py +0 -0
  115. {codext-1.16.1 → codext-1.16.2}/src/codext/others/dna.py +0 -0
  116. {codext-1.16.1 → codext-1.16.2}/src/codext/others/kbshift.py +0 -0
  117. {codext-1.16.1 → codext-1.16.2}/src/codext/others/letters.py +0 -0
  118. {codext-1.16.1 → codext-1.16.2}/src/codext/others/markdown.py +0 -0
  119. {codext-1.16.1 → codext-1.16.2}/src/codext/others/uuencode.py +0 -0
  120. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/__init__.py +0 -0
  121. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/hexagram.py +0 -0
  122. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/klopf.py +0 -0
  123. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/resistor.py +0 -0
  124. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/rick.py +0 -0
  125. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/sms.py +0 -0
  126. {codext-1.16.1 → codext-1.16.2}/src/codext/stegano/whitespace.py +0 -0
  127. {codext-1.16.1 → codext-1.16.2}/src/codext/web/__init__.py +0 -0
  128. {codext-1.16.1 → codext-1.16.2}/src/codext/web/html.py +0 -0
  129. {codext-1.16.1 → codext-1.16.2}/src/codext/web/url.py +0 -0
  130. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/dependency_links.txt +0 -0
  131. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/entry_points.txt +0 -0
  132. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/requires.txt +0 -0
  133. {codext-1.16.1 → codext-1.16.2}/src/codext.egg-info/top_level.txt +0 -0
  134. {codext-1.16.1 → codext-1.16.2}/tests/__init__.py +0 -0
  135. {codext-1.16.1 → codext-1.16.2}/tests/test_common.py +0 -0
  136. {codext-1.16.1 → codext-1.16.2}/tests/test_generated.py +0 -0
  137. {codext-1.16.1 → codext-1.16.2}/tests/test_manual.py +0 -0
@@ -65,7 +65,7 @@ jobs:
65
65
  - uses: actions/checkout@v5
66
66
  with:
67
67
  fetch-depth: 0
68
- ref: ${{ github.head_ref || github.ref_name }}
68
+ ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
69
69
  - name: Set up Python ${{ env.python_version }}
70
70
  uses: actions/setup-python@v6
71
71
  with:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codext
3
- Version: 1.16.1
3
+ Version: 1.16.2
4
4
  Summary: Native codecs extension
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1034,6 +1034,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
1034
1034
  - [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
1035
1035
  - [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
1036
1036
  - [X] `citrix`: aka Citrix CTX1 password encoding
1037
+ - [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
1037
1038
  - [X] `polybius`: aka Polybius Square Cipher
1038
1039
  - [X] `railfence`: aka Rail Fence Cipher
1039
1040
  - [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])
@@ -336,6 +336,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
336
336
  - [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
337
337
  - [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
338
338
  - [X] `citrix`: aka Citrix CTX1 password encoding
339
+ - [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
339
340
  - [X] `polybius`: aka Polybius Square Cipher
340
341
  - [X] `railfence`: aka Rail Fence Cipher
341
342
  - [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])
@@ -162,6 +162,25 @@ This implements the Citrix CTX1 password encoding algorithm.
162
162
 
163
163
  -----
164
164
 
165
+ ### Phillips Cipher
166
+
167
+ This implements Phillips cipher, a polyalphabetic code using 8 grids generated with one keyword.
168
+
169
+ **Codec** | **Conversions** | **Aliases** | **Comment**
170
+ :---: | :---: | --- | ---
171
+ `phillips` | text <-> phillips ciphertext | `phillips-key`, `phillips_password`, ... |
172
+
173
+ ```python
174
+ >>> codext.encode("this is a test", "phillips_mysuperkey")
175
+ 'ZCNM NM E XKMVZ'
176
+ >>> codext.encode("Another Test String", "phillips-PaSsWoRd")
177
+ 'SMEZKBE LBON OLEHQHV'
178
+ >>> codext.decode("SMEZKBE LBON OLEHQHV", "phillips-password")
179
+ 'ANOTHER TEST STRINGX'
180
+ ```
181
+
182
+ -----
183
+
165
184
  ### Polybius Square Cipher
166
185
 
167
186
  This implements the well-known Polybius Square cipher, using the square with the alphabet in normal order as the default. It can be used dynamically with a custom alphabet.
@@ -0,0 +1 @@
1
+ 1.16.2
@@ -130,6 +130,10 @@ def base_encode(input, charset, errors="strict", exc=BaseEncodeError):
130
130
  while i > 0:
131
131
  i, c = divmod(i, n)
132
132
  r = charset[c] + r
133
+ # preserve leading zero bytes: big-integer bases such as Base58 map each
134
+ # leading null byte of the input to a leading charset[0] character
135
+ if not isinstance(input, int):
136
+ r = charset[0] * (len(input) - len(input.lstrip("\x00"))) + r
133
137
  return r
134
138
 
135
139
 
@@ -151,7 +155,8 @@ def base_decode(input, charset, errors="strict", exc=BaseDecodeError):
151
155
  i = i * n + charset.index(c)
152
156
  except ValueError:
153
157
  handle_error("base", errors, exc, decode=True)(c, k, dec(i), "base%d" % n)
154
- return dec(i)
158
+ # restore the leading zero bytes encoded as leading charset[0] characters
159
+ return chr(0) * (len(input) - len(input.lstrip(charset[0]))) + dec(i)
155
160
 
156
161
 
157
162
  # base codec factory functions
@@ -34,7 +34,10 @@ def base45_encode(mode):
34
34
  b45 = _get_charset(B45, mode)
35
35
  def encode(text, errors="strict"):
36
36
  t, s = b(text), ""
37
- for i in range(0, len(text), 2):
37
+ # iterate over the byte sequence (t), not len(text): when the input
38
+ # holds non-ASCII characters, b(text) is longer than text and using
39
+ # len(text) silently drops the trailing bytes
40
+ for i in range(0, len(t), 2):
38
41
  n = 256 * __ord(t[i])
39
42
  try:
40
43
  n += __ord(t[i+1])
@@ -54,7 +57,7 @@ def base45_decode(mode):
54
57
  def decode(text, errors="strict"):
55
58
  t, s = b(text), ""
56
59
  ehandler = handle_error("base45", errors, decode=True)
57
- for i in range(0, len(text), 3):
60
+ for i in range(0, len(t), 3):
58
61
  try:
59
62
  n = b45[__chr(t[i])]
60
63
  except KeyError:
@@ -4,6 +4,7 @@ from .atbash import *
4
4
  from .bacon import *
5
5
  from .barbie import *
6
6
  from .citrix import *
7
+ from .phillips import *
7
8
  from .polybius import *
8
9
  from .railfence import *
9
10
  from .rot import *
@@ -0,0 +1,128 @@
1
+ # -*- coding: UTF-8 -*-
2
+ """Phillips Cipher Codec - phillips content encoding.
3
+
4
+ The Phillips cipher is a polyalphabetic substitution cipher using 8 key
5
+ squares. The first square is a 5×5 grid built from a keyword (I and J share
6
+ one cell). Seven additional squares are derived by rotating every row of the
7
+ previous square one step to the left. Plaintext is enciphered in bigrams,
8
+ each pair using the next square in a cycle of 8. Non-alphabetic characters
9
+ are passed through unchanged; J is treated as I.
10
+
11
+ This codec:
12
+ - en/decodes strings from str to str
13
+ - en/decodes strings from bytes to bytes
14
+ - decodes file content to str (read)
15
+ - encodes file content from str to bytes (write)
16
+
17
+ Reference: https://www.dcode.fr/phillips-cipher
18
+ """
19
+ from ..__common__ import *
20
+
21
+
22
+ __examples__ = {
23
+ 'enc(phillips)': None,
24
+ 'enc(phillips-key)': {'ATTACK': 'BSSBIC', 'TESTME': 'QBTPLY', 'ABCDEF': 'BKDFYD'},
25
+ 'enc-dec(phillips-key)': ['ATTACK', 'TESTME', 'ABCDEF'],
26
+ 'enc-dec(phillips-secret)': ['HELLOWORLD', 'ATTACKATDAWN'],
27
+ }
28
+ __guess__ = ["phillips-key", "phillips-secret", "phillips-password"]
29
+
30
+
31
+ _ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
32
+
33
+
34
+ def __make_grids(key):
35
+ """Return all 8 grids: the initial grid plus 7 row-rotated variants."""
36
+ # build the initial 5×5 grid from a keyword (J treated as I)
37
+ seen, letters = set(), []
38
+ for c in key.upper().replace("J", "I") + _ALPHABET:
39
+ if c in set(_ALPHABET) and c not in seen:
40
+ letters.append(c)
41
+ seen.add(c)
42
+ grid = [letters[i * 5:(i + 1) * 5] for i in range(5)]
43
+ # now build the other 7 row-rotated variant grids
44
+ grids = [grid]
45
+ for _ in range(7):
46
+ grid = [row[1:] + [row[0]] for row in grid]
47
+ grids.append(grid)
48
+ return grids
49
+
50
+
51
+ def __process_pair(a, b, grid, decode=False):
52
+ """Encode or decode a letter pair using Playfair substitution rules.
53
+
54
+ Same row → each letter shifts one step right (encode) / left (decode).
55
+ Same col → each letter shifts one step down (encode) / up (decode).
56
+ Rectangle → each letter moves to the other's column (self-inverse).
57
+ """
58
+ pos = {ch: (r, c) for r, row in enumerate(grid) for c, ch in enumerate(row)}
59
+ r1, c1 = pos[a]
60
+ r2, c2 = pos[b]
61
+ d = -1 if decode else 1
62
+ if r1 == r2:
63
+ return grid[r1][(c1 + d) % 5], grid[r2][(c2 + d) % 5]
64
+ if c1 == c2:
65
+ return grid[(r1 + d) % 5][c1], grid[(r2 + d) % 5][c2]
66
+ return grid[r1][c2], grid[r2][c1] # rectangle rule is its own inverse
67
+
68
+
69
+ def phillips_encode(key):
70
+ _key = (key or "").strip()
71
+ # Compute grids eagerly if key is valid; otherwise defer error to call time
72
+ _grids = __make_grids(_key) if _key and _key.isalpha() else None
73
+ def encode(text, errors="strict"):
74
+ if _grids is None:
75
+ raise LookupError("Bad parameter for encoding 'phillips': "
76
+ "key must be a non-empty alphabetic string")
77
+ t = ensure_str(text).upper().replace("J", "I")
78
+ alpha = [(i, c) for i, c in enumerate(t) if c in set(_ALPHABET)]
79
+ # Pad to an even count with a trailing X
80
+ padding_char = None
81
+ if len(alpha) % 2 == 1:
82
+ alpha.append((-1, "X"))
83
+ enc_map = {}
84
+ for pair_num, k in enumerate(range(0, len(alpha), 2)):
85
+ pos1, a = alpha[k]
86
+ pos2, b = alpha[k + 1]
87
+ e1, e2 = __process_pair(a, b, _grids[pair_num % 8])
88
+ enc_map[pos1] = e1
89
+ if pos2 >= 0:
90
+ enc_map[pos2] = e2
91
+ else:
92
+ padding_char = e2
93
+ result = [enc_map.get(i, c) for i, c in enumerate(t)]
94
+ if padding_char is not None:
95
+ result.append(padding_char)
96
+ return "".join(result), len(text)
97
+ return encode
98
+
99
+
100
+ def phillips_decode(key):
101
+ _key = (key or "").strip()
102
+ # Compute grids eagerly if key is valid; otherwise defer error to call time
103
+ _grids = __make_grids(_key) if _key and _key.isalpha() else None
104
+ def decode(text, errors="strict"):
105
+ if _grids is None:
106
+ raise LookupError("Bad parameter for decoding 'phillips': "
107
+ "key must be a non-empty alphabetic string")
108
+ t = ensure_str(text).upper().replace("J", "I")
109
+ alpha = [(i, c) for i, c in enumerate(t) if c in set(_ALPHABET)]
110
+ if len(alpha) % 2 == 1:
111
+ if errors == "strict":
112
+ raise ValueError("phillips: encoded text must contain an even "
113
+ "number of alphabetic characters")
114
+ alpha = alpha[:-1]
115
+ dec_map = {}
116
+ for pair_num, k in enumerate(range(0, len(alpha), 2)):
117
+ pos1, a = alpha[k]
118
+ pos2, b = alpha[k + 1]
119
+ d1, d2 = __process_pair(a, b, _grids[pair_num % 8], decode=True)
120
+ dec_map[pos1] = d1
121
+ dec_map[pos2] = d2
122
+ return "".join(dec_map.get(i, c) for i, c in enumerate(t)), len(text)
123
+ return decode
124
+
125
+
126
+ add("phillips", phillips_encode, phillips_decode, r"^phillips(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", printables_rate=1.,
127
+ penalty=.1)
128
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codext
3
- Version: 1.16.1
3
+ Version: 1.16.2
4
4
  Summary: Native codecs extension
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1034,6 +1034,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
1034
1034
  - [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
1035
1035
  - [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
1036
1036
  - [X] `citrix`: aka Citrix CTX1 password encoding
1037
+ - [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
1037
1038
  - [X] `polybius`: aka Polybius Square Cipher
1038
1039
  - [X] `railfence`: aka Rail Fence Cipher
1039
1040
  - [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])
@@ -85,6 +85,7 @@ src/codext/crypto/atbash.py
85
85
  src/codext/crypto/bacon.py
86
86
  src/codext/crypto/barbie.py
87
87
  src/codext/crypto/citrix.py
88
+ src/codext/crypto/phillips.py
88
89
  src/codext/crypto/polybius.py
89
90
  src/codext/crypto/railfence.py
90
91
  src/codext/crypto/rot.py
@@ -172,7 +172,15 @@ class TestCodecsBase(TestCase):
172
172
  self.assertEqual(codecs.decode(B58, "base58-fl"), STR)
173
173
  self.assertEqual(codecs.encode(STR, "base58-short-url"), B58)
174
174
  self.assertEqual(codecs.encode(STR, "base58-url"), B58)
175
-
175
+ # leading null bytes must be preserved as leading charset[0] ('1')
176
+ self.assertEqual(codecs.encode("\x00abc", "base58"), "1ZiCa")
177
+ self.assertEqual(codecs.encode("\x00", "base58"), "1")
178
+ self.assertEqual(codecs.encode("\x00\x00abc", "base58"), "11ZiCa")
179
+ self.assertEqual(codecs.decode("1ZiCa", "base58"), "\x00abc")
180
+ self.assertEqual(codecs.decode("11ZiCa", "base58"), "\x00\x00abc")
181
+ self.assertEqual(codecs.encode(b("\x00abc"), "base58"), b("1ZiCa"))
182
+ self.assertEqual(codecs.decode(b("1ZiCa"), "base58"), b("\x00abc"))
183
+
176
184
  def test_codec_base62(self):
177
185
  for b62, enc in zip(["CsoB4HQ5gmgMyCenF7E", "M2yLERaFqwqW8MoxPHO"], ["base62", "base62-inv"]):
178
186
  self.assertEqual(codecs.encode(STR, enc), b62)
@@ -211,6 +219,19 @@ class TestCodecsBase(TestCase):
211
219
  self.assertRaises(ValueError, codecs.decode, b(B100)[1:], "base100")
212
220
  self.assertIsNotNone(codecs.decode(b(B100) + b"\n", "base100", "ignore"))
213
221
 
222
+ def test_codec_base45(self):
223
+ # RFC 9285 test vectors
224
+ for s, b45 in [("AB", "BB8"), ("Hello!!", "%69 VD92EX0"), ("base-45", "UJCLQE7W581")]:
225
+ self.assertEqual(codecs.encode(s, "base45"), b45)
226
+ self.assertEqual(codecs.encode(b(s), "base45"), b(b45))
227
+ self.assertEqual(codecs.decode(b45, "base45"), s)
228
+ self.assertEqual(codecs.decode(b(b45), "base45"), b(s))
229
+ # a trailing non-ASCII byte must not be dropped (byte length, not str length, drives encoding)
230
+ self.assertEqual(codecs.encode(b"\xcf\xb1\x1b", "base45"), b"OBQR0")
231
+ self.assertEqual(codecs.decode(b"OBQR0", "base45"), b"\xcf\xb1\x1b")
232
+ for data in [b"\xff\xfe", b"hello", b"\x00", b"\x80\x81\x82\x83\x84"]:
233
+ self.assertEqual(codecs.decode(codecs.encode(data, "base45"), "base45"), data)
234
+
214
235
  def test_codec_base_generic(self):
215
236
  for n in range(2, 255):
216
237
  bn = "base{}_generic".format(n)
@@ -1 +0,0 @@
1
- 1.16.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes