Redid some core IO stuff and fixed tests.

This commit is contained in:
flash 2018-01-02 19:39:21 +01:00
parent 5ec90ec1ba
commit eae6e12ac0
6 changed files with 344 additions and 330 deletions

View file

@ -1,167 +1,26 @@
<?php <?php
namespace Misuzu\IO; namespace Misuzu\IO;
use ErrorException;
/** /**
* Wrapper for f* (file stream) functions and other cool stuff. * Static file meta functions.
* @package Misuzu\IO * @package Misuzu\IO
* @author flashwave <me@flash.moe> * @author flashwave <me@flash.moe>
*/ */
class File class File
{ {
/** public static function open(string $filename): FileStream
* Open + Read flag.
*/
public const OPEN_READ = 'rb';
/**
* Open + Write flag.
*/
public const OPEN_WRITE = 'cb';
/**
* Open + Read + Write flag.
*/
public const OPEN_READ_WRITE = 'rb+';
/**
* Create (truncates!) + Write flag.
*/
public const CREATE_WRITE = 'wb';
/**
* Create (truncates!) + Read + Write flag.
*/
public const CREATE_READ_WRITE = 'wb+';
/**
* Open + Write flag.
*/
public const OPEN_CREATE_WRITE = 'xb';
/**
* Open or Create + Read + Write flag.
*/
public const OPEN_CREATE_READ_WRITE = 'xb+';
/**
* Resource/stream container.
* @var resource
*/
private $resource;
/**
* Filename.
* @var string
*/
public $name = '';
/**
* Real, fixed path.
* @var string
*/
public $path = '';
/**
* Filesize in bytes.
* @var int
*/
public $size = 0;
/**
* ID of file owner.
* @var int
*/
public $owner = 0;
/**
* ID of file's owning group.
* @var int
*/
public $group = 0;
/**
* Last time this file has been accessed.
* @var int
*/
public $accessTime = 0;
/**
* Last time this file has been modified.
* @var int
*/
public $modifiedTime = 0;
/**
* Current stream position.
* @var int
*/
public $position = 0;
/**
* Fixes path and opens resource.
* @param string $path
* @param string $mode
* @throws FileDoesNotExistException
* @throws IOException
*/
public function __construct(string $path, string $mode)
{ {
$path = Directory::fixSlashes($path); return new FileStream($filename, FileStream::MODE_READ_WRITE, true);
$this->path = realpath($path);
$this->name = basename($this->path);
try {
$this->resource = fopen($path, $mode, false);
} catch (ErrorException $e) {
throw new FileDoesNotExistException($e->getMessage());
}
if (!is_resource($this->resource)) {
throw new IOException('Failed to create resource.');
}
$this->updateMetaData();
}
/**
* Updates the meta data of the resource.
*/
private function updateMetaData(): void
{
$meta = fstat($this->resource);
$this->size = intval($meta['size']);
$this->owner = intval($meta['uid']);
$this->group = intval($meta['gid']);
$this->accessTime = intval($meta['atime']);
$this->modifiedTime = intval($meta['mtime']);
}
/**
* Updates the position variable.
*/
private function updatePosition(): void
{
$this->position = ftell($this->resource);
}
/**
* Calls the close method.
*/
public function __destruct()
{
$this->close();
} }
/** /**
* Creates an instance of a temporary file. * Creates an instance of a temporary file.
* @param string $prefix * @param string $prefix
* @return File * @return FileStream
*/ */
public static function temp(string $prefix = 'Misuzu'): File public static function temp(string $prefix = 'Misuzu'): FileStream
{ {
return new static(tempnam(sys_get_temp_dir(), $prefix)); return static::open(tempnam(sys_get_temp_dir(), $prefix));
} }
/** /**
@ -190,145 +49,4 @@ class File
unlink($path); unlink($path);
} }
/**
* Closes the resource context.
*/
public function close(): void
{
if (is_resource($this->resource)) {
fclose($this->resource);
}
}
/**
* Moves the position to 0.
*/
public function start(): void
{
rewind($this->resource);
$this->updatePosition();
}
/**
* Moves the position to the end of the file.
*/
public function end(): void
{
fseek($this->resource, 0, SEEK_END);
$this->updatePosition();
}
/**
* Sets the position.
* @param int $offset
* @param bool $relative
* @return int
*/
public function position(int $offset, bool $relative = false): int
{
fseek($this->resource, $offset, $relative ? SEEK_CUR : SEEK_SET);
$this->updatePosition();
return $this->position;
}
/**
* Checks if the current position is the end of the file.
* @return bool
*/
public function atEOF(): bool
{
return feof($this->resource);
}
/**
* Tries to find the position of a string in the context.
* @param string $string
* @return int
*/
public function find(string $string): int
{
while ($this->position < $this->size) {
$find = strpos($this->read(8192), $string);
if ($find !== false) {
return $find + $this->position;
}
$this->position($this->position + 8192);
}
return -1;
}
/**
* Locks the file and reads from it.
* @param int $length
* @throws IOException
* @return string
*/
public function read(int $length = null): string
{
if ($length === null) {
$length = $this->size;
$this->start();
}
flock($this->resource, LOCK_SH);
$data = fread($this->resource, $length);
flock($this->resource, LOCK_UN);
if ($data === false) {
throw new IOException('Read failed.');
}
$this->updateMetaData();
return $data;
}
/**
* Gets the character at the current position.
* @return string
*/
public function char(): string
{
return fgetc($this->resource);
}
/**
* Locks the file, writes to the stream and flushes to file.
* @param string $data
* @param int $length
* @throws IOException
*/
public function write(string $data, int $length = 0): void
{
if ($length > 0) {
$length = strlen($data);
}
flock($this->resource, LOCK_EX);
$write = fwrite($this->resource, $data, $length);
$flush = fflush($this->resource);
flock($this->resource, LOCK_UN);
if ($write === false || $flush === false) {
throw new IOException('Write failed.');
}
$this->updateMetaData();
$this->updatePosition();
}
/**
* The same as write except it moves to the end of the file first.
* @param string $data
* @param int $length
*/
public function append(string $data, int $length = 0): void
{
$this->end();
$this->write($data, $length);
}
} }

