v1.1.0 - Added validation and line formatting, fixed zero-length padding. Added tests.

This commit is contained in:
2026-02-16 12:32:27 +00:00
parent 823cc324c4
commit db4f5e5ac2
5 changed files with 253 additions and 17 deletions
+1
View File
@@ -2,3 +2,4 @@ __pycache__/
dist/
.*
!.gitignore
*.lock
+11 -3
View File
@@ -6,7 +6,7 @@ Replaces the standard library's `base64.urlsafe_b64encode` and `base64.urlsafe_b
## Features
- **URL safe**: Uses only characters that are safe for URLs and filenames
- **Urlsafe**: Uses only characters that are safe for URLs and filenames
- **No padding**: Removes trailing `=` characters for cleaner output
- **String output**: Returns proper strings instead of bytes (unlike Python's standard library)
- **Fast**: Based on Python stdlib, with constant-time padding restoration
@@ -35,6 +35,14 @@ data = base64url.dec(text) # Recovers the bytes
Base64 encode bytes to a URL-safe string without padding.
### `dec(s: str) -> bytes`
### `dec(s: str, *, validate=True) -> bytes`
Decode URL-safe Base64 into bytes. Padding optional.
Decode URL-safe Base64 into bytes. Padding optional. Set `validate=False` to silently ignore any unknown characters (Python base64 default behavior).
### `enc_lines(data: bytes, length: int = 76) -> str`
Encode with newlines inserted every `length` characters.
### `dec_lines(s: str) -> bytes`
Decode formatted input, ignoring all whitespace (but not other unknown characters).
+33 -12
View File
@@ -1,18 +1,16 @@
"""URL-safe Base64 encoding without padding.
This format uses only characters that are safe for URLs, filenames etc.
Refer to Python's urlsafe_b64encode and base64.urlsafe_b64decode for more details.
"""Base64 encoding in urlsafe format without padding.
Fixes Python base64 module's faults:
- Padding "==" is optional and not produced by b64url.enc
- Base64 type is str (stdlib is retarded and uses bytes)
- Otherwise identical to urlsafe_b64encode/urlsafe_b64decode
- Padding "==" is optional and not produced by base64url.enc
- Base64 type is str (stdlib uses bytes)
- Doesn't silently ignore invalid characters and produce garbage
"""
from base64 import urlsafe_b64decode as _decode
# We cannot use urlsafe_b64decode which misses the validation argument
from base64 import b64decode as _decode
from base64 import urlsafe_b64encode as _encode
__all__ = ["enc", "dec"]
__all__ = ["enc", "dec", "enc_lines", "dec_lines"]
def enc(data: bytes) -> str:
@@ -20,7 +18,30 @@ def enc(data: bytes) -> str:
return _encode(data).decode("ascii").rstrip("=")
def dec(s: str) -> bytes:
"""Decode URL-safe Base64 into bytes. Padding optional."""
def dec(s: str, *, validate=True) -> bytes:
"""Decode standard or urlsafe Base64, with or without padding.
Validation raises binascii.Error (ValueError) on invalid characters.
If False, any unrecognized characters are silently ignored.
"""
# Testing whether needs padding is slower, this is the fastest way and constant time.
return _decode(s + "=" * (4 - (len(s) % 4)))
return _decode(s + -len(s) % 4 * "=", altchars=b"-_", validate=validate)
def enc_lines(data: bytes, length: int = 76, sep="\n") -> str:
"""Base64 encode bytes to a URL-safe string split into lines.
Returns a string with newlines inserted after every `length` characters.
The value should be a multiple of 4 for best compatibility with other systems.
"""
s = enc(data)
return sep.join([s[i : i + length] for i in range(0, len(s), length)])
def dec_lines(s: str) -> bytes:
"""Decode standard or urlsafe Base64 from a multiline string.
All whitespace is ignored to allow formatted input.
Raises binascii.Error (ValueError) on other invalid characters.
"""
return dec("".join(s.split()))
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "base64url"
version = "1.0.0"
description = "Base64 encoding without Python's base64 flaws. No padding, str types."
version = "1.1.0"
description = "Base64 encoding without Python's base64 flaws. Output urlsafe without padding, as str. Decodes any variant with or without padding, with validation."
readme = "README.md"
requires-python = ">=3.8"
dependencies = []
+206
View File
@@ -0,0 +1,206 @@
"""Tests for base64url module."""
import binascii
import pytest
import base64url
class TestEnc:
"""Tests for enc() function."""
def test_empty(self):
assert base64url.enc(b"") == ""
def test_single_byte(self):
assert base64url.enc(b"\x00") == "AA"
def test_no_padding_needed(self):
# 3 bytes encodes to 4 chars, no padding
assert base64url.enc(b"abc") == "YWJj"
def test_one_padding_stripped(self):
# 2 bytes encodes to 3 chars + 1 padding
assert base64url.enc(b"ab") == "YWI"
assert "=" not in base64url.enc(b"ab")
def test_two_padding_stripped(self):
# 1 byte encodes to 2 chars + 2 padding
assert base64url.enc(b"a") == "YQ"
assert "=" not in base64url.enc(b"a")
def test_urlsafe_chars(self):
# Bytes that produce + and / in standard Base64 should use - and _
# 0xfb -> standard: +, urlsafe: -
# 0xff -> standard: /, urlsafe: _
data = b"\xfb\xff\xfe"
result = base64url.enc(data)
assert "+" not in result
assert "/" not in result
assert "-" in result or "_" in result
def test_all_bytes(self):
# Verify all byte values encode/decode correctly
data = bytes(range(256))
encoded = base64url.enc(data)
assert "+" not in encoded
assert "/" not in encoded
assert "=" not in encoded
class TestDec:
"""Tests for dec() function."""
def test_empty(self):
assert base64url.dec("") == b""
def test_single_byte(self):
assert base64url.dec("AA") == b"\x00"
def test_without_padding(self):
assert base64url.dec("YWJj") == b"abc"
assert base64url.dec("YWI") == b"ab"
assert base64url.dec("YQ") == b"a"
def test_with_padding(self):
# Padding should be optional
assert base64url.dec("YWI=") == b"ab"
assert base64url.dec("YQ==") == b"a"
def test_urlsafe_chars(self):
# Should accept - instead of +, _ instead of /
assert base64url.dec("--__") == base64url.dec("++//")
def test_standard_base64_chars(self):
# Should also accept standard Base64 with + and /
assert base64url.dec("++//") == b"\xfb\xef\xff"
def test_mixed_chars(self):
# Mix of standard and urlsafe should work
assert base64url.dec("+-/_") is not None
def test_invalid_char_raises(self):
# Invalid characters should raise binascii.Error
with pytest.raises(binascii.Error):
base64url.dec("!!!!")
def test_invalid_char_in_middle_raises(self):
with pytest.raises(binascii.Error):
base64url.dec("YW!j")
def test_whitespace_raises(self):
# Whitespace should not be silently ignored
with pytest.raises(binascii.Error):
base64url.dec("YW Jj")
def test_newline_raises(self):
with pytest.raises(binascii.Error):
base64url.dec("YWJj\n")
class TestRoundtrip:
"""Tests for enc/dec roundtrip."""
def test_empty_roundtrip(self):
assert base64url.dec(base64url.enc(b"")) == b""
def test_hello_roundtrip(self):
assert base64url.dec(base64url.enc(b"hello")) == b"hello"
def test_binary_roundtrip(self):
data = bytes(range(256))
assert base64url.dec(base64url.enc(data)) == data
def test_various_lengths(self):
# Test lengths 0-20 to cover all padding scenarios
for i in range(21):
data = bytes(range(i))
assert base64url.dec(base64url.enc(data)) == data
def test_random_bytes(self):
import os
for _ in range(10):
data = os.urandom(100)
assert base64url.dec(base64url.enc(data)) == data
class TestEncLines:
"""Tests for enc_lines() function."""
def test_empty(self):
assert base64url.enc_lines(b"") == ""
def test_short_no_wrap(self):
# Short data fits on one line
result = base64url.enc_lines(b"hello", length=76)
assert "\n" not in result
assert result == "aGVsbG8"
def test_wrapping(self):
# 60 bytes -> 80 base64 chars, should wrap at 76
data = b"x" * 60
result = base64url.enc_lines(data, length=76)
lines = result.split("\n")
assert len(lines) == 2
assert len(lines[0]) == 76
assert len(lines[1]) == 4
def test_custom_line_length(self):
data = b"hello world"
result = base64url.enc_lines(data, length=4)
lines = result.split("\n")
assert all(len(line) <= 4 for line in lines)
def test_exact_multiple(self):
# 6 bytes -> 8 base64 chars, exactly 2 lines of 4
data = b"abcdef"
result = base64url.enc_lines(data, length=4)
lines = result.split("\n")
assert len(lines) == 2
assert all(len(line) == 4 for line in lines)
class TestDecLines:
"""Tests for dec_lines() function."""
def test_empty(self):
assert base64url.dec_lines("") == b""
def test_single_line(self):
assert base64url.dec_lines("aGVsbG8") == b"hello"
def test_multiline(self):
multiline = "aGVs\nbG8"
assert base64url.dec_lines(multiline) == b"hello"
def test_spaces(self):
spaced = "aGVs bG8"
assert base64url.dec_lines(spaced) == b"hello"
def test_tabs(self):
tabbed = "aGVs\tbG8"
assert base64url.dec_lines(tabbed) == b"hello"
def test_mixed_whitespace(self):
mixed = " aGVs\n\t bG8 \n"
assert base64url.dec_lines(mixed) == b"hello"
def test_crlf(self):
crlf = "aGVs\r\nbG8"
assert base64url.dec_lines(crlf) == b"hello"
class TestLinesRoundtrip:
"""Tests for enc_lines/dec_lines roundtrip."""
def test_roundtrip(self):
data = bytes(range(256))
assert base64url.dec_lines(base64url.enc_lines(data)) == data
def test_roundtrip_various_line_lengths(self):
data = b"hello world" * 10
for line_length in [4, 16, 64, 76, 100]:
encoded = base64url.enc_lines(data, length=line_length)
assert base64url.dec_lines(encoded) == data