Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.05% covered (warning)
86.05%
148 / 172
54.84% covered (warning)
54.84%
17 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 2
Stream
87.06% covered (warning)
87.06%
148 / 170
58.62% covered (warning)
58.62%
17 / 29
91.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 new
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 open
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 openOrCreate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 openStandardInput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 openStandardOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 openStandardError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 openMemory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 openTemporary
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getState
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getMetaData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 seek
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 seekHead
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 seekTail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOffset
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 isEnd
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 flush
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 writeBinary
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 writeBom
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 writeString
33.33% covered (danger)
33.33%
4 / 12
0.00% covered (danger)
0.00%
0 / 1
12.41
 writeLine
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readBinary
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 readBom
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 readBinaryContents
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 readStringContents
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 readLine
98.04% covered (success)
98.04%
50 / 51
0.00% covered (danger)
0.00%
0 / 1
16
 release
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValidType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
LocalNoReleaseStream
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 2
6
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 release
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\IO;
6
7use PeServer\Core\Binary;
8use PeServer\Core\Collection\Arr;
9use PeServer\Core\Encoding;
10use PeServer\Core\Errors\ErrorHandler;
11use PeServer\Core\IO\File;
12use PeServer\Core\IO\IOUtility;
13use PeServer\Core\IO\StreamMetaData;
14use PeServer\Core\ResourceBase;
15use PeServer\Core\ResultData;
16use PeServer\Core\Text;
17use PeServer\Core\Throws\ArgumentException;
18use PeServer\Core\Throws\IOException;
19use PeServer\Core\Throws\StreamException;
20
21/**
22 * ストリーム。
23 *
24 * @phpstan-extends ResourceBase<mixed>
25 */
26class Stream extends ResourceBase
27{
28    #region define
29
30    /** 読み込みモード。 */
31    public const MODE_READ = 1;
32    /** 書き込みモード。 */
33    public const MODE_WRITE = 2;
34    /** 読み書きモード。 */
35    public const MODE_EDIT = 3;
36
37    /** シーク位置: 絶対値。 */
38    public const WHENCE_SET = SEEK_SET;
39    /** シーク位置: 現在位置。 */
40    public const WHENCE_CURRENT = SEEK_CUR;
41    /** シーク位置: 末尾(設定値は負数となる)。 */
42    public const WHENCE_TAIL = SEEK_END;
43
44    #endregion
45
46    #region variable
47
48    /** 文字列として扱うエンコーディング。(バイナリデータは気にしなくてよい) */
49    private Encoding $encoding;
50
51    /**
52     * 改行文字。
53     *
54     * 書き込み時に使用される(読み込み時は頑張る)
55     */
56    public string $newLine = PHP_EOL;
57
58    #endregion
59
60    /**
61     * 生成。
62     *
63     * 内部的にしか使わない。
64     *
65     * @param $resource ファイルリソース。
66     * @param Encoding|null $encoding
67     */
68    public function __construct(
69        $resource,
70        ?Encoding $encoding = null
71    ) {
72        parent::__construct($resource);
73
74        $this->encoding = $encoding ?? Encoding::getDefaultEncoding();
75    }
76
77    #region function
78
79    /**
80     * `fopen` を使用してファイルストリームを生成。
81     *
82     * 原則以下の処理を使ってればよい。
83     * * `self::create`
84     * * `self::open`
85     * * `self::openOrCreate`
86     * * `self::openStandardInput`
87     * * `self::openStandardOutput`
88     * * `self::openStandardError`
89     * * `self::openMemory`
90     * * `self::openTemporary`
91     *
92     * @param string $path ファイルパス。
93     * @param string $mode `fopen:mode` を参照。
94     * @param Encoding|null $encoding
95     * @return self
96     * @throws IOException
97     * @see https://www.php.net/manual/function.fopen.php
98     */
99    public static function new(string $path, string $mode, ?Encoding $encoding = null): self
100    {
101        if (strpos($mode, 'b', 0) === false) {
102            $mode .= 'b';
103        }
104
105        $result = ErrorHandler::trap(fn () => fopen($path, $mode));
106        if ($result->isFailureOrFalse()) {
107            throw new IOException($path);
108        }
109
110        return new self($result->value, $encoding);
111    }
112
113    /**
114     * 新規ファイルのファイルストリームを生成。
115     *
116     * @param string $path ファイルパス。
117     * @param Encoding|null $encoding
118     * @return self
119     * @throws IOException 既にファイルが存在する。
120     */
121    public static function create(string $path, ?Encoding $encoding = null): self
122    {
123        if (File::exists($path)) {
124            throw new IOException($path);
125        }
126
127        return self::new($path, 'x+', $encoding);
128    }
129
130    /**
131     * 既存ファイルからファイルストリームを生成。
132     *
133     * @param string $path ファイルパス。
134     * @param self::MODE_* $mode `self::MODE_*` を指定。 `self::MODE_CRETE`: ファイルが存在しない場合失敗する。
135     * @param Encoding|null $encoding
136     * @return self
137     * @throws IOException
138     */
139    public static function open(string $path, int $mode, ?Encoding $encoding = null): self
140    {
141        $openMode = match ($mode) {
142            self::MODE_READ => 'r',
143            self::MODE_WRITE => 'a',
144            self::MODE_EDIT => 'r+',
145        };
146
147        if ($mode === self::MODE_WRITE || $mode == self::MODE_EDIT) {
148            if (!File::exists($path)) {
149                throw new IOException($path);
150            }
151        }
152
153        return self::new($path, $openMode, $encoding);
154    }
155
156    /**
157     * 既存ファイルからファイルストリームを生成、既存ファイルが存在しない場合新規ファイルを作成してファイルストリームを生成。
158     *
159     * @param string $path ファイルパス。
160     * @param int $mode `self::MODE_*` を指定。 `self::MODE_CRETE`: ファイルが存在しない場合に空ファイルが作成される(読むしかできないけど開ける。意味があるかは知らん)。
161     * @phpstan-param self::MODE_* $mode
162     * @param Encoding|null $encoding
163     * @return self
164     * @throws IOException
165     */
166    public static function openOrCreate(string $path, int $mode, ?Encoding $encoding = null): self
167    {
168        $openMode = match ($mode) {
169            self::MODE_READ => 'r',
170            self::MODE_WRITE => 'a',
171            self::MODE_EDIT => 'r+',
172        };
173
174        if ($mode === self::MODE_READ) {
175            if (!File::exists($path)) {
176                File::createEmptyFileIfNotExists($path);
177            }
178        }
179
180        return self::new($path, $openMode, $encoding);
181    }
182
183    /**
184     * 標準エラーストリームを開く。
185     *
186     * @param Encoding|null $encoding
187     * @return self
188     */
189    public static function openStandardInput(?Encoding $encoding = null): self
190    {
191        return new LocalNoReleaseStream(STDIN, $encoding);
192    }
193    /**
194     * 標準出力ストリームを開く。
195     *
196     * @param Encoding|null $encoding
197     * @return self
198     */
199    public static function openStandardOutput(?Encoding $encoding = null): self
200    {
201        return new LocalNoReleaseStream(STDOUT, $encoding);
202    }
203    /**
204     * 標準エラーストリームを開く。
205     *
206     * @param Encoding|null $encoding
207     * @return self
208     */
209    public static function openStandardError(?Encoding $encoding = null): self
210    {
211        return new LocalNoReleaseStream(STDERR, $encoding);
212    }
213
214    /**
215     * メモリストリームを開く。
216     *
217     * @param Encoding|null $encoding
218     * @return self
219     */
220    public static function openMemory(?Encoding $encoding = null): self
221    {
222        return self::new('php://memory', 'r+', $encoding);
223    }
224
225    /**
226     * 一時メモリストリームを開く。
227     *
228     * @param int|null $memoryByteSize 指定した値を超過した際に一時ファイルに置き換わる。`null`の場合は 2MB(`php://temp` 参照のこと)。
229     * @param Encoding|null $encoding
230     * @return self
231     */
232    public static function openTemporary(?int $memoryByteSize = null, ?Encoding $encoding = null): self
233    {
234        $path = 'php://temp';
235
236        if ($memoryByteSize !== null) {
237            if ($memoryByteSize < 0) {
238                throw new ArgumentException('$byteSize: ' . $memoryByteSize);
239            }
240            if ($memoryByteSize) {
241                //cspell:disable-next-line
242                $path .= '/maxmemory:' . (string)$memoryByteSize;
243            }
244        }
245
246        return self::new($path, 'r+', $encoding);
247    }
248
249    public function getState(): IOState
250    {
251        $this->throwIfDisposed();
252
253        $result = ErrorHandler::trap(fn () => fstat($this->resource));
254        if ($result->isFailureOrFalse()) {
255            throw new IOException();
256        }
257
258        return IOState::createFromStat($result->value);
259    }
260
261    public function getMetaData(): StreamMetaData
262    {
263        $this->throwIfDisposed();
264
265        $values = stream_get_meta_data($this->resource);
266        return StreamMetaData::createFromStream($values);
267    }
268
269    /**
270     * 現在位置をシーク。
271     *
272     * @param int $offset
273     * @param int $whence
274     * @phpstan-param self::WHENCE_* $whence
275     * @return bool
276     * @see https://www.php.net/manual/function.fseek.php
277     */
278    public function seek(int $offset, int $whence): bool
279    {
280        $this->throwIfDisposed();
281
282        $result = fseek($this->resource, $offset, $whence);
283        return $result === 0;
284    }
285
286    /**
287     * 先頭へシーク。
288     *
289     * @return bool
290     * @see https://www.php.net/manual/function.rewind.php
291     */
292    public function seekHead(): bool
293    {
294        $this->throwIfDisposed();
295
296        return rewind($this->resource);
297    }
298
299    /**
300     * 末尾へシーク。
301     *
302     * @return bool
303     */
304    public function seekTail(): bool
305    {
306        return $this->seek(0, self::WHENCE_TAIL);
307    }
308
309    /**
310     * 現在位置を取得。
311     *
312     * @return int
313     * @return-param non-negative-int
314     * @throws StreamException
315     * @see https://www.php.net/manual/function.ftell.php
316     */
317    public function getOffset(): int
318    {
319        $this->throwIfDisposed();
320
321        $result = ftell($this->resource);
322        if ($result === false) {
323            throw new StreamException();
324        }
325        return $result;
326    }
327
328    /**
329     * EOFか。
330     *
331     * `feof` ラッパー。
332     *
333     * @return bool
334     * @see https://www.php.net/manual/function.feof.php
335     */
336    public function isEnd(): bool
337    {
338        if ($this->isDisposed()) {
339            return false;
340        }
341
342        return feof($this->resource);
343    }
344
345    /**
346     * フラッシュ。
347     *
348     * @throws StreamException
349     * @see https://www.php.net/manual/function.fflush.php
350     */
351    public function flush(): void
352    {
353        $this->throwIfDisposed();
354
355        $result = fflush($this->resource);
356        if (!$result) {
357            throw new StreamException();
358        }
359    }
360
361    /**
362     * バイナリ書き込み。
363     *
364     * @param Binary $data データ。
365     * @param int|null $byteSize 書き込みサイズ。
366     * @phpstan-param non-negative-int|null $byteSize
367     * @return int 書き込んだバイトサイズ。
368     * @phpstan-return non-negative-int
369     * @throws StreamException
370     * @see https://www.php.net/manual/function.fwrite.php
371     */
372    public function writeBinary(Binary $data, ?int $byteSize = null): int
373    {
374        $this->throwIfDisposed();
375
376        $result = ErrorHandler::trap(fn () => fwrite($this->resource, $data->raw, $byteSize));
377        if ($result->isFailureOrFalse()) {
378            throw new StreamException();
379        }
380
381        return $result->value;
382    }
383
384    /**
385     * 現在のエンコーディングを使用してBOMを書き込み。
386     *
387     * * 現在位置に書き込む点に注意(シーク位置が先頭以外であれば無視される)。
388     * * エンコーディングがBOM情報を持っていれば出力されるためBOM不要な場合は使用しないこと。
389     *
390     * @return int 書き込まれたバイトサイズ。
391     * @phpstan-return non-negative-int
392     */
393    public function writeBom(): int
394    {
395        $this->throwIfDisposed();
396
397        if ($this->getOffset() !== 0) {
398            return 0;
399        }
400
401        $bom = $this->encoding->getByteOrderMark();
402        if ($bom->count()) {
403            return $this->writeBinary($bom);
404        }
405
406        return 0;
407    }
408
409    /**
410     * 文字列書き込み。
411     *
412     * @param string $data データ。
413     * @param int|null $count 文字数。
414     * @phpstan-param non-negative-int|null $count
415     * @return int 書き込まれたバイト数。
416     * @phpstan-return non-negative-int
417     */
418    public function writeString(string $data, ?int $count = null): int
419    {
420        $this->throwIfDisposed();
421
422        if (!Text::getByteCount($data)) {
423            return 0;
424        }
425
426        if ($count === null) {
427            return $this->writeBinary($this->encoding->getBinary($data));
428        }
429
430        if ($count === 0) {
431            return 0;
432        }
433
434        $dataLength = Text::getLength($data);
435        if ($dataLength <= $count) {
436            return $this->writeBinary($this->encoding->getBinary($data));
437        }
438
439        $s = Text::substring($data, 0, $count);
440        return $this->writeBinary($this->encoding->getBinary($s));
441    }
442
443    /**
444     * 文字列を改行付きで書き込み。
445     *
446     * @param string $data
447     * @return int 書き込まれたバイト数。
448     * @phpstan-return non-negative-int
449     */
450    public function writeLine(string $data): int
451    {
452        return $this->writeString($data . $this->newLine);
453    }
454
455    /**
456     * バイナリ読み込み。
457     *
458     * @param int $byteSize 読み込みバイトサイズ。
459     * @phpstan-param positive-int $byteSize
460     * @return Binary 読み込んだデータ。
461     * @throws StreamException
462     * @see https://www.php.net/manual/function.fread.php
463     */
464    public function readBinary(int $byteSize): Binary
465    {
466        $this->throwIfDisposed();
467
468        $result = ErrorHandler::trap(fn () => fread($this->resource, $byteSize));
469        if ($result->isFailureOrFalse()) {
470            throw new StreamException();
471        }
472
473        return new Binary($result->value);
474    }
475
476    /**
477     * 現在のエンコーディングを使用してBOMを読み取る。
478     *
479     * * 現在位置から読み込む点に注意(シーク位置が先頭以外であれば無視される)。
480     * * 読み込まれた場合(エンコーディングがBOMを持っていて合致した場合)はその分読み進められる。
481     *
482     * @return bool BOMが読み込まれたか。
483     */
484    public function readBom(): bool
485    {
486        $this->throwIfDisposed();
487
488        if ($this->getOffset() !== 0) {
489            return false;
490        }
491
492        $bom = $this->encoding->getByteOrderMark();
493        $bomLength = $bom->count();
494        if (!$bomLength) {
495            return false;
496        }
497
498        $readBuffer = $this->readBinary($bomLength);
499
500        if ($bom->isEquals($readBuffer)) {
501            return true;
502        }
503
504        $this->seek(-$readBuffer->count(), self::WHENCE_CURRENT);
505        return false;
506    }
507
508    /**
509     * 残りのストリームを全てバイナリとして読み込み。
510     *
511     * @param int|null $byteSize 読み込む最大バイト数。`null`で全て。
512     * @phpstan-param non-negative-int|null $byteSize
513     * @param int $offset 読み込みを開始する前に移動する位置。
514     * @return Binary
515     * @throws StreamException
516     * @see https://www.php.net/manual/function.stream-get-contents.php
517     */
518    public function readBinaryContents(?int $byteSize = null, int $offset = -1): Binary
519    {
520        $this->throwIfDisposed();
521
522        $result = stream_get_contents($this->resource, $byteSize, $offset);
523        if ($result === false) {
524            throw new StreamException();
525        }
526
527        return new Binary($result);
528    }
529
530    /**
531     * 残りのストリームを全て文字列として読み込み。
532     *
533     * エンコーディングにより復元不可の可能性あり。
534     *
535     * @return string
536     * @throws StreamException
537     */
538    public function readStringContents(): string
539    {
540        $result = $this->readBinaryContents();
541        if (!$result->count()) {
542            return Text::EMPTY;
543        }
544
545        return $this->encoding->toString($result);
546    }
547
548    /**
549     * 現在のストリーム位置から1行分のデータを取得。
550     *
551     * * 位置を進めたり戻したりするので操作可能なストリームで処理すること。
552     * * エンコーディングにより復元不可の可能性あり。
553     *
554     * @param int $bufferByteSize 1回読み進めるにあたり予め取得するサイズ。このサイズが改行(CR,LF)のサイズより小さい場合は改行(CR,LF)のサイズが設定される(1指定のutf16とか)
555     * @phpstan-param positive-int $bufferByteSize
556     * @return string
557     */
558    public function readLine(int $bufferByteSize = 1024): string
559    {
560        $this->throwIfDisposed();
561
562        if ($bufferByteSize < 1) { //@phpstan-ignore-line [DOCTYPE]
563            throw new ArgumentException('$bufferByteSize');
564        }
565
566        $cr = $this->encoding->getBinary("\r")->raw;
567        $lf = $this->encoding->getBinary("\n")->raw;
568        $newlineWidth = strlen($cr);
569
570        if ($bufferByteSize < $newlineWidth) {
571            $bufferByteSize = $newlineWidth;
572        }
573
574        $startOffset = $this->getOffset();
575
576        $totalCount = 0;
577        $totalBuffer = '';
578
579        $findCr = false;
580        $findLf = false;
581        $hasNewLine = false;
582
583        while (!$this->isEnd()) {
584            $binary = $this->readBinary($bufferByteSize);
585            $currentLength = $binary->count();
586            if (!$currentLength) {
587                break;
588            }
589
590            $currentBuffer = $binary->raw;
591            $currentOffset = 0;
592
593            while ($currentOffset < $currentLength) {
594                if (!$findCr) {
595                    $findCr = !substr_compare($currentBuffer, $cr, $currentOffset, $newlineWidth, false);
596                    if (!$findCr) {
597                        $findLf = !substr_compare($currentBuffer, $lf, $currentOffset, $newlineWidth, false);
598                    }
599                    $currentOffset += $newlineWidth;
600                }
601                if ($findLf) {
602                    $hasNewLine = true;
603                    break;
604                }
605                if ($findCr && $currentOffset < $currentLength) {
606                    $findLf = !substr_compare($currentBuffer, $lf, $currentOffset, $newlineWidth, false);
607                    if ($findLf) {
608                        $currentOffset += $newlineWidth;
609                    }
610                    $hasNewLine = true;
611                    break;
612                }
613            }
614
615            $totalBuffer .= $currentBuffer;
616            $totalCount += $currentOffset;
617
618            if ($hasNewLine) {
619                break;
620            }
621        }
622
623        if ($hasNewLine) {
624            $dropWidth = 0;
625            if ($findCr) {
626                $dropWidth += $newlineWidth;
627            }
628            if ($findLf) {
629                $dropWidth += $newlineWidth;
630            }
631            $this->seek($startOffset + $totalCount, self::WHENCE_SET);
632            $raw = substr($totalBuffer, 0, $totalCount - $dropWidth);
633            $str = $this->encoding->toString(new Binary($raw));
634
635            return $str;
636        }
637
638        return $this->encoding->toString(new Binary($totalBuffer));
639    }
640
641    #endregion
642
643    #region ResourceBase
644
645    protected function release(): void
646    {
647        fclose($this->resource);
648    }
649
650    protected function isValidType(string $resourceType): bool
651    {
652        return $resourceType === 'stream';
653    }
654
655    #endregion
656}
657
658//phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses
659class LocalNoReleaseStream extends Stream
660{
661    /**
662     * 生成
663     *
664     * @param $resource ファイルリソース。
665     * @param Encoding|null $encoding
666     */
667    public function __construct(
668        $resource,
669        ?Encoding $encoding = null
670    ) {
671        parent::__construct($resource, $encoding);
672    }
673
674    protected function release(): void
675    {
676        //NOP
677    }
678}