<?php
declare(strict_types=1);
namespace ParagonIE\Halite\Stream;
use \ParagonIE\Halite\Contract\StreamInterface;
use \ParagonIE\Halite\Alerts as CryptoException;
use \ParagonIE\Halite\Util as CryptoUtil;
/**
* Contrast with ReadOnlyFile: does not prevent race conditions by itself
*/
class MutableFile implements StreamInterface
{
const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default
private $closeAfter = false;
private $fp;
private $pos;
private $stat = [];
public function __construct($file)
{
if (is_string($file)) {
$this->fp = \fopen($file, 'wb');
$this->closeAfter = true;
$this->pos = 0;
$this->stat = \fstat($this->fp);
} elseif (is_resource($file)) {
$this->fp = $file;
$this->pos = \ftell($this->fp);
$this->stat = \fstat($this->fp);
} else {
throw new \ParagonIE\Halite\Alerts\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
}
public function close()
{
if ($this->closeAfter) {
$this->closeAfter = false;
\fclose($this->fp);
\clearstatcache();
}
}
public function __destruct()
{
$this->close();
}
/**
* Read from a stream; prevent partial reads
*
* @param int $num
* @return string
* @throws CryptoException\AccessDenied
* @throws CryptoException\CannotPerformOperation
*/
public function readBytes(int $num, bool $skipTests = false): string
{
if ($num <= 0) {
throw new \Exception('num < 0');
}
if (($this->pos + $num) > $this->stat['size']) {
throw new CryptoException\CannotPerformOperation('Out-of-bounds read');
}
$buf = '';
$remaining = $num;
do {
if ($remaining <= 0) {
break;
}
$read = \fread($this->fp, $remaining);
if ($read === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$buf .= $read;
$readSize = CryptoUtil::safeStrlen($read);
$this->pos += $readSize;
$remaining -= $readSize;
} while ($remaining > 0);
return $buf;
}
/**
* Write to a stream; prevent partial writes
*
* @param string $buf
* @param int $num (number of bytes)
* @return int
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\CannotPerformOperation
*/
public function writeBytes(string $buf, int $num = null): int
{
$bufSize = CryptoUtil::safeStrlen($buf);
if ($num === null || $num > $bufSize) {
$num = $bufSize;
}
if ($num < 0) {
throw new CryptoException\CannotPerformOperation('num < 0');
}
$remaining = $num;
do {
if ($remaining <= 0) {
break;
}
$written = \fwrite($this->fp, $buf, $remaining);
if ($written === false) {
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
$buf = CryptoUtil::safeSubstr($buf, $written, null);
$this->pos += $written;
$this->stat = \fstat($this->fp);
$remaining -= $written;
} while ($remaining > 0);
return $num;
}
/**
* Set the current cursor position to the desired location
*
* @param int $i
* @return boolean
* @throws CryptoException\CannotPerformOperation
*/
public function reset(int $i = 0): bool
{
$this->pos = $i;
if (\fseek($this->fp, $i, SEEK_SET) === 0) {
return true;
}
throw new CryptoException\CannotPerformOperation(
'fseek() failed'
);
}
}
|