diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index af2bb9e..3868068 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -18,9 +18,9 @@ import hashlib from typing import Optional import shutil import time -import select import builtins from termcolor import colored +from utils.input_utils import timed_input from password_manager.encryption import EncryptionManager from password_manager.entry_management import EntryManager @@ -1285,14 +1285,12 @@ class PasswordManager: sys.stdout.write(f"\r{bar} {remaining:2d}s") sys.stdout.flush() try: - if ( - sys.stdin - in select.select([sys.stdin], [], [], 1)[0] - ): - user_input = sys.stdin.readline().strip().lower() - if user_input == "b": - exit_loop = True - break + user_input = timed_input("", 1) + if user_input.strip().lower() == "b": + exit_loop = True + break + except TimeoutError: + pass except KeyboardInterrupt: exit_loop = True print() @@ -2009,10 +2007,11 @@ class PasswordManager: print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s") sys.stdout.flush() try: - if sys.stdin in select.select([sys.stdin], [], [], 1)[0]: - user_input = sys.stdin.readline().strip().lower() - if user_input == "b": - break + user_input = timed_input("", 1) + if user_input.strip().lower() == "b": + break + except TimeoutError: + pass except KeyboardInterrupt: print() break diff --git a/src/password_manager/password_generation.py b/src/password_manager/password_generation.py index 5b80e37..8d95e0d 100644 --- a/src/password_manager/password_generation.py +++ b/src/password_manager/password_generation.py @@ -30,6 +30,15 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.backends import default_backend from bip_utils import Bip39SeedGenerator +# Ensure the ``imghdr`` module is available for ``pgpy`` on Python 3.13+ +try: # pragma: no cover - only executed on Python >= 3.13 + import imghdr # type: ignore +except ModuleNotFoundError: # pragma: no cover - fallback for removed module + from utils import imghdr_stub as imghdr # type: ignore + import sys + + sys.modules.setdefault("imghdr", imghdr) + from local_bip85.bip85 import BIP85 from constants import DEFAULT_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH diff --git a/src/tests/test_manager_display_totp_codes.py b/src/tests/test_manager_display_totp_codes.py index c9f12a7..b2773e0 100644 --- a/src/tests/test_manager_display_totp_codes.py +++ b/src/tests/test_manager_display_totp_codes.py @@ -50,7 +50,7 @@ def test_handle_display_totp_codes(monkeypatch, capsys): # interrupt the loop after first iteration monkeypatch.setattr( - "password_manager.manager.select.select", + "password_manager.manager.timed_input", lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()), ) @@ -91,7 +91,7 @@ def test_display_totp_codes_excludes_blacklisted(monkeypatch, capsys): ) monkeypatch.setattr( - "password_manager.manager.select.select", + "password_manager.manager.timed_input", lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()), ) diff --git a/src/tests/test_manager_list_entries.py b/src/tests/test_manager_list_entries.py index b780360..5d7dd3a 100644 --- a/src/tests/test_manager_list_entries.py +++ b/src/tests/test_manager_list_entries.py @@ -69,10 +69,9 @@ def test_list_entries_show_details(monkeypatch, capsys): pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1 ) monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None) - monkeypatch.setattr(sys.stdin, "readline", lambda *a, **k: "b\n") monkeypatch.setattr( - "password_manager.manager.select.select", - lambda *a, **k: ([sys.stdin], [], []), + "password_manager.manager.timed_input", + lambda *a, **k: "b", ) inputs = iter(["1", "0"]) diff --git a/src/tests/test_manager_retrieve_totp.py b/src/tests/test_manager_retrieve_totp.py index e127773..b67fe27 100644 --- a/src/tests/test_manager_retrieve_totp.py +++ b/src/tests/test_manager_retrieve_totp.py @@ -49,10 +49,9 @@ def test_handle_retrieve_totp_entry(monkeypatch, capsys): pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1 ) monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None) - monkeypatch.setattr(sys.stdin, "readline", lambda *a, **k: "b\n") monkeypatch.setattr( - "password_manager.manager.select.select", - lambda *a, **k: ([sys.stdin], [], []), + "password_manager.manager.timed_input", + lambda *a, **k: "b", ) pm.handle_retrieve_entry() diff --git a/src/tests/test_secret_mode.py b/src/tests/test_secret_mode.py index 193094f..7087396 100644 --- a/src/tests/test_secret_mode.py +++ b/src/tests/test_secret_mode.py @@ -66,7 +66,7 @@ def test_totp_display_secret_mode(monkeypatch, capsys): pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 30 ) monkeypatch.setattr( - "password_manager.manager.select.select", + "password_manager.manager.timed_input", lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()), ) called = [] @@ -115,7 +115,7 @@ def test_totp_display_no_secret_mode(monkeypatch, capsys): pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 30 ) monkeypatch.setattr( - "password_manager.manager.select.select", + "password_manager.manager.timed_input", lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()), ) called = [] diff --git a/src/utils/imghdr_stub.py b/src/utils/imghdr_stub.py new file mode 100644 index 0000000..326824f --- /dev/null +++ b/src/utils/imghdr_stub.py @@ -0,0 +1,216 @@ +"""Compat module providing :mod:`imghdr` for Python 3.13+. + +This is a copy of Python 3.12's :mod:`imghdr` module which was removed in +Python 3.13. It is used by the ``pgpy`` dependency when deriving PGP keys. +""" + +from __future__ import annotations + +from os import PathLike +import warnings + +__all__ = ["what"] + +warnings._deprecated(__name__, remove=(3, 13)) + +# ------------------------- +# Recognize image headers +# ------------------------- + + +def what(file, h=None): + """Return the type of image contained in a file or byte stream.""" + f = None + try: + if h is None: + if isinstance(file, (str, PathLike)): + f = open(file, "rb") + h = f.read(32) + else: + location = file.tell() + h = file.read(32) + file.seek(location) + for tf in tests: + res = tf(h, f) + if res: + return res + finally: + if f: + f.close() + return None + + +# --------------------------------- +# Subroutines per image file type +# --------------------------------- + +tests: list = [] + + +def test_jpeg(h, f): + """Test for JPEG data with JFIF or Exif markers; and raw JPEG.""" + if h[6:10] in (b"JFIF", b"Exif"): + return "jpeg" + elif h[:4] == b"\xff\xd8\xff\xdb": + return "jpeg" + + +tests.append(test_jpeg) + + +def test_png(h, f): + """Verify if the image is a PNG.""" + if h.startswith(b"\211PNG\r\n\032\n"): + return "png" + + +tests.append(test_png) + + +def test_gif(h, f): + """Verify if the image is a GIF ('87 or '89 variants).""" + if h[:6] in (b"GIF87a", b"GIF89a"): + return "gif" + + +tests.append(test_gif) + + +def test_tiff(h, f): + """Verify if the image is a TIFF (Motorola or Intel).""" + if h[:2] in (b"MM", b"II"): + return "tiff" + + +tests.append(test_tiff) + + +def test_rgb(h, f): + """Test for the SGI image library.""" + if h.startswith(b"\001\332"): + return "rgb" + + +tests.append(test_rgb) + + +def test_pbm(h, f): + """Verify if the image is a PBM.""" + if len(h) >= 3 and h[0] == ord(b"P") and h[1] in b"14" and h[2] in b" \t\n\r": + return "pbm" + + +tests.append(test_pbm) + + +def test_pgm(h, f): + """Verify if the image is a PGM.""" + if len(h) >= 3 and h[0] == ord(b"P") and h[1] in b"25" and h[2] in b" \t\n\r": + return "pgm" + + +tests.append(test_pgm) + + +def test_ppm(h, f): + """Verify if the image is a PPM.""" + if len(h) >= 3 and h[0] == ord(b"P") and h[1] in b"36" and h[2] in b" \t\n\r": + return "ppm" + + +tests.append(test_ppm) + + +def test_rast(h, f): + """Test for the Sun raster file.""" + if h.startswith(b"\x59\xa6\x6a\x95"): + return "rast" + + +tests.append(test_rast) + + +def test_xbm(h, f): + """Verify if the image is a X bitmap.""" + if h.startswith(b"#define "): + return "xbm" + + +tests.append(test_xbm) + + +def test_bmp(h, f): + """Verify if the image is a BMP file.""" + if h.startswith(b"BM"): + return "bmp" + + +tests.append(test_bmp) + + +def test_webp(h, f): + """Verify if the image is a WebP.""" + if h.startswith(b"RIFF") and h[8:12] == b"WEBP": + return "webp" + + +tests.append(test_webp) + + +def test_exr(h, f): + """Verify if the image is an OpenEXR file.""" + if h.startswith(b"\x76\x2f\x31\x01"): + return "exr" + + +tests.append(test_exr) + + +# -------------------- +# Small test program +# -------------------- + + +def test(): # pragma: no cover - only used for manual testing + import sys + + recursive = 0 + if sys.argv[1:] and sys.argv[1] == "-r": + del sys.argv[1:2] + recursive = 1 + try: + if sys.argv[1:]: + testall(sys.argv[1:], recursive, 1) + else: + testall(["."], recursive, 1) + except KeyboardInterrupt: + sys.stderr.write("\n[Interrupted]\n") + sys.exit(1) + + +def testall(list, recursive, toplevel): # pragma: no cover - only for manual use + import sys + import os + + for filename in list: + if os.path.isdir(filename): + print(filename + "/:", end=" ") + if recursive or toplevel: + print("recursing down:") + import glob + + names = glob.glob(os.path.join(glob.escape(filename), "*")) + testall(names, recursive, 0) + else: + print("*** directory (use -r) ***") + else: + print(filename + ":", end=" ") + sys.stdout.flush() + try: + print(what(filename)) + except OSError: + print("*** not found ***") + + +if __name__ == "__main__": # pragma: no cover - manual run + test()