Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore binary offsets of PDOStatement parameters #5897

Open
wants to merge 4 commits into
base: 3.9.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/Driver/IBMDB2/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use Throwable;

use function assert;
use function db2_bind_param;
use function db2_execute;
use function error_get_last;
use function fclose;
use function fseek;
use function ftell;
use function func_num_args;
use function is_int;
use function is_resource;
Expand All @@ -28,6 +31,7 @@
use const DB2_LONG;
use const DB2_PARAM_FILE;
use const DB2_PARAM_IN;
use const SEEK_SET;

final class Statement implements StatementInterface
{
Expand Down Expand Up @@ -213,8 +217,28 @@ private function createTemporaryFile()
*/
private function copyStreamToStream($source, $target): void
{
$resetTo = false;
if (stream_get_meta_data($source)['seekable']) {
$resetTo = ftell($source);
}

if (@stream_copy_to_stream($source, $target) === false) {
throw CannotCopyStreamToStream::new(error_get_last());
$copyToStreamError = error_get_last();
if ($resetTo !== false) {
try {
fseek($source, $resetTo, SEEK_SET);
} catch (Throwable $e) {
// Swallow, we want the original exception from stream_copy_to_stream
}
}

throw CannotCopyStreamToStream::new($copyToStreamError);
}

if ($resetTo === false) {
return;
}

fseek($source, $resetTo, SEEK_SET);
}
}
30 changes: 23 additions & 7 deletions src/Driver/Mysqli/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
use function count;
use function feof;
use function fread;
use function fseek;
use function ftell;
use function func_num_args;
use function get_resource_type;
use function is_int;
use function is_resource;
use function str_repeat;
use function stream_get_meta_data;

use const SEEK_SET;

final class Statement implements StatementInterface
{
Expand Down Expand Up @@ -213,15 +218,26 @@ private function bindTypedParameters(): void
private function sendLongData(array $streams): void
{
foreach ($streams as $paramNr => $stream) {
while (! feof($stream)) {
$chunk = fread($stream, 8192);
$resetTo = false;
if (stream_get_meta_data($stream)['seekable']) {
$resetTo = ftell($stream);
}

if ($chunk === false) {
throw FailedReadingStreamOffset::new($paramNr);
}
try {
while (! feof($stream)) {
$chunk = fread($stream, 8192);

if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
throw StatementError::new($this->stmt);
if ($chunk === false) {
throw FailedReadingStreamOffset::new($paramNr);
}

if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
throw StatementError::new($this->stmt);
}
}
} finally {
if ($resetTo !== false) {
fseek($stream, $resetTo, SEEK_SET);
}
}
}
Expand Down
94 changes: 91 additions & 3 deletions src/Driver/OCI8/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;

use function fseek;
use function ftell;
use function func_num_args;
use function is_int;
use function is_resource;
use function oci_bind_by_name;
use function oci_execute;
use function oci_new_descriptor;
use function stream_get_meta_data;

use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_COMMIT_ON_SUCCESS;
use const OCI_D_LOB;
use const OCI_NO_AUTO_COMMIT;
use const OCI_TEMP_BLOB;
use const SEEK_SET;
use const SQLT_CHR;

final class Statement implements StatementInterface
Expand All @@ -34,6 +39,9 @@ final class Statement implements StatementInterface
/** @var array<int,string> */
private array $parameterMap;

/** @var mixed[]|null */
private ?array $paramResources = null;

private ExecutionMode $executionMode;

/**
Expand Down Expand Up @@ -65,6 +73,10 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool
);
}

if ($type === ParameterType::BINARY || $type === ParameterType::LARGE_OBJECT) {
$this->trackParamResource($value);
}

return $this->bindParam($param, $value, $type);
}

Expand Down Expand Up @@ -164,11 +176,87 @@ public function execute($params = null): ResultInterface
$mode = OCI_NO_AUTO_COMMIT;
}

$ret = @oci_execute($this->statement, $mode);
if (! $ret) {
throw Error::new($this->statement);
$resourceOffsets = $this->getResourceOffsets();
try {
$ret = @oci_execute($this->statement, $mode);
if (! $ret) {
throw Error::new($this->statement);
}
} finally {
if ($resourceOffsets !== null) {
$this->restoreResourceOffsets($resourceOffsets);
}
}

return new Result($this->statement);
}

/**
* Track a binary parameter reference at binding time. These
* are cached for later analysis by the getResourceOffsets.
*
* @param mixed $resource
*/
private function trackParamResource($resource): void
{
if (! is_resource($resource)) {
return;
}

$this->paramResources ??= [];
$this->paramResources[] = $resource;
}

/**
* Determine the offset that any resource parameters needs to be
* restored to after the statement is executed. Call immediately
* before execute (not during bindValue) to get the most accurate offset.
*
* @return int[]|null Return offsets to restore if needed. The array may be sparse.
*/
private function getResourceOffsets(): ?array
{
if ($this->paramResources === null) {
return null;
}

$resourceOffsets = null;
foreach ($this->paramResources as $index => $resource) {
$position = false;
if (stream_get_meta_data($resource)['seekable']) {
$position = ftell($resource);
}

if ($position === false) {
continue;
}

$resourceOffsets ??= [];
$resourceOffsets[$index] = $position;
}

if ($resourceOffsets === null) {
$this->paramResources = null;
}

return $resourceOffsets;
}

/**
* Restore resource offsets moved by PDOStatement->execute
*
* @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets.
*/
private function restoreResourceOffsets(?array $resourceOffsets): void
{
if ($resourceOffsets === null || $this->paramResources === null) {
return;
}

foreach ($resourceOffsets as $index => $offset) {
fseek($this->paramResources[$index], $offset, SEEK_SET);
}

$this->paramResources = null;
}
}
98 changes: 98 additions & 0 deletions src/Driver/PDO/SQLSrv/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@
use Doctrine\DBAL\Driver\Exception\UnknownParameterType;
use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware;
use Doctrine\DBAL\Driver\PDO\Statement as PDOStatement;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use PDO;

use function fseek;
use function ftell;
use function func_num_args;
use function is_resource;
use function stream_get_meta_data;

use const SEEK_SET;

final class Statement extends AbstractStatementMiddleware
{
private PDOStatement $statement;

/** @var mixed[]|null */
private ?array $paramResources = null;

/** @internal The statement can be only instantiated by its driver connection. */
public function __construct(PDOStatement $statement)
{
Expand Down Expand Up @@ -104,6 +114,94 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool
);
}

if ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) {
$this->trackParamResource($value);
}

