diff --git a/AUTHORS b/AUTHORS index db49b9eb4..6eb4011b6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -155,6 +155,8 @@ Contributors: * Chris (ChrisJr404) * Pieter Ouwerkerk (pouwerkerk) + * jackwalkerlabs (Codex-assisted) + Creator: -------- Amjith Ramanujam diff --git a/changelog.rst b/changelog.rst index fbdbc4917..995137e53 100644 --- a/changelog.rst +++ b/changelog.rst @@ -22,6 +22,9 @@ Features: Bug fixes: ---------- +* Preserve hashes, percent signs, commas and quotes in service-file passwords, + matching libpq. Quotes around service-file values are now literal; remove + quotes previously added solely to escape ConfigObj syntax. * Fix special commands being broken while explain mode (F5) is on. Every input was prefixed with ``EXPLAIN (...)`` and sent to the server as SQL, including backslash commands and the bare words ``exit``/``quit``, so ``\q``, ``\d``, diff --git a/pgcli/main.py b/pgcli/main.py index 8c172b85d..44ce5ccd6 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -1,5 +1,5 @@ from zoneinfo import ZoneInfoNotFoundError -from configobj import ConfigObj, ParseError +from configparser import ConfigParser from pgspecial.namedqueries import NamedQueries from .config import skip_initial_comment @@ -2213,14 +2213,13 @@ def parse_service_info(service): return None, service_file with open(service_file, newline="") as f: skipped_lines = skip_initial_comment(f) - try: - service_file_config = ConfigObj(f) - except ParseError as err: - err.line_number += skipped_lines - raise err + # libpq treats values literally: hashes, commas, quotes and percent + # signs are part of the value, not ConfigObj comments or syntax. + service_file_config = ConfigParser(interpolation=None, delimiters=("=",), comment_prefixes=("#",)) + service_file_config.read_file(itertools.chain(itertools.repeat("\n", skipped_lines), f), source=service_file) if service not in service_file_config: return None, service_file - service_conf = service_file_config.get(service) + service_conf = service_file_config[service] return service_conf, service_file diff --git a/tests/test_main.py b/tests/test_main.py index c8a28b419..62a94d213 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -21,6 +21,7 @@ get_connect_timeout, get_editor, notify_callback, + parse_service_info, PGCli, OutputSettings, COLOR_CODE_REGEX, @@ -590,6 +591,18 @@ def test_quoted_db_uri(tmpdir): mock_connect.assert_called_with(database="testdb[", host="baz.com", user="bar^", passwd="]foo") +@pytest.mark.parametrize("password", ["abc#def", "#leading", "abc #def", "abc%def", "a,b", '"quoted"', "'quoted'"]) +def test_pg_service_password_is_literal(tmp_path, monkeypatch, password): + service_file = tmp_path / "pg_service.conf" + service_file.write_text(f"# comment\n[myservice]\npassword={password}\n") + monkeypatch.setenv("PGSERVICEFILE", str(service_file)) + + config, filename = parse_service_info("myservice") + + assert filename == str(service_file) + assert config["password"] == password + + def test_pg_service_file(tmpdir): with mock.patch.object(PGCli, "connect") as mock_connect: cli = PGCli(pgclirc_file=str(tmpdir.join("rcfile")))