Skip to content

Commit

Permalink
fix backslash deescape for bytea PostgreSQL II
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Apr 27, 2024
1 parent 819fcd4 commit 5357d24
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 21 deletions.
33 changes: 24 additions & 9 deletions src/Persistence/Sql/Postgresql/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,38 @@ function ($sqlLeft, $sqlRight) use ($makeSqlFx) {
};

$escapeNonUtf8Fx = function ($sql, $neverBytea = false) use ($iffByteaSqlFx) {
$byteaSql = 'cast(replace(cast(' . $sql . ' as text), ' . $this->escapeStringLiteral('\\')
. ', ' . $this->escapeStringLiteral('\\\\') . ') as bytea)';

$sql = $neverBytea
? $byteaSql
: $iffByteaSqlFx(
$doubleBackslashesFx = function ($sql) {
return 'replace(' . $sql . ', ' . $this->escapeStringLiteral('\\')
. ', ' . $this->escapeStringLiteral('\\\\') . ')';
};

$byteaSql = 'cast(' . $doubleBackslashesFx('cast(' . $sql . ' as text)') . ' as bytea)';
if (!$neverBytea) {
$byteaSql = $iffByteaSqlFx(
$sql,
'decode(' . $iffByteaSqlFx(
$sql,
'substring(cast(' . $sql . ' as text) from 3)',
$doubleBackslashesFx('substring(cast(' . $sql . ' as text) from 3)'),
$this->escapeStringLiteral('')
) . ', ' . $this->escapeStringLiteral('hex') . ')',
$byteaSql
);
}

// 0x00 and 0x80+ bytes will be escaped as "\xddd"
$res = 'encode(' . $byteaSql . ', ' . $this->escapeStringLiteral('escape') . ')';

// replace backslash in "\xddd" for LIKE/REGEXP
$res = 'regexp_replace(' . $res . ', '
. $this->escapeStringLiteral('(?<!\\\)((\\\\\\\)*)\\\(\d\d\d)') . ', '
. $this->escapeStringLiteral('\1~~bytea~\3~~') . ', '
. $this->escapeStringLiteral('g') . ')';

// revert double backslashes
$res = 'replace(' . $res . ', ' . $this->escapeStringLiteral('\\\\')
. ', ' . $this->escapeStringLiteral('\\') . ')';

return 'replace(encode(' . $sql . ', ' . $this->escapeStringLiteral('escape') . '), '
. $this->escapeStringLiteral('\\\\') . ', ' . $this->escapeStringLiteral('\\') . ')';
return $res;
};

return $iffByteaSqlFx(
Expand Down
39 changes: 29 additions & 10 deletions tests/ConditionSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ public function testLikeCondition(string $type, bool $isBinary): void
['name' => 'Ja[n]e'],
['name' => 'Ja\[^n]e'],
['name' => 'heiß'],
['name' => 'hei\ß'],
['name' => 'hei\\'],
['name' => 'hei\123'],
]);

