Repository: suminb/base62

Issue:
Ignoring leading zero bytes
Hello,

first of all, thank you for this library. I am using it for encoding 16 byte blocks and I have noticed, that during encoding, leading bytes that are equal to `0x00` are ignored. This is due to conversion to integer, which the library internally does. I believe this is not a correct behavior, because without knowledge of the input bytes block length, you cannot reconstruct (decode) the original input from output. But for example in encryption (and many other areas), all bytes (incl. leading zero bytes) matter.

I'll give an example using base64, which does this correctly:
```
encoded = b64encode(b'\x00\x00\x01').decode()
print(encoded)
decoded = b64decode(encoded)
print(decoded)
```
This code yields:
```
AAAB
b'\x00\x00\x01'
```

Now your library:
```
encoded = base62.encodebytes(b'\x00\x00\x01')
print(encoded)
decoded = base62.decodebytes(encoded)
print(decoded)
```
Yields:
```
1
b'\x01'
```

As you can see, decoded output is not equal the input (it misses the two leading zero bytes).

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..616d055
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,28 @@
+sudo: false
+language: python
+python:
+  - "2.7"
+  - "3.5"
+  - "3.6"
+  - "3.7"
+  - "3.8-dev"
+  - "pypy"
+
+addons:
+  sonarcloud:
+    organization: "suminb-github"
+
+install:
+  - pip install --requirement tests/requirements.txt
+  - pip install "black; python_version >= '3.6'"
+
+script:
+  - |
+    if [ -x "$(command -v black)" ]; then
+      black --check .
+    fi
+  - py.test tests --cov base62 --durations=10
+
+after_success:
+  - coveralls
+  - sonar-scanner
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 550d023..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
-
-## 0.6.0 (2023-02-05)
-
-- Remove Python 2 compatibility code
-- Fix Python 3.6 failing in CI
-- Update the build badge in the README
diff --git a/README.rst b/README.rst
index 1aa0e51..c81a09b 100644
--- a/README.rst
+++ b/README.rst
@@ -7,8 +7,8 @@ A Python module for ``base62`` encoding. Ported from PHP code that I wrote
 in mid-2000, which can be found
 `here <http://philosophical.one/posts/base62>`__.
 
-.. |Build Status| image:: https://github.com/suminb/base62/actions/workflows/ci.yml/badge.svg
-   :target: https://github.com/suminb/base62/actions/workflows/ci.yml?query=branch%3Adevelop
+.. |Build Status| image:: https://travis-ci.org/suminb/base62.svg?branch=master
+   :target: https://travis-ci.org/suminb/base62
 .. |PyPI| image:: https://img.shields.io/pypi/v/pybase62.svg
    :target: https://pypi.python.org/pypi/pybase62
 .. |Coveralls| image:: https://coveralls.io/repos/github/suminb/base62/badge.svg?branch=master
diff --git a/base62.py b/base62.py
index df45c41..e02ba13 100644
--- a/base62.py
+++ b/base62.py
@@ -9,31 +9,25 @@ Originated from http://blog.suminb.com/archives/558
 __title__ = "base62"
 __author__ = "Sumin Byeon"
 __email__ = "suminb@gmail.com"
-__version__ = "0.6.0"
+__version__ = "0.5.0"
 
 BASE = 62
 CHARSET_DEFAULT = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 CHARSET_INVERTED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
 
-def encode(n, minlen=1, charset=CHARSET_DEFAULT):
+def encode(n, charset=CHARSET_DEFAULT):
     """Encodes a given integer ``n``."""
 
     chs = []
     while n > 0:
-        r = n % BASE
-        n //= BASE
+        n, r = divmod(n, BASE)
+        chs.insert(0, charset[r])
 
-        chs.append(charset[r])
+    if not chs:
+        return "0"
 
-    if len(chs) > 0:
-        chs.reverse()
-    else:
-        chs.append("0")
-
-    s = "".join(chs)
-    s = charset[0] * max(minlen - len(s), 0) + s
-    return s
+    return "".join(chs)
 
 
 def encodebytes(barray, charset=CHARSET_DEFAULT):
@@ -45,7 +39,27 @@ def encodebytes(barray, charset=CHARSET_DEFAULT):
     """
 
     _check_type(barray, bytes)
-    return encode(int.from_bytes(barray, "big"), charset=charset)
+
+    # Count the number of leading zeros.
+    leading_zeros_count = 0
+    for i in range(len(barray)):
+        if barray[i] != 0:
+            break
+        leading_zeros_count += 1
+
+    # Encode the leading zeros as "0" followed by a character indicating the count.
+    # This pattern may occur several times if there are many leading zeros.
+    n, r = divmod(leading_zeros_count, len(charset) - 1)
+    zero_padding = f"0{charset[-1]}" * n
+    if r:
+        zero_padding += f"0{charset[r]}"
+
+    # Special case: the input is empty, or is entirely null bytes.
+    if leading_zeros_count == len(barray):
+        return zero_padding
+
+    value = encode(int.from_bytes(barray, "big"), charset=charset)
+    return zero_padding + value
 
 
 def decode(encoded, charset=CHARSET_DEFAULT):
@@ -56,9 +70,6 @@ def decode(encoded, charset=CHARSET_DEFAULT):
     """
     _check_type(encoded, str)
 