252
src/IO/FileStream.php Normal file
View file

@ -0,0 +1,252 @@
<?php
namespace Misuzu\IO;
use ErrorException;
class FileStream extends Stream
{
public const MODE_READ = 0x1;
public const MODE_WRITE = 0x2;
public const MODE_READ_WRITE = self::MODE_READ | self::MODE_WRITE;
private const MODE_TRUNCATE_RAW = 0x4;
public const MODE_TRUNCATE = self::MODE_TRUNCATE_RAW | self::MODE_WRITE;
private const MODE_APPEND_RAW = 0x8;
public const MODE_APPEND = self::MODE_APPEND_RAW | self::MODE_WRITE;
protected $fileHandle;
protected $filePath;
protected $fileMode;
protected $isLocked;
public function __construct(string $path, int $mode, bool $lock = true)
{
$this->isLocked = $lock;
$this->filePath = $path;
$this->fileMode = $mode;
try {
$this->fileHandle = fopen($this->filePath, static::constructFileMode($this->fileMode));
} catch (ErrorException $ex) {
throw new FileDoesNotExistException($ex->getMessage());
}
$this->ensureHandleActive();
if ($this->isLocked) {
flock($this->fileHandle, LOCK_EX | LOCK_NB);
}
}
public function __destruct()
{
if (!is_resource($this->fileHandle)) {
return;
}
$this->close();
}
protected static function constructFileMode(int $mode): string
{
$mode_read = ($mode & static::MODE_READ) > 0;
$mode_write = ($mode & static::MODE_WRITE) > 0;
$mode_truncate = ($mode & static::MODE_TRUNCATE_RAW) > 0;
$mode_append = ($mode & static::MODE_APPEND_RAW) > 0;
// why would you ever
if ($mode_append && $mode_truncate) {
throw new IOException("Can't append and truncate at the same time.");
}
if (($mode_append || $mode_truncate) && !$mode_write) {
throw new IOException("Can't append or truncate without write privileges.");
}
$mode_string = '';
if ($mode_append) {
$mode_string = 'a';
} elseif ($mode_truncate) {
$mode_string = 'w';
} elseif ($mode_write) {
$mode_string = 'c';
}
$mode_string .= 'b';
if ($mode_read) {
if (strlen($mode_string) < 2) {
$mode_string = 'r' . $mode_string;
} else {
$mode_string .= '+';
}
}
// should be at least two characters because of the b flag
if (strlen($mode_string) < 2) {
throw IOException('Failed to construct mode???');
}
return $mode_string;
}
public function getResource(): resource
{
$this->ensureHandleActive();
return $this->fileHandle;
}
protected function ensureHandleActive(): void
{
if (!is_resource($this->fileHandle)) {
throw new IOException("No active file handle.");
}
}
protected function ensureCanRead(): void
{
if (!$this->getCanRead()) {
throw new IOException('This stream cannot perform read operations.');
}
}
protected function ensureCanWrite(): void
{
if (!$this->getCanWrite()) {
throw new IOException('This stream cannot perform write operations.');
}
}
protected function ensureCanSeek(): void
{
if (!$this->getCanSeek()) {
throw new IOException('This stream cannot perform seek operations.');
}
}
protected function getCanRead(): bool
{
return ($this->fileMode & static::MODE_READ) > 0 && is_readable($this->filePath);
}
protected function getCanSeek(): bool
{
return ($this->fileMode & static::MODE_APPEND_RAW) == 0 && $this->getCanRead();
}
protected function getCanTimeout(): bool
{
return false;
}
protected function getCanWrite(): bool
{
return ($this->fileMode & static::MODE_WRITE) > 0 && is_writable($this->filePath);
}
protected function getLength(): int
{
$this->ensureHandleActive();
return fstat($this->fileHandle)['size'];
}
protected function getPosition(): int
{
$this->ensureHandleActive();
return ftell($this->fileHandle);
}
protected function getReadTimeout(): int
{
return -1;
}
protected function getWriteTimeout(): int
{
return -1;
}
public function flush(): void
{
$this->ensureHandleActive();
fflush($this->fileHandle);
}
public function close(): void
{
$this->ensureHandleActive();
if ($this->isLocked) {
flock($this->fileHandle, LOCK_UN | LOCK_NB);
}
fclose($this->fileHandle);
}
public function read(int $length): string
{
$this->ensureHandleActive();
$this->ensureCanRead();
$read = fread($this->fileHandle, $length);
if ($read === false) {
throw new IOException('Read failed.');
}
return $read;
}
public function readChar(): int
{
$this->ensureHandleActive();
return ord(fgetc($this->fileHandle));
}
public function write(string $data): int
{
$this->ensureHandleActive();
$this->ensureCanWrite();
$write = fwrite($this->fileHandle, $data);
if ($write === false) {
throw new IOException('Write failed.');
}
return $write;
}
public function writeChar(int $char): void
{
$this->ensureHandleActive();
$this->write(chr($char), 0, 1);
}
public function seek(int $offset, int $origin): void
{
$this->ensureHandleActive();
$this->ensureCanSeek();
switch ($origin) {
case Stream::ORIGIN_BEGIN:
$origin = SEEK_SET;
break;
case Stream::ORIGIN_END:
$origin = SEEK_END;
break;
case Stream::ORIGIN_CURRENT:
$origin = SEEK_CUR;
break;
default:
throw new IOException('Invalid seek origin.');
}
if (fseek($this->fileHandle, $offset, $origin) !== 0) {
throw new IOException('Seek operation failed.');
}
}
}

