diff --git a/src/main/php/io/streams/Buffer.class.php b/src/main/php/io/streams/Buffer.class.php new file mode 100755 index 000000000..5e76a7bef --- /dev/null +++ b/src/main/php/io/streams/Buffer.class.php @@ -0,0 +1,124 @@ += 0'); + } + + $this->files= $files instanceof Folder ? $files->getURI() : (string)$files; + $this->threshold= $threshold; + } + + /** Returns buffer size */ + public function size(): int { return $this->size; } + + /** Returns the underlying file, if any */ + public function file(): ?File { return $this->file; } + + /** Returns whether this buffer is draining */ + public function draining(): bool { return $this->draining; } + + /** + * Write a string + * + * @param var $arg + * @return void + * @throws lang.IllegalStateException + */ + public function write($bytes) { + if ($this->draining) throw new IllegalStateException('Started draining buffer'); + + $this->size+= strlen($bytes); + if ($this->size <= $this->threshold) { + $this->memory.= $bytes; + return; + } + + if (null === $this->file) { + $this->file= new File(tempnam($this->files, "b{$this->threshold}")); + $this->file->open(File::READWRITE); + $this->file->write($this->memory); + $this->memory= null; + } + $this->file->write($bytes); + } + + /** @return void */ + public function flush() { + $this->file && $this->file->flush(); + } + + /** + * Resets buffer to be able to read from the beginning + * + * @return void + */ + public function reset() { + $this->file ? $this->file->seek(0, SEEK_SET) : $this->pointer= 0; + $this->draining= true; + } + + /** @return int */ + public function available() { + return $this->draining + ? $this->size - ($this->file ? $this->file->tell() : $this->pointer) + : $this->size + ; + } + + /** + * Read a string + * + * @param int $limit + * @return string + */ + public function read($limit= 8192) { + if ($this->file) { + $this->draining || $this->file->seek(0, SEEK_SET) && $this->draining= true; + return (string)$this->file->read($limit); + } else { + $this->draining= true; + $chunk= substr($this->memory, $this->pointer, $limit); + $this->pointer+= strlen($chunk); + return $chunk; + } + } + + /** @return void */ + public function close() { + if (null === $this->file || !$this->file->isOpen()) return; + + $this->file->close(); + $this->file->unlink(); + } + + /** Ensure the file (if any) is closed */ + public function __destruct() { + $this->close(); + } +} \ No newline at end of file diff --git a/src/test/php/io/unittest/BufferTest.class.php b/src/test/php/io/unittest/BufferTest.class.php new file mode 100755 index 000000000..6ce774141 --- /dev/null +++ b/src/test/php/io/unittest/BufferTest.class.php @@ -0,0 +1,108 @@ +temp= Environment::tempDir(); + } + + #[Test] + public function can_create() { + new Buffer($this->temp, self::THRESHOLD); + } + + #[Test] + public function threshold_must_be_larger_than_zero() { + Assert::throws(IllegalArgumentException::class, fn() => new Buffer($this->temp, -1)); + } + + #[Test, Values([1, 127, 128])] + public function uses_memory_under_threshold($length) { + $bytes= str_repeat('*', $length); + + $fixture= new Buffer($this->temp, self::THRESHOLD); + $fixture->write($bytes); + + Assert::null($fixture->file()); + Assert::equals($length, $fixture->size()); + Assert::equals($bytes, $fixture->read()); + } + + #[Test, Values([129, 256, 1024])] + public function stores_file_when_exceeding_threshold($length) { + $bytes= str_repeat('*', $length); + + $fixture= new Buffer($this->temp, self::THRESHOLD); + $fixture->write($bytes); + + Assert::instance(File::class, $fixture->file()); + Assert::equals($length, $fixture->size()); + Assert::equals($bytes, $fixture->read()); + } + + #[Test, Values([127, 128, 129])] + public function read_after_eof($length) { + $bytes= str_repeat('*', $length); + + $fixture= new Buffer($this->temp, self::THRESHOLD); + $fixture->write($bytes); + + Assert::equals($length, $fixture->available()); + Assert::equals($bytes, $fixture->read()); + Assert::equals(0, $fixture->available()); + Assert::equals('', $fixture->read()); + } + + #[Test, Values([127, 128, 129])] + public function reset($length) { + $bytes= str_repeat('*', $length); + + $fixture= new Buffer($this->temp, self::THRESHOLD); + $fixture->write($bytes); + + Assert::equals($length, $fixture->available()); + Assert::equals($bytes, $fixture->read()); + + $fixture->reset(); + + Assert::equals($length, $fixture->available()); + Assert::equals($bytes, $fixture->read()); + } + + #[Test] + public function cannot_write_after_draining_started() { + $fixture= new Buffer($this->temp, self::THRESHOLD); + $fixture->write('Test'); + Assert::false($fixture->draining()); + + $fixture->read(); + Assert::true($fixture->draining()); + Assert::throws(IllegalStateException::class, fn() => $fixture->write('Test')); + } + + #[Test] + public function file_deleted_on_close() { + $fixture= new Buffer($this->temp, 0); + $fixture->write('Test'); + + $fixture->close(); + Assert::false($fixture->file()->exists()); + } + + #[Test] + public function double_close() { + $fixture= new Buffer($this->temp, 0); + $fixture->write('Test'); + + $fixture->close(); + $fixture->close(); + } +} \ No newline at end of file