$findIdsLikeFx = function (string $field, string $value, bool $negated = false) use ($u) {
Expand Down Expand Up @@ -567,7 +570,7 @@ public function testLikeCondition(string $type, bool $isBinary): void
self::assertSame($isBinary ? [] : [9], $findIdsLikeFx('name', 'Heiß'));
self::assertSame([], $findIdsLikeFx('name', 'Joh'));
self::assertSame([1, 3], $findIdsLikeFx('name', 'Jo%'));
self::assertSame(array_values(array_diff(range(1, 9), [1, 3])), $findIdsLikeFx('name', 'Jo%', true));
self::assertSame(array_values(array_diff(range(1, 12), [1, 3])), $findIdsLikeFx('name', 'Jo%', true));
self::assertSame([1], $findIdsLikeFx('name', '%John%'));
self::assertSame([1], $findIdsLikeFx('name', 'Jo%n'));
self::assertSame([1], $findIdsLikeFx('name', 'J%n'));
Expand Down Expand Up @@ -607,6 +610,14 @@ public function testLikeCondition(string $type, bool $isBinary): void
self::assertSame([], $findIdsLikeFx('name', '%li\\\\\%e%'));
self::assertSame([5], $findIdsLikeFx('name', '%li\\\\\\\%e%'));
self::assertSame([], $findIdsLikeFx('name', '%li\\\\\\\\\%e%'));
self::assertSame([10], $findIdsLikeFx('name', 'hei\ß'));
self::assertSame([10], $findIdsLikeFx('name', 'hei\\'));
self::assertSame([11], $findIdsLikeFx('name', 'hei\\\\'));
self::assertSame([11], $findIdsLikeFx('name', 'hei\\\\\\'));
self::assertSame([], $findIdsLikeFx('name', 'hei\\\\\\\\'));
self::assertSame([12], $findIdsLikeFx('name', 'hei\123'));
self::assertSame([12], $findIdsLikeFx('name', 'hei\\\123'));
self::assertSame([], $findIdsLikeFx('name', 'hei\\\\\123'));

self::assertSame([4], $findIdsLikeFx('name', '%l_\ne%'));
self::assertSame([5], $findIdsLikeFx('name', '%l__\ne%'));
Expand Down Expand Up @@ -663,6 +674,9 @@ public function testRegexpCondition(string $type, bool $isBinary): void
['name' => 'Sa~ra'],
['name' => 'Sa$ra'],
['name' => 'heiß'],
['name' => 'hei\ß'],
['name' => 'hei\\'],
['name' => 'hei\123'],
]);

if ($this->getDatabasePlatform() instanceof SQLServerPlatform) {
Expand Down Expand Up @@ -710,7 +724,7 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame($isBinary ? [] : [13], $findIdsRegexFx('name', 'Heiß'));
self::assertSame([1], $findIdsRegexFx('name', 'Joh'));
self::assertSame([1], $findIdsRegexFx('name', 'ohn'));
self::assertSame([1, 2, 3, ...($this->getDatabasePlatform() instanceof OraclePlatform ? [] : [4]), 13], $findIdsRegexFx('name', 'a', true));
self::assertSame([1, 2, 3, ...($this->getDatabasePlatform() instanceof OraclePlatform ? [] : [4]), 13, 14, 15, 16], $findIdsRegexFx('name', 'a', true));

self::assertSame([1], $findIdsRegexFx('c', '1'));
self::assertSame([2], $findIdsRegexFx('c', '2000'));
Expand All @@ -719,9 +733,14 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame([1, 2], $findIdsRegexFx('rating', '\.5'));
self::assertSame([2], $findIdsRegexFx('rating', '2\.5'));

self::assertSame([9, 10], $findIdsRegexFx('name', '\\\\'));
self::assertSame([10], $findIdsRegexFx('name', '\\\\\\\\'));
self::assertSame([9, 10, 14, 15, 16], $findIdsRegexFx('name', '\\\\'));
self::assertSame([10, 15], $findIdsRegexFx('name', '\\\\\\\\'));
self::assertSame([], $findIdsRegexFx('name', '\\\\\\\\\\\\'));
self::assertSame([14], $findIdsRegexFx('name', 'hei\\'));
self::assertSame([15], $findIdsRegexFx('name', 'hei\\\\\\'));
self::assertSame([], $findIdsRegexFx('name', 'hei\\\\\\\\\\'));
self::assertSame([16], $findIdsRegexFx('name', 'hei\\\123'));
self::assertSame([], $findIdsRegexFx('name', 'hei\\\\\\\123'));
self::assertSame([7], $findIdsRegexFx('name', '\.'));
self::assertSame([12], $findIdsRegexFx('name', '\$'));
self::assertSame([8], $findIdsRegexFx('name', '/ra'));
Expand All @@ -739,12 +758,12 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame([5], $findIdsRegexFx('name', '\ ra'));
}