40
src/IO/Stream.php Normal file
View file

@ -0,0 +1,40 @@
<?php
namespace Misuzu\IO;
abstract class Stream
{
public const ORIGIN_CURRENT = 0;
public const ORIGIN_BEGIN = 1;
public const ORIGIN_END = 2;
public function __get(string $name)
{
$name = 'get' . ucfirst($name);
if (method_exists(static::class, $name)) {
return $this->{$name}();
}
// todo: specialised exception type
throw new IOException('Invalid property.');
}
abstract protected function getCanRead(): bool;
abstract protected function getCanSeek(): bool;
abstract protected function getCanTimeout(): bool;
abstract protected function getCanWrite(): bool;
abstract protected function getLength(): int;
abstract protected function getPosition(): int;
abstract protected function getReadTimeout(): int;
abstract protected function getWriteTimeout(): int;
abstract public function flush(): void;
abstract public function close(): void;
abstract public function seek(int $offset, int $origin): void;
abstract public function read(int $length): string;
abstract public function readChar(): int;
abstract public function write(string $data): int;
abstract public function writeChar(int $char): void;
}

View file

@ -40,9 +40,9 @@ class Zalgo
public const ZALGO_MODE_MAX = 3; public const ZALGO_MODE_MAX = 3;
private const ZALGO_MODES = [ private const ZALGO_MODES = [
ZALGO_MODE_MINI, self::ZALGO_MODE_MINI,
ZALGO_MODE_NORMAL, self::ZALGO_MODE_NORMAL,
ZALGO_MODE_MAX, self::ZALGO_MODE_MAX,
]; ];
public const ZALGO_DIR_UP = 0x1; public const ZALGO_DIR_UP = 0x1;
@ -105,7 +105,7 @@ class Zalgo
} }
$str .= $char; $str .= $char;
switch ($mode) { switch ($mode) {
case self::ZALGO_MODE_MINI: case self::ZALGO_MODE_MINI:
$num_up = mt_rand(0, 8); $num_up = mt_rand(0, 8);
@ -129,9 +129,11 @@ class Zalgo
if ($going_up) { if ($going_up) {
$str .= self::getZalgo(self::ZALGO_CHARS_UP, $num_up); $str .= self::getZalgo(self::ZALGO_CHARS_UP, $num_up);
} }
if ($going_mid) { if ($going_mid) {
$str .= self::getZalgo(self::ZALGO_CHARS_MIDDLE, $num_mid); $str .= self::getZalgo(self::ZALGO_CHARS_MIDDLE, $num_mid);
} }
if ($going_down) { if ($going_down) {
$str .= self::getZalgo(self::ZALGO_CHARS_DOWN, $num_down); $str .= self::getZalgo(self::ZALGO_CHARS_DOWN, $num_down);
} }

View file

@ -4,6 +4,7 @@ namespace MisuzuTests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Misuzu\IO\Directory; use Misuzu\IO\Directory;
use Misuzu\IO\File; use Misuzu\IO\File;
use Misuzu\IO\FileStream;
define('WORKING_DIR', sys_get_temp_dir() . '/MisuzuFileSystemTest' . time()); define('WORKING_DIR', sys_get_temp_dir() . '/MisuzuFileSystemTest' . time());
@ -34,48 +35,48 @@ class FileSystemTest extends TestCase
public function testCreateFile() public function testCreateFile()
{ {
$file = new File(WORKING_DIR . '/file', File::CREATE_READ_WRITE); $file = File::open(WORKING_DIR . '/file');
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$file->close(); $file->close();
} }
public function testWriteFile() public function testWriteFile()
{ {
$file = new File(WORKING_DIR . '/file', File::OPEN_WRITE); $file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_TRUNCATE, false);
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$file->write('misuzu'); $file->write('misuzu');
$this->assertEquals(6, $file->size); $this->assertEquals(6, $file->length);
$file->close(); $file->close();
} }
public function testAppendFile() public function testAppendFile()
{ {
$file = new File(WORKING_DIR . '/file', File::OPEN_WRITE); $file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_APPEND);
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$file->append(' test'); $file->write(' test');
$this->assertEquals(11, $file->size); $this->assertEquals(11, $file->length);
$file->close(); $file->close();
} }
public function testPosition() public function testPosition()
{ {
$file = new File(WORKING_DIR . '/file', File::OPEN_READ); $file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_READ);
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$file->start(); $file->seek(0, FileStream::ORIGIN_BEGIN);
$this->assertEquals(0, $file->position); $this->assertEquals(0, $file->position);
$file->end(); $file->seek(0, FileStream::ORIGIN_END);
$this->assertEquals($file->size, $file->position); $this->assertEquals($file->length, $file->position);
$file->position(4); $file->seek(4, FileStream::ORIGIN_BEGIN);
$this->assertEquals(4, $file->position); $this->assertEquals(4, $file->position);
$file->position(4, true); $file->seek(4, FileStream::ORIGIN_CURRENT);
$this->assertEquals(8, $file->position); $this->assertEquals(8, $file->position);
$file->close(); $file->close();
@ -83,34 +84,24 @@ class FileSystemTest extends TestCase
public function testRead() public function testRead()
{ {
$file = new File(WORKING_DIR . '/file', File::OPEN_READ); $file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_READ);
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$this->assertEquals('misuzu test', $file->read()); $this->assertEquals('misuzu test', $file->read($file->length));
$file->position(7); $file->seek(7, FileStream::ORIGIN_BEGIN);
$this->assertEquals('test', $file->read()); $this->assertEquals('test', $file->read(4));
$file->close();
}
public function testFind()
{
$file = new File(WORKING_DIR . '/file', File::OPEN_READ);
$this->assertInstanceOf(File::class, $file);
$this->assertEquals(7, $file->find('test'));
$file->close(); $file->close();
} }
public function testChar() public function testChar()
{ {
$file = new File(WORKING_DIR . '/file', File::OPEN_READ); $file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_READ);
$this->assertInstanceOf(File::class, $file); $this->assertInstanceOf(FileStream::class, $file);
$file->position(3); $file->seek(3, FileStream::ORIGIN_BEGIN);
$this->assertEquals('s', $file->char()); $this->assertEquals(ord('u'), $file->readChar());
$file->close(); $file->close();
} }

View file

@ -10,8 +10,19 @@ class ZalgoTest extends TestCase
public function testStrip() public function testStrip()
{ {
$this->assertEquals(TEST_STRING, Zalgo::strip(Zalgo::run(TEST_STRING, Zalgo::ZALGO_MODE_MINI))); $this->assertEquals(
$this->assertEquals(TEST_STRING, Zalgo::strip(Zalgo::run(TEST_STRING, Zalgo::ZALGO_MODE_NORMAL))); static::TEST_STRING,
$this->assertEquals(TEST_STRING, Zalgo::strip(Zalgo::run(TEST_STRING, Zalgo::ZALGO_MODE_MAX))); Zalgo::strip(Zalgo::run(static::TEST_STRING, Zalgo::ZALGO_MODE_MINI))
);
$this->assertEquals(
static::TEST_STRING,
Zalgo::strip(Zalgo::run(static::TEST_STRING, Zalgo::ZALGO_MODE_NORMAL))
);
$this->assertEquals(
static::TEST_STRING,
Zalgo::strip(Zalgo::run(static::TEST_STRING, Zalgo::ZALGO_MODE_MAX))
);
} }
} }