return $this->bindParam($param, $value, $type);
}

/**
* {@inheritDoc}
*/
public function execute($params = null): Result
{
$resourceOffsets = $this->getResourceOffsets();
try {
return parent::execute($params);
} finally {
if ($resourceOffsets !== null) {
$this->restoreResourceOffsets($resourceOffsets);
}
}
}

/**
* Track a binary parameter reference at binding time. These
* are cached for later analysis by the getResourceOffsets.
*
* @param mixed $resource
*/
private function trackParamResource($resource): void
{
if (! is_resource($resource)) {
return;
}

$this->paramResources ??= [];
$this->paramResources[] = $resource;
}

/**
* Determine the offset that any resource parameters needs to be
* restored to after the statement is executed. Call immediately
* before execute (not during bindValue) to get the most accurate offset.
*
* @return int[]|null Return offsets to restore if needed. The array may be sparse.
*/
private function getResourceOffsets(): ?array
{
if ($this->paramResources === null) {
return null;
}

$resourceOffsets = null;
foreach ($this->paramResources as $index => $resource) {
$position = false;
if (stream_get_meta_data($resource)['seekable']) {
$position = ftell($resource);
}

if ($position === false) {
continue;
}

$resourceOffsets ??= [];
$resourceOffsets[$index] = $position;
}

if ($resourceOffsets === null) {
$this->paramResources = null;
}

return $resourceOffsets;
}

/**
* Restore resource offsets moved by PDOStatement->execute
*
* @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets.
*/
private function restoreResourceOffsets(?array $resourceOffsets): void
{
if ($resourceOffsets === null || $this->paramResources === null) {
return;
}

foreach ($resourceOffsets as $index => $offset) {
fseek($this->paramResources[$index], $offset, SEEK_SET);
}

$this->paramResources = null;
}
}
Loading
Loading