diff --git a/fixit/base64utils.py b/fixit/base64utils.py new file mode 100644 index 0000000..1880528 --- /dev/null +++ b/fixit/base64utils.py @@ -0,0 +1,20 @@ +"""Base64 text encoding and decoding utilities.""" + +import base64 +import binascii + + +def encode_text(text: str) -> str: + """Encode UTF-8 text as a Base64 string.""" + encoded = base64.b64encode(text.encode("utf-8")) + return encoded.decode("ascii") + + +def decode_text(text: str) -> str: + """Decode a Base64 string into UTF-8 text.""" + try: + encoded = text.encode("ascii") + decoded = base64.b64decode(encoded, validate=True) + return decoded.decode("utf-8") + except (binascii.Error, UnicodeEncodeError, UnicodeDecodeError) as exc: + raise ValueError("Invalid Base64 input.") from exc diff --git a/fixit/cli.py b/fixit/cli.py index 1dd3eb0..af139d1 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -1,10 +1,20 @@ + """Command line entry point: `fixit `.""" import argparse import sys import uuid from pathlib import Path -from fixit import __version__, convert, jsonfmt, password, textutils, wordcount +from fixit import ( + __version__, + base64utils, + convert, + jsonfmt, + password, + textutils, + wordcount, +) + CONVERSIONS = { "c2f": convert.celsius_to_fahrenheit, @@ -81,6 +91,7 @@ def run_slugify(args): "json": run_json, "palindrome": run_palindrome, "reverse": run_reverse, + "base64": run_base64, "slugify": run_slugify, "top-words": run_top_words, } @@ -94,8 +105,15 @@ def build_parser() -> argparse.ArgumentParser: args = parser.parse_args(["wc", "hello world"]) assert args.command == "wc" """ - parser = argparse.ArgumentParser(prog="fixit", description="A tiny toolbox to practice open source.") - parser.add_argument("--version", action="version", version=f"fixit {__version__}") + parser = argparse.ArgumentParser( + prog="fixit", + description="A tiny toolbox to practice open source.", + ) + parser.add_argument( + "--version", + action="version", + version=f"fixit {__version__}", + ) sub = parser.add_subparsers(dest="command", required=True) wc = sub.add_parser("wc", help="count words, lines and characters") @@ -122,7 +140,28 @@ def build_parser() -> argparse.ArgumentParser: rw = sub.add_parser("reverse", help="reverse the words in a sentence") rw.add_argument("text") - sg = sub.add_parser("slugify", help="convert text into a URL-friendly slug") + b64 = sub.add_parser("base64", help="encode or decode Base64 text") + b64_sub = b64.add_subparsers( + dest="base64_action", + required=True, + ) + + b64_encode = b64_sub.add_parser( + "encode", + help="encode text as Base64", + ) + b64_encode.add_argument("text") + + b64_decode = b64_sub.add_parser( + "decode", + help="decode Base64 text", + ) + b64_decode.add_argument("text") + + sg = sub.add_parser( + "slugify", + help="convert text into a URL-friendly slug", + ) sg.add_argument("text") top = sub.add_parser("top-words", help="show the most frequent words") diff --git a/tests/test_base64.py b/tests/test_base64.py new file mode 100644 index 0000000..958d87a --- /dev/null +++ b/tests/test_base64.py @@ -0,0 +1,43 @@ +import pytest + +from fixit import base64utils, cli + + +def test_encode_text(): + assert base64utils.encode_text("hello") == "aGVsbG8=" + + +def test_decode_text(): + assert base64utils.decode_text("aGVsbG8=") == "hello" + + +def test_encode_unicode_text(): + text = "\u0928\u092e\u0938\u094d\u0924\u0947" + assert base64utils.decode_text(base64utils.encode_text(text)) == text + + +def test_empty_text(): + assert base64utils.encode_text("") == "" + assert base64utils.decode_text("") == "" + + +@pytest.mark.parametrize("value", ["%%%", "aGVsbG8", "////"]) +def test_invalid_base64_or_non_utf8_text(value): + with pytest.raises(ValueError, match="Invalid Base64 input"): + base64utils.decode_text(value) + + +def test_cli_encode(capsys): + assert cli.main(["base64", "encode", "hello"]) == 0 + assert capsys.readouterr().out == "aGVsbG8=\n" + + +def test_cli_decode(capsys): + assert cli.main(["base64", "decode", "aGVsbG8="]) == 0 + assert capsys.readouterr().out == "hello\n" + + +def test_cli_invalid_base64(capsys): + assert cli.main(["base64", "decode", "%%%"]) == 1 + captured = capsys.readouterr() + assert "Invalid Base64 input" in captured.err