From f610fdc6344b6f4e6d48325af147302c3401cebe Mon Sep 17 00:00:00 2001 From: riteshr19 Date: Fri, 27 Mar 2026 12:22:50 -0500 Subject: [PATCH] fix: handle emoji/special-character paths in ascii terminals (closes #23) --- README.md | 6 +-- passifypdf/encryptpdf.py | 28 ++++++++++++-- .../test_encryptpdf_integration.py | 38 +++++++++++++++++++ tests/unittests/test_encryptpdf.py | 33 +++++++++++++++- 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ea88e13..3760942 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,12 @@ passifypdf -i input.pdf -o protected.pdf -p mySecretPassword ``` ## Known Issues -If you have nay special chars(example: an emoji like Star ๐ŸŒŸ) in the PDF file, it gives a minor complain during execution. -But it still does the job, so you can ingore that "char or object error" which you see in the output. +- Some PDF object warnings may appear for malformed source PDFs, depending on the input file. +- Emoji and other special characters in input/output file paths are supported. ## Note: In general you can use passifypdf to protect your PDF files against chance attackers. But you should not rely on this for mission-critical data or situation. ## Build & Run Locally -Visit [BUILD.md](BUILD.md) \ No newline at end of file +Visit [BUILD.md](BUILD.md) diff --git a/passifypdf/encryptpdf.py b/passifypdf/encryptpdf.py index e326cab..2e2745a 100644 --- a/passifypdf/encryptpdf.py +++ b/passifypdf/encryptpdf.py @@ -2,7 +2,7 @@ import sys from pathlib import Path -from typing import Union +from typing import TextIO, Union from pypdf import PdfReader, PdfWriter @@ -49,6 +49,23 @@ def encrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], passw raise Exception(f"Failed to encrypt PDF: {e}") +def _stream_safe_text(text: str, stream: TextIO) -> str: + """Return text that can always be encoded by the target stream.""" + encoding = getattr(stream, "encoding", None) or "utf-8" + try: + text.encode(encoding) + return text + except UnicodeEncodeError: + return text.encode(encoding, errors="backslashreplace").decode(encoding, errors="ignore") + except LookupError: + return text.encode("utf-8", errors="backslashreplace").decode("utf-8", errors="ignore") + + +def _safe_print(text: str, stream: TextIO) -> None: + """Print text without failing on terminal encoding limitations.""" + print(_stream_safe_text(text, stream), file=stream) + + def main() -> int: """ Main function to run the CLI. @@ -61,12 +78,15 @@ def main() -> int: try: encrypt_pdf(args.input, args.output, args.passwd) - print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'") + _safe_print( + f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'", + sys.stdout, + ) return 0 except Exception as e: - print(f"Error: {e}", file=sys.stderr) + _safe_print(f"Error: {e}", sys.stderr) return 1 if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/tests/integrationtests/test_encryptpdf_integration.py b/tests/integrationtests/test_encryptpdf_integration.py index 7cebc00..a67d83a 100644 --- a/tests/integrationtests/test_encryptpdf_integration.py +++ b/tests/integrationtests/test_encryptpdf_integration.py @@ -1,4 +1,9 @@ import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path from unittest import TestCase from pypdf import PdfReader, PdfWriter @@ -39,3 +44,36 @@ def test_encrypt_pdf_integration(self): # Verify we can decrypt it self.assertTrue(reader.decrypt(self.password)) self.assertEqual(len(reader.pages), 1) + + def test_cli_ascii_stdout_handles_emoji_paths(self): + sample_pdf = Path(__file__).parent.parent / "resources" / "Sample_PDF.pdf" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + input_pdf = temp_path / "in๐Ÿ“„put๐Ÿ˜€.pdf" + output_pdf = temp_path / "out๐Ÿ”’file๐Ÿ˜€.pdf" + shutil.copy(sample_pdf, input_pdf) + + env = os.environ.copy() + env["PYTHONIOENCODING"] = "ascii" + + result = subprocess.run( + [ + sys.executable, + "-m", + "passifypdf.encryptpdf", + "-i", + str(input_pdf), + "-o", + str(output_pdf), + "-p", + "secret123", + ], + capture_output=True, + text=True, + env=env, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertTrue(output_pdf.exists()) + self.assertIn("\\U0001f512", result.stdout) diff --git a/tests/unittests/test_encryptpdf.py b/tests/unittests/test_encryptpdf.py index d8d2aa9..4951ca6 100644 --- a/tests/unittests/test_encryptpdf.py +++ b/tests/unittests/test_encryptpdf.py @@ -1,6 +1,9 @@ +import argparse +import io from unittest import TestCase -from unittest.mock import patch, mock_open -from passifypdf.encryptpdf import encrypt_pdf +from unittest.mock import MagicMock, mock_open, patch + +from passifypdf.encryptpdf import encrypt_pdf, main class TestPdfUnitTests(TestCase): @@ -59,3 +62,29 @@ def test_encrypt_pdf_is_directory(self, mock_path_cls): with self.assertRaises(IsADirectoryError): encrypt_pdf("directory", "output.pdf", "secret") + + @patch("passifypdf.encryptpdf.encrypt_pdf") + @patch("passifypdf.encryptpdf.get_arg_parser") + def test_main_succeeds_with_ascii_stdout_and_emoji_output_path( + self, mock_get_arg_parser, mock_encrypt_pdf + ): + mock_parser = MagicMock() + mock_parser.parse_args.return_value = argparse.Namespace( + input="input.pdf", + output="out๐Ÿ”’file๐Ÿ˜€.pdf", + passwd="secret123", + ) + mock_get_arg_parser.return_value = mock_parser + + stdout_bytes = io.BytesIO() + stderr_bytes = io.BytesIO() + ascii_stdout = io.TextIOWrapper(stdout_bytes, encoding="ascii", errors="strict") + ascii_stderr = io.TextIOWrapper(stderr_bytes, encoding="ascii", errors="strict") + + with patch("sys.stdout", ascii_stdout), patch("sys.stderr", ascii_stderr): + exit_code = main() + ascii_stdout.flush() + + self.assertEqual(exit_code, 0) + self.assertIn("\\U0001f512", stdout_bytes.getvalue().decode("ascii")) + mock_encrypt_pdf.assert_called_once_with("input.pdf", "out๐Ÿ”’file๐Ÿ˜€.pdf", "secret123")