diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd37da4..edc3695 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 2026.01.09 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 2026.01.09 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} @@ -47,10 +47,29 @@ jobs: - name: C++ Performance Tests run: make perf + # # # # java: + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 5.2.0 - 2026.0 + with: + java-version: '21' + distribution: 'temurin' + + - name: Java Compilation + run: javac -d out src/main/java/base91x/Base91x.java src/test/java/base91x/Base91xTest.java + + - name: Java Test + run: java -cp out base91x.Base91xTest + + # # # # js: + + - name: JS Test + run: node --test + # # # # python: - name: Set up latest Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 2026.01.22 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - 2026.01.22 with: python-version: 3.x @@ -64,7 +83,7 @@ jobs: run: pytest --random-order-bucket=global --cov=src --cov-report xml:xmlcov/coverage.xml - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 2026.03.18 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 - 2026.03.18 with: token: ${{ secrets.CODECOV_TOKEN }} files: xmlcov/coverage.xml @@ -134,7 +153,7 @@ jobs: - name: Failure data upload if: ${{ failure() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 2026.02.26 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - 2026.02.26 with: name: crash_data path: ./*.dat @@ -143,7 +162,7 @@ jobs: - name: Publish if: ${{ 'release' == github.event_name }} - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 2026.09.04 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - 2026.09.04 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} diff --git a/README.md b/README.md index 489ba3a..d5554eb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://pypi.org/project/base91x/ [![License](https://img.shields.io/badge/licence-MIT-green.svg?style=flat)](LICENSE) [![Python](https://img.shields.io/pypi/pyversions/base91x.svg)](https://badge.fury.io/py/base91x) -This base91x method provides data encoding and decoding +This base91x method provides data encoding and decoding using numeric system of base 91 with specific alphabet that does not require escaping any symbols in C, C++ (and many other languages?) string. 'x' - means the alphabet was obtained with XOR function. @@ -23,11 +23,11 @@ The alphabet contains visible printable characters of ASCII except: `\` Backslash -An encoded string might be used for JSON string if JSON does not require to escape / Slash. +An encoded string might be used for JSON string if JSON does not require to escape ``/`` Slash. Encoded string size ~ 16 * original size / 13. -There is possibility to extend the algorithm to use 89 codes during decode. +There is possibility to extend the algorithm to use 89 codes during decode. (breaks compatibility) The alphabet transforms from base91 value with operation XOR(0x7F) with the tree exceptions. @@ -47,4 +47,14 @@ char string[]=R"%%%( a string )%%%"; Workaround: use space | line feed | tab in encoded text to break wrong sequence due the algorithm skips non alphabet symbols. e.g. ``Ma^7*/0629`` -> ``Ma^7* /0629`` +HTML built-in encoding may hurt with ```` and so on, but it is almost impossible to appear the symbols from binary data. +Anyway - extra spaces do the workaround to fix parsing issue. ``< / script >`` is not valid closed tag. + The algorithm is not compatible with https://base91.sourceforge.net/ + +Current realization for: +- [Python](./src/base91x/base91x.py) +- [C](./src/base91x.c) +- [C++](./include/base91x.hpp) (header realization) +- [Java](./src/main/java/base91x/Base91x.java) +- [JS](./src/base91x.js) diff --git a/package.json b/package.json new file mode 100644 index 0000000..9af8713 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "js-base91x-lib", + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "node --test" + } +} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..44da853 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +pythonpath = src +testpaths = tests +addopts = -ra -v diff --git a/src/base91x.c b/src/base91x.c index 2a03c00..0feaaa5 100644 --- a/src/base91x.c +++ b/src/base91x.c @@ -47,33 +47,40 @@ static const unsigned b91word_mask = 0x1FFF; /** base91x JSON OPTIMIZED ALPHABET: */ // clang-format off -static const unsigned char BASE91X_ALPHABET[BASE91X_LEN] = {'!', '~', '}', '|', '{', 'z', 'y', 'x', 'w', 'v', - 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', - 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', - 'a', '`', '_', '^', ']', '#', '[', 'Z', 'Y', 'X', - 'W', 'V', 'U', 'T', 'S', 'R', 'Q', 'P', 'O', 'N', - 'M', 'L', 'K', 'J', 'I', 'H', 'G', 'F', 'E', 'D', - 'C', 'B', 'A', '@', '?', '>', '=', '<', ';', ':', - '9', '8', '7', '6', '5', '4', '3', '2', '1', '0', - '/', '.', '-', ',', '+', '*', ')', '(', '$', '&', - '%'}; +static const unsigned char BASE91X_ALPHABET[BASE91X_LEN] = { + '!', '~', '}', '|', '{', 'z', 'y', 'x', 'w', 'v', + 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', + 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', + 'a', '`', '_', '^', ']', '#', '[', 'Z', 'Y', 'X', + 'W', 'V', 'U', 'T', 'S', 'R', 'Q', 'P', 'O', 'N', + 'M', 'L', 'K', 'J', 'I', 'H', 'G', 'F', 'E', 'D', + 'C', 'B', 'A', '@', '?', '>', '=', '<', ';', ':', + '9', '8', '7', '6', '5', '4', '3', '2', '1', '0', + '/', '.', '-', ',', '+', '*', ')', '(', '$', '&', + '%' +}; // clang-format on /** base91x reverse table for quick decoding from an 8-bit unsigned index */ // clang-format off -static const char BASE91X_ZYX[0x100] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, 0, -1, 35, 88, 90, 89, -1, 87, 86, 85, 84, 83, 82, 81, 80, - 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, - 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, - 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, -1, 34, 33, 32, - 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, - 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - }; +static const char BASE91X_ZYX[0x100] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, +0, -1, 35, 88, 90, 89, -1, 87, 86, 85, 84, 83, 82, 81, 80, + 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, + 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, + 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, -1, 34, 33, 32, + 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, +9, +8, +7, +6, +5, +4, +3, +2, +1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; // clang-format on /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ diff --git a/src/base91x.js b/src/base91x.js new file mode 100644 index 0000000..422313c --- /dev/null +++ b/src/base91x.js @@ -0,0 +1,140 @@ +/* +MIT License + +Copyright (c) 2026 Roman Babenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +const BASE91X_LEN = 91; +const CHAR_BIT = 8; +const B91WORD_BIT = 13; +const B91WORD_MASK = 0x1FFF; + +const BASE91X_ALPHABET = [ + '!', '~', '}', '|', '{', 'z', 'y', 'x', 'w', 'v', + 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', + 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', + 'a', '`', '_', '^', ']', '#', '[', 'Z', 'Y', 'X', + 'W', 'V', 'U', 'T', 'S', 'R', 'Q', 'P', 'O', 'N', + 'M', 'L', 'K', 'J', 'I', 'H', 'G', 'F', 'E', 'D', + 'C', 'B', 'A', '@', '?', '>', '=', '<', ';', ':', + '9', '8', '7', '6', '5', '4', '3', '2', '1', '0', + '/', '.', '-', ',', '+', '*', ')', '(', '$', '&', + '%' +]; + +const BASE91X_ZYX = new Int16Array([ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, +0, -1, 35, 88, 90, 89, -1, 87, 86, 85, 84, 83, 82, 81, 80, + 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, + 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, + 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, -1, 34, 33, 32, + 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, +9, +8, +7, +6, +5, +4, +3, +2, +1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]); + + +export function encode(input) { + let collector = 0; + let bitCollected = 0; + + const out = []; + + for (let i = 0; i < input.length; i++) { + collector |= input[i] << bitCollected; + bitCollected += CHAR_BIT; + + while (bitCollected >= B91WORD_BIT) { + const value = collector & B91WORD_MASK; + + const quot = (value / BASE91X_LEN) | 0; + const rem = value - quot * BASE91X_LEN; + + out.push(BASE91X_ALPHABET[rem]); + out.push(BASE91X_ALPHABET[quot]); + + collector >>= B91WORD_BIT; + bitCollected -= B91WORD_BIT; + } + } + + if (bitCollected !== 0) { + const value = collector & B91WORD_MASK; + + const rem = value % BASE91X_LEN; + const quot = Math.floor(value / BASE91X_LEN); + + out.push(BASE91X_ALPHABET[rem]); + + if (bitCollected >= 7) { + out.push(BASE91X_ALPHABET[quot]); + } + } + + return out.join(""); +} + + +export function decode(str) { + let collector = 0; + let bitCollected = 0; + let lower = -1; + + const out = []; + + for (let i = 0; i < str.length; i++) { + const digit = BASE91X_ZYX[str.charCodeAt(i)]; + if (digit === -1) continue; + + if (lower === -1) { + lower = digit; + continue; + } + + collector |= (BASE91X_LEN * digit + lower) << bitCollected; + bitCollected += B91WORD_BIT; + lower = -1; + + while (bitCollected >= CHAR_BIT) { + out.push(collector & 0xFF); + collector >>= CHAR_BIT; + bitCollected -= CHAR_BIT; + } + } + if (lower !== -1) { + collector |= lower << bitCollected; + bitCollected += (CHAR_BIT - 1); + } + + if (bitCollected >= CHAR_BIT) { + out.push(collector & 0xFF); + } + + return Uint8Array.from(out); +} diff --git a/src/main/java/base91x/Base91x.java b/src/main/java/base91x/Base91x.java index 01f6e1e..b071c28 100644 --- a/src/main/java/base91x/Base91x.java +++ b/src/main/java/base91x/Base91x.java @@ -25,48 +25,55 @@ of this software and associated documentation files (the "Software"), to deal public final class Base91x { - private Base91x() {} - - // --- constants --- + private Base91x() { + } private static final int BASE = 91; private static final int CHAR_BITS = 8; private static final int WORD_BITS = 13; private static final int WORD_MASK = 0x1FFF; - // --- alphabet --- - - private static final byte[] ALPHABET = new byte[] { - '!', '~', '}', '|', '{', 'z', 'y', 'x', 'w', 'v', - 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', - 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', - 'a', '`', '_', '^', ']', '#', '[', 'Z', 'Y', 'X', - 'W', 'V', 'U', 'T', 'S', 'R', 'Q', 'P', 'O', 'N', - 'M', 'L', 'K', 'J', 'I', 'H', 'G', 'F', 'E', 'D', - 'C', 'B', 'A', '@', '?', '>', '=', '<', ';', ':', - '9', '8', '7', '6', '5', '4', '3', '2', '1', '0', - '/', '.', '-', ',', '+', '*', ')', '(', '$', '&', - '%' + private static final byte[] BASE91X_ALPHABET = new byte[] { + '!', '~', '}', '|', '{', 'z', 'y', 'x', 'w', 'v', + 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', + 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', + 'a', '`', '_', '^', ']', '#', '[', 'Z', 'Y', 'X', + 'W', 'V', 'U', 'T', 'S', 'R', 'Q', 'P', 'O', 'N', + 'M', 'L', 'K', 'J', 'I', 'H', 'G', 'F', 'E', 'D', + 'C', 'B', 'A', '@', '?', '>', '=', '<', ';', ':', + '9', '8', '7', '6', '5', '4', '3', '2', '1', '0', + '/', '.', '-', ',', '+', '*', ')', '(', '$', '&', + '%' }; - private static final int[] REVERSE = new int[256]; - - static { - for (int i = 0; i < REVERSE.length; i++) { - REVERSE[i] = -1; - } - for (int i = 0; i < ALPHABET.length; i++) { - REVERSE[ALPHABET[i] & 0xFF] = i; - } - } - - // --- size helpers --- + private static final byte[] BASE91X_ZYX = new byte[] { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, +0, -1, 35, 88, 90, 89, -1, 87, 86, 85, 84, 83, 82, 81, 80, + 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, + 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, + 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, -1, 34, 33, 32, + 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, +9, +8, +7, +6, +5, +4, +3, +2, +1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; public static int computeEncodedSize(int size) { size <<= 4; - return (size % WORD_BITS != 0) - ? (size / WORD_BITS + 1) - : (size / WORD_BITS); + if (0 != size % WORD_BITS) { + size /= WORD_BITS; + size += 1; + } else { + size /= WORD_BITS; + } + return size; } public static int assumeDecodedSize(int size) { @@ -75,10 +82,9 @@ public static int assumeDecodedSize(int size) { return size; } - // --- encode --- - public static byte[] encode(byte[] input) { - if (input == null) return null; + if (null == input) + return null; byte[] out = new byte[computeEncodedSize(input.length)]; @@ -90,49 +96,48 @@ public static byte[] encode(byte[] input) { collector |= (b & 0xFF) << bits; bits += CHAR_BITS; - while (bits >= WORD_BITS) { + while (WORD_BITS <= bits) { int value = collector & WORD_MASK; - int rem = value % BASE; int quot = value / BASE; + int rem = value % BASE; - out[outPos++] = ALPHABET[rem]; - if (outPos >= out.length) break; + out[outPos++] = BASE91X_ALPHABET[rem]; + if (outPos >= out.length) + break; - out[outPos++] = ALPHABET[quot]; + out[outPos++] = BASE91X_ALPHABET[quot]; collector >>>= WORD_BITS; bits -= WORD_BITS; } } - if (bits > 0 && outPos < out.length) { + if (0 < bits && outPos < out.length) { int value = collector & WORD_MASK; int rem = value % BASE; int quot = value / BASE; - out[outPos++] = ALPHABET[rem]; + out[outPos++] = BASE91X_ALPHABET[rem]; - if (bits >= 7 && outPos < out.length) { - out[outPos++] = ALPHABET[quot]; + if (7 <= bits && outPos < out.length) { + out[outPos++] = BASE91X_ALPHABET[quot]; } } - if (outPos == out.length) { + if (out.length == outPos) { return out; } - // trim (Java-style) byte[] result = new byte[outPos]; System.arraycopy(out, 0, result, 0, outPos); return result; } - // --- decode --- - public static byte[] decode(byte[] input) { - if (input == null) return null; + if (null == input) + return null; byte[] out = new byte[assumeDecodedSize(input.length)]; @@ -142,10 +147,11 @@ public static byte[] decode(byte[] input) { int lower = -1; for (byte b : input) { - int digit = REVERSE[b & 0xFF]; - if (digit == -1) continue; + int digit = BASE91X_ZYX[b & 0xFF]; + if (-1 == digit) + continue; - if (lower == -1) { + if (-1 == lower) { lower = digit; continue; } @@ -154,19 +160,19 @@ public static byte[] decode(byte[] input) { bits += WORD_BITS; lower = -1; - while (bits >= CHAR_BITS && outPos < out.length) { + while (CHAR_BITS <= bits && outPos < out.length) { out[outPos++] = (byte) (collector & 0xFF); collector >>>= CHAR_BITS; bits -= CHAR_BITS; } } - if (lower != -1) { + if (-1 != lower) { collector |= lower << bits; bits += (CHAR_BITS - 1); } - if (bits >= CHAR_BITS && outPos < out.length) { + if (CHAR_BITS <= bits && outPos < out.length) { out[outPos++] = (byte) (collector & 0xFF); } @@ -174,4 +180,4 @@ public static byte[] decode(byte[] input) { System.arraycopy(out, 0, result, 0, outPos); return result; } -} \ No newline at end of file +} diff --git a/src/test/java/base91x/Base91xTest.java b/src/test/java/base91x/Base91xTest.java index 6240086..3df0d31 100644 --- a/src/test/java/base91x/Base91xTest.java +++ b/src/test/java/base91x/Base91xTest.java @@ -26,14 +26,6 @@ of this software and associated documentation files (the "Software"), to deal import java.nio.charset.StandardCharsets; import java.util.Arrays; -/* - -TEXT = "The quick brown\r\nfox\tjumps\nover\rthe lazy\n\rdog!" -PANGRAM = "Thequickbrownfoxjumpsoverthelazydog!" -DATA = [88, 136, 162, 112, 31, 156, 195, 75, 208, 5, 61, 106, 20, 163, 227, 172, 240, 150, 163, 100, 63, 170, 82, - 175, 58, 17, 203, 5, 3] - */ - public class Base91xTest { public static void main(String[] args) { @@ -45,28 +37,28 @@ public static void main(String[] args) { } private static void testString() { - String input = "hello"; + byte[] input = "hello".getBytes(StandardCharsets.UTF_8); - String encoded = Base91x.encode(input); - String decoded = Base91x.decode(encoded); + byte[] encoded = Base91x.encode(input); + byte[] decoded = Base91x.decode(encoded); - assertEquals(input, decoded, "String test failed"); + assertArrayEquals(input, decoded, "Refurbish test failed"); } private static void testBytes() { - byte[] input = new byte[] { 88, (byte)136, (byte)162, 112, 31, (byte)156, (byte)195, 75, (byte)208, 5, 61, 106, 20, (byte)163, (byte)227, - (byte)172, (byte)240, (byte)150, (byte)163, 100, 63, (byte)170, 82, (byte)175, 58, 17, (byte)203, 5, 3 }; + byte[] input = new byte[] { 88, (byte) 136, (byte) 162, 112, 31, (byte) 156, (byte) 195, 75, (byte) 208, 5, 61, + 106, 20, (byte) 163, (byte) 227, (byte) 172, (byte) 240, (byte) 150, (byte) 163, 100, 63, (byte) 170, + 82, (byte) 175, 58, 17, (byte) 203, 5, 3 }; byte[] encoded = Base91x.encode(input); String actual = new String(encoded, StandardCharsets.US_ASCII); - assertEquals("Thequickbrownfoxjumpsoverthelazydog!", actual,"Unmatch"); + assertEquals("Thequickbrownfoxjumpsoverthelazydog!", actual, "Unmatch"); byte[] decoded = Base91x.decode(encoded); assertArrayEquals(input, decoded, "Bytes test failed"); } private static void testNull() { - assertEquals(null, Base91x.encode((String) null), "Null string failed"); assertEquals(null, Base91x.decode((byte[]) null), "Null bytes failed"); } @@ -87,7 +79,4 @@ private static void fail(String message) { System.exit(1); } } -/* - * rm -fr out && mkdir -p out && javac -d out src/main/java/base91x/Base91x.java - * src/test/java/base91x/Base91xTest.java && java -cp out base91x.Base91xTest - */ \ No newline at end of file +// rm -fr out && mkdir -p out && javac -d out src/main/java/base91x/Base91x.java src/test/java/base91x/Base91xTest.java && java -cp out base91x.Base91xTest diff --git a/tests/base91x.test.js b/tests/base91x.test.js new file mode 100644 index 0000000..17ab2b0 --- /dev/null +++ b/tests/base91x.test.js @@ -0,0 +1,52 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import {readFileSync} from "node:fs"; +import {encode, decode} from "../src/base91x.js"; +import {webcrypto as crypto} from "node:crypto"; + +function hexToBytes(hex) { + if (hex.length === 0) return new Uint8Array(); + return Uint8Array.from(hex.match(/.{1,2}/g).map(b => parseInt(b, 16))); +} + +function bytesToHex(bytes) { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); +} + +const vectors = JSON.parse( + readFileSync(new URL("./test.json", import.meta.url)) +); + +// fixed tests cross validation + +for (const [hex, expected] of Object.entries(vectors)) { + test(`encode ${hex}`, () => { + const input = hexToBytes(hex); + assert.equal(encode(input), expected); + }); + + test(`decode ${expected}`, () => { + const output = decode(expected); + assert.equal(bytesToHex(output), hex); + }); +} + +// random roundtrip + +function randomBytes(len) { + const arr = new Uint8Array(len); + crypto.getRandomValues(arr); + return arr; +} + +test("random roundtrip", () => { + for (let i = 0; i < 100; i++) { + const input = randomBytes((Math.random() * 100) | 0); + const encoded = encode(input); + const decoded = decode(encoded); + + assert.deepEqual(decoded, input); + } +}); diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..1d18fcb --- /dev/null +++ b/tests/index.html @@ -0,0 +1,80 @@ + + + + + + + + + + diff --git a/tests/test_base91x.py b/tests/test_base91x.py index 8dd72db..a848f0b 100644 --- a/tests/test_base91x.py +++ b/tests/test_base91x.py @@ -28,7 +28,7 @@ from pathlib import Path from random import randbytes -from src.base91x import base91x +from base91x import base91x TEXT = "The quick brown\r\nfox\tjumps\nover\rthe lazy\n\rdog!" PANGRAM = "Thequickbrownfoxjumpsoverthelazydog!"