Passed
Push — master ( 6bc1c8...8ff536 )
by Morris
12:21
created

AssemblyStream::stream_seek()   B

Complexity

Conditions 8
Paths 21

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 21
nop 2
dl 0
loc 36
rs 8.4444
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Markus Goetz <[email protected]>
7
 * @author Robin Appelman <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 * @author Vincent Petry <[email protected]>
10
 *
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
27
namespace OCA\DAV\Upload;
28
29
use Sabre\DAV\IFile;
30
31
/**
32
 * Class AssemblyStream
33
 *
34
 * The assembly stream is a virtual stream that wraps multiple chunks.
35
 * Reading from the stream transparently accessed the underlying chunks and
36
 * give a representation as if they were already merged together.
37
 *
38
 * @package OCA\DAV\Upload
39
 */
40
class AssemblyStream implements \Icewind\Streams\File {
41
42
	/** @var resource */
43
	private $context;
44
45
	/** @var IFile[] */
46
	private $nodes;
47
48
	/** @var int */
49
	private $pos = 0;
50
51
	/** @var int */
52
	private $size = 0;
53
54
	/** @var resource */
55
	private $currentStream = null;
56
57
	/** @var int */
58
	private $currentNode = 0;
59
60
	/** @var int */
61
	private $currentNodeRead = 0;
62
63
	/**
64
	 * @param string $path
65
	 * @param string $mode
66
	 * @param int $options
67
	 * @param string &$opened_path
68
	 * @return bool
69
	 */
70
	public function stream_open($path, $mode, $options, &$opened_path) {
71
		$this->loadContext('assembly');
72
73
		$nodes = $this->nodes;
74
		// http://stackoverflow.com/a/10985500
75
		@usort($nodes, function (IFile $a, IFile $b) {
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for usort(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

75
		/** @scrutinizer ignore-unhandled */ @usort($nodes, function (IFile $a, IFile $b) {

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
76
			return strnatcmp($a->getName(), $b->getName());
77
		});
78
		$this->nodes = array_values($nodes);
79
		$this->size = array_reduce($this->nodes, function ($size, IFile $file) {
80
			return $size + $file->getSize();
81
		}, 0);
82
		return true;
83
	}
84
85
	/**
86
	 * @param int $offset
87
	 * @param int $whence
88
	 * @return bool
89
	 */
90
	public function stream_seek($offset, $whence = SEEK_SET) {
91
		if ($whence === SEEK_CUR) {
92
			$offset = $this->stream_tell() + $offset;
93
		} else if ($whence === SEEK_END) {
94
			$offset = $this->size + $offset;
95
		}
96
97
		if ($offset > $this->size) {
98
			return false;
99
		}
100
101
		$nodeIndex = 0;
102
		$nodeStart = 0;
103
		while (true) {
104
			if (!isset($this->nodes[$nodeIndex + 1])) {
105
				break;
106
			}
107
			$node = $this->nodes[$nodeIndex];
108
			if ($nodeStart + $node->getSize() > $offset) {
109
				break;
110
			}
111
			$nodeIndex++;
112
			$nodeStart += $node->getSize();
113
		}
114
115
		$stream = $this->getStream($this->nodes[$nodeIndex]);
116
		$nodeOffset = $offset - $nodeStart;
117
		if(fseek($stream, $nodeOffset) === -1) {
118
			return false;
119
		}
120
		$this->currentNode = $nodeIndex;
121
		$this->currentNodeRead = $nodeOffset;
122
		$this->currentStream = $stream;
123
		$this->pos = $offset;
124
125
		return true;
126
	}
127
128
	/**
129
	 * @return int
130
	 */
131
	public function stream_tell() {
132
		return $this->pos;
133
	}
134
135
	/**
136
	 * @param int $count
137
	 * @return string
138
	 */
139
	public function stream_read($count) {
140
		if (is_null($this->currentStream)) {
0 ignored issues
show
introduced by
The condition is_null($this->currentStream) is always false.
Loading history...
141
			if ($this->currentNode < count($this->nodes)) {
142
				$this->currentStream = $this->getStream($this->nodes[$this->currentNode]);
143
			} else {
144
				return '';
145
			}
146
		}
147
148
		do {
149
			$data = fread($this->currentStream, $count);
150
			$read = strlen($data);
151
			$this->currentNodeRead += $read;
152
153
			if (feof($this->currentStream)) {
154
				fclose($this->currentStream);
155
				$currentNodeSize = $this->nodes[$this->currentNode]->getSize();
156
				if ($this->currentNodeRead < $currentNodeSize) {
157
					throw new \Exception('Stream from assembly node shorter than expected, got ' . $this->currentNodeRead . ' bytes, expected ' . $currentNodeSize);
158
				}
159
				$this->currentNode++;
160
				$this->currentNodeRead = 0;
161
				if ($this->currentNode < count($this->nodes)) {
162
					$this->currentStream = $this->getStream($this->nodes[$this->currentNode]);
163
				} else {
164
					$this->currentStream = null;
165
				}
166
			}
167
			// if no data read, try again with the next node because
168
			// returning empty data can make the caller think there is no more
169
			// data left to read
170
		} while ($read === 0 && !is_null($this->currentStream));
171
172
		// update position
173
		$this->pos += $read;
174
		return $data;
175
	}
176
177
	/**
178
	 * @param string $data
179
	 * @return int
180
	 */
181
	public function stream_write($data) {
182
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
183
	}
184
185
	/**
186
	 * @param int $option
187
	 * @param int $arg1
188
	 * @param int $arg2
189
	 * @return bool
190
	 */
191
	public function stream_set_option($option, $arg1, $arg2) {
192
		return false;
193
	}
194
195
	/**
196
	 * @param int $size
197
	 * @return bool
198
	 */
199
	public function stream_truncate($size) {
200
		return false;
201
	}
202
203
	/**
204
	 * @return array
205
	 */
206
	public function stream_stat() {
207
		return [
208
			'size' => $this->size,
209
		];
210
	}
211
212
	/**
213
	 * @param int $operation
214
	 * @return bool
215
	 */
216
	public function stream_lock($operation) {
217
		return false;
218
	}
219
220
	/**
221
	 * @return bool
222
	 */
223
	public function stream_flush() {
224
		return false;
225
	}
226
227
	/**
228
	 * @return bool
229
	 */
230
	public function stream_eof() {
231
		return $this->pos >= $this->size || ($this->currentNode >= count($this->nodes) && $this->currentNode === null);
232
	}
233
234
	/**
235
	 * @return bool
236
	 */
237
	public function stream_close() {
238
		return true;
239
	}
240
241
242
	/**
243
	 * Load the source from the stream context and return the context options
244
	 *
245
	 * @param string $name
246
	 * @return array
247
	 * @throws \BadMethodCallException
248
	 */
249
	protected function loadContext($name) {
250
		$context = stream_context_get_options($this->context);
251
		if (isset($context[$name])) {
252
			$context = $context[$name];
253
		} else {
254
			throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
255
		}
256
		if (isset($context['nodes']) and is_array($context['nodes'])) {
257
			$this->nodes = $context['nodes'];
258
		} else {
259
			throw new \BadMethodCallException('Invalid context, nodes not set');
260
		}
261
		return $context;
262
	}
263
264
	/**
265
	 * @param IFile[] $nodes
266
	 * @return resource
267
	 *
268
	 * @throws \BadMethodCallException
269
	 */
270
	public static function wrap(array $nodes) {
271
		$context = stream_context_create([
272
			'assembly' => [
273
				'nodes' => $nodes
274
			]
275
		]);
276
		stream_wrapper_register('assembly', self::class);
277
		try {
278
			$wrapped = fopen('assembly://', 'r', null, $context);
279
		} catch (\BadMethodCallException $e) {
280
			stream_wrapper_unregister('assembly');
281
			throw $e;
282
		}
283
		stream_wrapper_unregister('assembly');
284
		return $wrapped;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $wrapped could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
285
	}
286
287
	/**
288
	 * @param IFile $node
289
	 * @return resource
290
	 */
291
	private function getStream(IFile $node) {
292
		$data = $node->get();
293
		if (is_resource($data)) {
294
			return $data;
295
		} else {
296
			$tmp = fopen('php://temp', 'w+');
297
			fwrite($tmp, $data);
298
			rewind($tmp);
299
			return $tmp;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $tmp could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
300
		}
301
	}
302
}
303