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/ dist/
.* .*
!.gitignore !.gitignore
*.lock
+11 -3
View File
@@ -6,7 +6,7 @@ Replaces the standard library's `base64.urlsafe_b64encode` and `base64.urlsafe_b
## Features ## 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 - **No padding**: Removes trailing `=` characters for cleaner output
- **String output**: Returns proper strings instead of bytes (unlike Python's standard library) - **String output**: Returns proper strings instead of bytes (unlike Python's standard library)
- **Fast**: Based on Python stdlib, with constant-time padding restoration - **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. 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. """Base64 encoding in urlsafe format 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.
Fixes Python base64 module's faults: Fixes Python base64 module's faults:
- Padding "==" is optional and not produced by b64url.enc - Padding "==" is optional and not produced by base64url.enc
- Base64 type is str (stdlib is retarded and uses bytes) - Base64 type is str (stdlib uses bytes)
- Otherwise identical to urlsafe_b64encode/urlsafe_b64decode - 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 from base64 import urlsafe_b64encode as _encode
__all__ = ["enc", "dec"] __all__ = ["enc", "dec", "enc_lines", "dec_lines"]
def enc(data: bytes) -> str: def enc(data: bytes) -> str:
@@ -20,7 +18,30 @@ def enc(data: bytes) -> str:
return _encode(data).decode("ascii").rstrip("=") return _encode(data).decode("ascii").rstrip("=")
def dec(s: str) -> bytes: def dec(s: str, *, validate=True) -> bytes:
"""Decode URL-safe Base64 into bytes. Padding optional.""" """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. # 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] [project]
name = "base64url" name = "base64url"
version = "1.0.0" version = "1.1.0"
description = "Base64 encoding without Python's base64 flaws. No padding, str types." 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" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [] 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