self::assertSame([2, 3, 13], $findIdsRegexFx('name', '.e'));
self::assertSame(array_values(array_diff(range(1, 13), [4])), $findIdsRegexFx('name', '.'));
self::assertSame([2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '.e'));
self::assertSame(array_values(array_diff(range(1, 16), [4])), $findIdsRegexFx('name', '.'));
self::assertSame([5, 6, 7, 8, 9, 11, 12], $findIdsRegexFx('name', 'Sa.ra'));
self::assertSame([2, 3, 13], $findIdsRegexFx('name', '[e]'));
self::assertSame([1, 2, 3, 13], $findIdsRegexFx('name', '[eo]'));
self::assertSame([1, 2, 3, ...($isBinary ? [] : [13])], $findIdsRegexFx('name', '[A-P][aeo]'));
self::assertSame([2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '[e]'));
self::assertSame([1, 2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '[eo]'));
self::assertSame([1, 2, 3, ...($isBinary ? [] : [13, 14, 15, 16])], $findIdsRegexFx('name', '[A-P][aeo]'));
self::assertSame([3], $findIdsRegexFx('name', 'o[^h]'));
self::assertSame([5, 6, 7, 8, 9, 10, 11, 12], $findIdsRegexFx('name', '^Sa'));
self::assertSame([], $findIdsRegexFx('name', '^ra'));
Expand Down Expand Up @@ -789,7 +808,7 @@ public function testRegexpCondition(string $type, bool $isBinary): void
if (!$this->getDatabasePlatform() instanceof PostgreSQLPlatform || !$isBinary) {
self::assertSame([13], $findIdsRegexFx('name', 'hei\w$'));
}
self::assertSame([10], $findIdsRegexFx('name', '\W\\\\'));
self::assertSame([10, 15], $findIdsRegexFx('name', '\W\\\\'));
if ($type !== 'string' && !$this->getDatabasePlatform() instanceof OraclePlatform) {
self::assertSame([5], $findIdsRegexFx('name', '\x20'));
self::assertSame([6], $findIdsRegexFx('name', '\n'));
Expand Down
4 changes: 2 additions & 2 deletions tests/Persistence/Sql/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ public function testWhereSpecialValues(): void
}
self::assertSame(
<<<'EOF'
where case when pg_typeof("name") = 'bytea'::regtype then replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then substring(cast("name" as text) from 3) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), repeat(chr(92), 2), chr(92)) like regexp_replace(replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), repeat(chr(92), 2), chr(92)), '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) else cast("name" as citext) like regexp_replace(:a, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) end
where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?<!\\)((\\\\)*)\\(\d\d\d)', '\1~~bytea~\3~~', 'g'), repeat(chr(92), 2), chr(92)) like regexp_replace(replace(regexp_replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), '(?<!\\)((\\\\)*)\\(\d\d\d)', '\1~~bytea~\3~~', 'g'), repeat(chr(92), 2), chr(92)), '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) else cast("name" as citext) like regexp_replace(:a, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) end
EOF,
(new PostgresqlQuery('[where]'))->where('name', 'like', 'foo')->render()[0]
);
Expand Down Expand Up @@ -911,7 +911,7 @@ public function testWhereSpecialValues(): void
}
self::assertSame(
<<<'EOF'
where case when pg_typeof("name") = 'bytea'::regtype then replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then substring(cast("name" as text) from 3) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), repeat(chr(92), 2), chr(92)) ~ replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), repeat(chr(92), 2), chr(92)) else cast("name" as citext) ~ :a end
where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?<!\\)((\\\\)*)\\(\d\d\d)', '\1~~bytea~\3~~', 'g'), repeat(chr(92), 2), chr(92)) ~ replace(regexp_replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), '(?<!\\)((\\\\)*)\\(\d\d\d)', '\1~~bytea~\3~~', 'g'), repeat(chr(92), 2), chr(92)) else cast("name" as citext) ~ :a end
EOF,
(new PostgresqlQuery('[where]'))->where('name', 'regexp', 'foo')->render()[0]
);
Expand Down

0 comments on commit 5357d24

Please sign in to comment.