Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.30% covered (success)
97.30%
36 / 37
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalFile
97.30% covered (success)
97.30%
36 / 37
83.33% covered (warning)
83.33%
5 / 6
13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 open
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 read
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 assertRead
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 close
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 discard
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * SPDX-License-Identifier: EUPL-1.2
5 *
6 * This file is part of icap-flow.
7 *
8 * Licensed under the EUPL, Version 1.2 only (the "Licence");
9 * you may not use this work except in compliance with the Licence.
10 * You may obtain a copy of the Licence at:
11 *
12 *     https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the Licence is distributed on an "AS IS" basis,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 */
18
19declare(strict_types=1);
20
21namespace Ndrstmr\Icap;
22
23use Amp\Cancellation;
24use Amp\CancelledException;
25use Amp\File\File;
26use Amp\File\Whence;
27use Ndrstmr\Icap\Exception\IcapStorageException;
28
29/**
30 * A local file that the client opened once for one operation and reads through {@see FileCalls}, so that the event
31 * loop keeps running while the file is read. The size is the end of the open file, and reads and the close go to that
32 * same file; the path is not looked at again. Only a regular file is opened, and its size is what the
33 * announced length of the body is checked against.
34 *
35 * The cancellation (the end of the operation) ends every wait for the driver. A scan ends with {@see close()} when it
36 * went through, and with {@see discard()} otherwise.
37 *
38 * @internal
39 */
40final class LocalFile
41{
42    /** The most one read asks of the driver, and so the buffer a file needs per operation. */
43    public const int BLOCK_SIZE = 65536;
44
45    /** Whether the last read confirmed the end of the file. */
46    private bool $atEnd = false;
47
48    private function __construct(
49        private ?File $file,
50        private readonly FileCalls $calls,
51        private readonly Cancellation $cancellation,
52        public readonly int $size,
53    ) {
54    }
55
56    /**
57     * @param string $path a path of the local file system; a relative one is taken from the working directory now
58     *
59     * @throws IcapStorageException When the file cannot be opened or measured
60     * @throws CancelledException   When the cancellation ends the wait for the driver
61     */
62    public static function open(string $path, Cancellation $cancellation): self
63    {
64        // The worker has the working directory it started in, which is not necessarily this one.
65        if (preg_match('~^(?:[\\\\/]|[A-Za-z]:[\\\\/])~', $path) !== 1) {
66            $directory = getcwd();
67            if ($directory === false) {
68                throw new IcapStorageException('Unable to resolve the relative path of file: ' . $path);
69            }
70            $path = $directory . DIRECTORY_SEPARATOR . $path;
71        }
72
73        $calls = FileCalls::current();
74        $file = $calls->open($path, $cancellation);
75
76        try {
77            $size = $calls->run($cancellation, static function () use ($file): int {
78                $size = $file->seek(0, Whence::End);
79                $file->seek(0);
80
81                return $size;
82            }, 'Unable to determine the size of file: ' . $path);
83        } catch (\Throwable $e) {
84            $calls->detach($file);
85            throw $e;
86        }
87
88        return new self($file, $calls, $cancellation, $size);
89    }
90
91    /**
92     * The next bytes of the file, at most {@see BLOCK_SIZE}, or null once the end of the file is confirmed. A driver
93     * that returns nothing without having reached the end would make the caller wait for ever.
94     *
95     * @param int<1, max> $length
96     *
97     * @throws IcapStorageException When the file cannot be read
98     * @throws CancelledException   When the cancellation ends the wait for the driver
99     */
100    public function read(int $length): ?string
101    {
102        $file = $this->file ?? throw new \LogicException('The file has been closed.');
103
104        $bytes = $this->calls->run(
105            $this->cancellation,
106            static fn (): ?string => $file->read(null, min($length, self::BLOCK_SIZE)),
107            'Unable to read the file to its end.',
108        );
109        if ($bytes === '') {
110            throw new IcapStorageException('Unable to read the file to its end.');
111        }
112        $this->atEnd = $bytes === null;
113
114        return $bytes;
115    }
116
117    /**
118     * Fails unless the file was read to its end. A formatter or transport that took less of the request than the
119     * file leaves the server to judge a body that is not the file, and its answer is no verdict on it.
120     *
121     * @throws IcapStorageException When no read has confirmed the end of the file
122     */
123    public function assertRead(): void
124    {
125        if (!$this->atEnd) {
126            throw new IcapStorageException('The request did not read the file to its end, so no verdict is one on the whole file.');
127        }
128    }
129
130    /**
131     * Ends a scan that went through: closes the file, waits for the close as long as the operation lives and reports
132     * a close that fails.
133     *
134     * @throws IcapStorageException When the file cannot be closed
135     * @throws CancelledException   When the cancellation ends the wait for the driver
136     */
137    public function close(): void
138    {
139        $file = $this->file;
140        $this->file = null;
141
142        if ($file !== null) {
143            $this->calls->close($file, $this->cancellation);
144        }
145    }
146
147    /**
148     * Ends a scan that failed or was given up. The close goes on without anyone waiting for it, and what it reports is
149     * not wanted over the failure that ended the scan. Does nothing after {@see close()}.
150     */
151    public function discard(): void
152    {
153        $file = $this->file;
154        $this->file = null;
155
156        if ($file !== null) {
157            $this->calls->detach($file);
158        }
159    }
160}