psycopg.git / summary / log / commit / refs

commit    c69d0efe59b3487c4a2e2bacb2daa5c19a39f2ee
Author:   Daniele Varrazzo <daniele.varrazzo@gmail.com>
Date:     Wed Nov 04 01:17:44 2020 +0000

    Text dumping raises DataError if it contains a NUL byte
    
    Note that psycopg2 was raising ValueError, but DataError is more
    appropriate. If the param is binary, Postgres will raise
    .CharacterNotInRepertoire, which is a DataError subclass.


psycopg3/psycopg3/types/text.py | 19 ++++++++++++++++--- tests/types/test_text.py | 21 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/psycopg3/psycopg3/types/text.py b/psycopg3/psycopg3/types/text.py index 1c3073cf..8d82c963 100644 --- a/psycopg3/psycopg3/types/text.py +++ b/psycopg3/psycopg3/types/text.py @@ -10,15 +10,14 @@ from typing import Optional, Union, TYPE_CHECKING from ..oids import builtins, INVALID_OID from ..adapt import Dumper, Loader from ..proto import AdaptContext, EncodeFunc, DecodeFunc +from ..errors import DataError from ..pq import Escaping if TYPE_CHECKING: from ..pq.proto import Escaping as EscapingProto -@Dumper.text(str) -@Dumper.binary(str) -class StringDumper(Dumper): +class _StringDumper(Dumper): def __init__(self, src: type, context: AdaptContext): super().__init__(src, context) @@ -31,10 +30,24 @@ class StringDumper(Dumper): else: self._encode = codecs.lookup("utf8").encode + +@Dumper.binary(str) +class StringBinaryDumper(_StringDumper): def dump(self, obj: str) -> bytes: return self._encode(obj)[0] +@Dumper.text(str) +class StringDumper(_StringDumper): + def dump(self, obj: str) -> bytes: + if "\x00" in obj: + raise DataError( + "PostgreSQL text fields cannot contain NUL (0x00) bytes" + ) + else: + return self._encode(obj)[0] + + @Loader.text(builtins["text"].oid) @Loader.binary(builtins["text"].oid) @Loader.text(builtins["varchar"].oid) diff --git a/tests/types/test_text.py b/tests/types/test_text.py index d15db13a..3c5f47ac 100644 --- a/tests/types/test_text.py +++ b/tests/types/test_text.py @@ -1,6 +1,7 @@ import pytest -from psycopg3 import DatabaseError, sql +import psycopg3 +from psycopg3 import sql from psycopg3.adapt import Format eur = "\u20ac" @@ -30,6 +31,22 @@ def test_quote_1char(conn): assert cur.fetchone()[0] is True, chr(i) +@pytest.mark.parametrize("fmt_in", [Format.TEXT, Format.BINARY]) +def test_dump_zero(conn, fmt_in): + cur = conn.cursor() + ph = "%s" if fmt_in == Format.TEXT else "%b" + s = "foo\x00bar" + with pytest.raises(psycopg3.DataError): + cur.execute(f"select {ph}", (s,)) + + +def test_quote_zero(conn): + cur = conn.cursor() + s = "foo\x00bar" + with pytest.raises(psycopg3.DataError): + cur.execute(sql.SQL("select {}").format(sql.Literal(s))) + + # the only way to make this pass is to reduce %% -> % every time # not only when there are query arguments # see https://github.com/psycopg/psycopg2/issues/825 @@ -108,7 +125,7 @@ def test_load_badenc(conn, typename, fmt_out): cur = conn.cursor(format=fmt_out) conn.client_encoding = "latin1" - with pytest.raises(DatabaseError): + with pytest.raises(psycopg3.DatabaseError): cur.execute(f"select chr(%s::int)::{typename}", (ord(eur),)) [parent: 97ed2cf385ff]