-    if encoded.startswith("0z"):
-        encoded = encoded[2:]
-
     l, i, v = len(encoded), 0, 0
     for x in encoded:
         v += _value(x, charset=charset) * (BASE ** (l - (i + 1)))
@@ -75,6 +86,11 @@ def decodebytes(encoded, charset=CHARSET_DEFAULT):
     :rtype: bytes
     """
 
+    leading_null_bytes = b""
+    while encoded.startswith("0") and len(encoded) >= 2:
+        leading_null_bytes += b"\x00" * _value(encoded[1], charset)
+        encoded = encoded[2:]
+
     decoded = decode(encoded, charset=charset)
     buf = bytearray()
     while decoded > 0:
@@ -82,7 +98,7 @@ def decodebytes(encoded, charset=CHARSET_DEFAULT):
         decoded //= 256
     buf.reverse()
 
-    return bytes(buf)
+    return leading_null_bytes + bytes(buf)
 
 
 def _value(ch, charset):
diff --git a/setup.py b/setup.py
index d4c8ce6..1329194 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,5 @@ setup(
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
     ],
 )
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..39686ef
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,4 @@
+sonar.projectKey=base62
+sonar.sources=.
+sonar.host.url=https://sonarcloud.io
+sonar.login=travisci


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 73f1d09..bade00f 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -4,7 +4,6 @@ import base62
 
 
 bytes_int_pairs = [
-    (b"\x00", 0),
     (b"\x01", 1),
     (b"\x01\x01", 0x0101),
     (b"\xff\xff", 0xFFFF),
@@ -20,9 +19,6 @@ def test_const():
 
 def test_basic():
     assert base62.encode(0) == "0"
-    assert base62.encode(0, minlen=0) == "0"
-    assert base62.encode(0, minlen=1) == "0"
-    assert base62.encode(0, minlen=5) == "00000"
     assert base62.decode("0") == 0
     assert base62.decode("0000") == 0
     assert base62.decode("000001") == 1
@@ -30,19 +26,11 @@ def test_basic():
     assert base62.encode(34441886726) == "base62"
     assert base62.decode("base62") == 34441886726
 
-    # NOTE: For backward compatibility. When I first wrote this module in PHP,
-    # I used to use the `0z` prefix to denote a base62 encoded string (similar
-    # to `0x` for hexadecimal strings).
-    assert base62.decode("0zbase62") == 34441886726
-
 
 def test_basic_inverted():
     kwargs = {"charset": base62.CHARSET_INVERTED}
 
     assert base62.encode(0, **kwargs) == "0"
-    assert base62.encode(0, minlen=0, **kwargs) == "0"
-    assert base62.encode(0, minlen=1, **kwargs) == "0"
-    assert base62.encode(0, minlen=5, **kwargs) == "00000"
     assert base62.decode("0", **kwargs) == 0
     assert base62.decode("0000", **kwargs) == 0
     assert base62.decode("000001", **kwargs) == 1
@@ -50,11 +38,6 @@ def test_basic_inverted():
     assert base62.encode(10231951886, **kwargs) == "base62"
     assert base62.decode("base62", **kwargs) == 10231951886
 
-    # NOTE: For backward compatibility. When I first wrote this module in PHP,
-    # I used to use the `0z` prefix to denote a base62 encoded string (similar
-    # to `0x` for hexadecimal strings).
-    assert base62.decode("0zbase62", **kwargs) == 10231951886
-
 
 @pytest.mark.parametrize("b, i", bytes_int_pairs)
 def test_bytes_to_int(b, i):
@@ -77,7 +60,7 @@ def test_encodebytes_rtype():
     assert isinstance(encoded, str)
 
 
-@pytest.mark.parametrize("s", ["0", "1", "a", "z", "ykzvd7ga", "0z1234"])
+@pytest.mark.parametrize("s", ["0", "1", "a", "z", "ykzvd7ga"])
 def test_decodebytes(s):
     assert int.from_bytes(base62.decodebytes(s), "big") == base62.decode(s)
 
@@ -113,3 +96,23 @@ def test_invalid_alphabet():
 def test_invalid_string():
     with pytest.raises(TypeError):
         base62.encodebytes({})
+
+
+@pytest.mark.parametrize(
+    "input_bytes, expected_encoded_text",
+    (
+        (b"", ""),
+        (b"\x00", "01"),
+        (b"\x00\x00", "02"),
+        (b"\x00\x01", "011"),
+        (b"\x00" * 61, "0z"),
+        (b"\x00" * 62, "0z01"),
+    ),
+)
+def test_leading_zeros(input_bytes, expected_encoded_text):
+    """Verify that leading null bytes are not lost."""
+
+    encoded_text = base62.encodebytes(input_bytes)
+    assert encoded_text == expected_encoded_text
+    output_bytes = base62.decodebytes(encoded_text)
+    assert output_bytes == input_bytes