1<?php
2
3namespace League\Flysystem;
4
5use League\Flysystem\Util\MimeType;
6use LogicException;
7
8class Util
9{
10    /**
11     * Get normalized pathinfo.
12     *
13     * @param string $path
14     *
15     * @return array pathinfo
16     */
17    public static function pathinfo($path)
18    {
19        $pathinfo = compact('path');
20
21        if ('' !== $dirname = dirname($path)) {
22            $pathinfo['dirname'] = static::normalizeDirname($dirname);
23        }
24
25        $pathinfo['basename'] = static::basename($path);
26
27        $pathinfo += pathinfo($pathinfo['basename']);
28
29        return $pathinfo + ['dirname' => ''];
30    }
31
32    /**
33     * Normalize a dirname return value.
34     *
35     * @param string $dirname
36     *
37     * @return string normalized dirname
38     */
39    public static function normalizeDirname($dirname)
40    {
41        return $dirname === '.' ? '' : $dirname;
42    }
43
44    /**
45     * Get a normalized dirname from a path.
46     *
47     * @param string $path
48     *
49     * @return string dirname
50     */
51    public static function dirname($path)
52    {
53        return static::normalizeDirname(dirname($path));
54    }
55
56    /**
57     * Map result arrays.
58     *
59     * @param array $object
60     * @param array $map
61     *
62     * @return array mapped result
63     */
64    public static function map(array $object, array $map)
65    {
66        $result = [];
67
68        foreach ($map as $from => $to) {
69            if ( ! isset($object[$from])) {
70                continue;
71            }
72
73            $result[$to] = $object[$from];
74        }
75
76        return $result;
77    }
78
79    /**
80     * Normalize path.
81     *
82     * @param string $path
83     *
84     * @throws LogicException
85     *
86     * @return string
87     */
88    public static function normalizePath($path)
89    {
90        return static::normalizeRelativePath($path);
91    }
92
93    /**
94     * Normalize relative directories in a path.
95     *
96     * @param string $path
97     *
98     * @throws LogicException
99     *
100     * @return string
101     */
102    public static function normalizeRelativePath($path)
103    {
104        $path = str_replace('\\', '/', $path);
105        $path = static::removeFunkyWhiteSpace($path);
106
107        $parts = [];
108
109        foreach (explode('/', $path) as $part) {
110            switch ($part) {
111                case '':
112                case '.':
113                break;
114
115            case '..':
116                if (empty($parts)) {
117                    throw new LogicException(
118                        'Path is outside of the defined root, path: [' . $path . ']'
119                    );
120                }
121                array_pop($parts);
122                break;
123
124            default:
125                $parts[] = $part;
126                break;
127            }
128        }
129
130        return implode('/', $parts);
131    }
132
133    /**
134     * Removes unprintable characters and invalid unicode characters.
135     *
136     * @param string $path
137     *
138     * @return string $path
139     */
140    protected static function removeFunkyWhiteSpace($path)
141    {
142        // We do this check in a loop, since removing invalid unicode characters
143        // can lead to new characters being created.
144        while (preg_match('#\p{C}+|^\./#u', $path)) {
145            $path = preg_replace('#\p{C}+|^\./#u', '', $path);
146        }
147
148        return $path;
149    }
150
151    /**
152     * Normalize prefix.
153     *
154     * @param string $prefix
155     * @param string $separator
156     *
157     * @return string normalized path
158     */
159    public static function normalizePrefix($prefix, $separator)
160    {
161        return rtrim($prefix, $separator) . $separator;
162    }
163
164    /**
165     * Get content size.
166     *
167     * @param string $contents
168     *
169     * @return int content size
170     */
171    public static function contentSize($contents)
172    {
173        return defined('MB_OVERLOAD_STRING') ? mb_strlen($contents, '8bit') : strlen($contents);
174    }
175
176    /**
177     * Guess MIME Type based on the path of the file and it's content.
178     *
179     * @param string          $path
180     * @param string|resource $content
181     *
182     * @return string|null MIME Type or NULL if no extension detected
183     */
184    public static function guessMimeType($path, $content)
185    {
186        $mimeType = MimeType::detectByContent($content);
187
188        if ( ! (empty($mimeType) || in_array($mimeType, ['application/x-empty', 'text/plain', 'text/x-asm']))) {
189            return $mimeType;
190        }
191
192        return MimeType::detectByFilename($path);
193    }
194
195    /**
196     * Emulate directories.
197     *
198     * @param array $listing
199     *
200     * @return array listing with emulated directories
201     */
202    public static function emulateDirectories(array $listing)
203    {
204        $directories = [];
205        $listedDirectories = [];
206
207        foreach ($listing as $object) {
208            list($directories, $listedDirectories) = static::emulateObjectDirectories($object, $directories, $listedDirectories);
209        }
210
211        $directories = array_diff(array_unique($directories), array_unique($listedDirectories));
212
213        foreach ($directories as $directory) {
214            $listing[] = static::pathinfo($directory) + ['type' => 'dir'];
215        }
216
217        return $listing;
218    }
219
220    /**
221     * Ensure a Config instance.
222     *
223     * @param null|array|Config $config
224     *
225     * @return Config config instance
226     *
227     * @throw  LogicException
228     */
229    public static function ensureConfig($config)
230    {
231        if ($config === null) {
232            return new Config();
233        }
234
235        if ($config instanceof Config) {
236            return $config;
237        }
238
239        if (is_array($config)) {
240            return new Config($config);
241        }
242
243        throw new LogicException('A config should either be an array or a Flysystem\Config object.');
244    }
245
246    /**
247     * Rewind a stream.
248     *
249     * @param resource $resource
250     */
251    public static function rewindStream($resource)
252    {
253        if (ftell($resource) !== 0 && static::isSeekableStream($resource)) {
254            rewind($resource);
255        }
256    }
257
258    public static function isSeekableStream($resource)
259    {
260        $metadata = stream_get_meta_data($resource);
261
262        return $metadata['seekable'];
263    }
264
265    /**
266     * Get the size of a stream.
267     *
268     * @param resource $resource
269     *
270     * @return int|null stream size
271     */
272    public static function getStreamSize($resource)
273    {
274        $stat = fstat($resource);
275
276        if ( ! is_array($stat) || ! isset($stat['size'])) {
277            return null;
278        }
279
280        return $stat['size'];
281    }
282
283    /**
284     * Emulate the directories of a single object.
285     *
286     * @param array $object
287     * @param array $directories
288     * @param array $listedDirectories
289     *
290     * @return array
291     */
292    protected static function emulateObjectDirectories(array $object, array $directories, array $listedDirectories)
293    {
294        if ($object['type'] === 'dir') {
295            $listedDirectories[] = $object['path'];
296        }
297
298        if ( ! isset($object['dirname']) || trim($object['dirname']) === '') {
299            return [$directories, $listedDirectories];
300        }
301
302        $parent = $object['dirname'];
303
304        while (isset($parent) && trim($parent) !== '' && ! in_array($parent, $directories)) {
305            $directories[] = $parent;
306            $parent = static::dirname($parent);
307        }
308
309        if (isset($object['type']) && $object['type'] === 'dir') {
310            $listedDirectories[] = $object['path'];
311
312            return [$directories, $listedDirectories];
313        }
314
315        return [$directories, $listedDirectories];
316    }
317
318    /**
319     * Returns the trailing name component of the path.
320     *
321     * @param string $path
322     *
323     * @return string
324     */
325    private static function basename($path)
326    {
327        $separators = DIRECTORY_SEPARATOR === '/' ? '/' : '\/';
328
329        $path = rtrim($path, $separators);
330
331        $basename = preg_replace('#.*?([^' . preg_quote($separators, '#') . ']+$)#', '$1', $path);
332
333        if (DIRECTORY_SEPARATOR === '/') {
334            return $basename;
335        }
336        // @codeCoverageIgnoreStart
337        // Extra Windows path munging. This is tested via AppVeyor, but code
338        // coverage is not reported.
339
340        // Handle relative paths with drive letters. c:file.txt.
341        while (preg_match('#^[a-zA-Z]{1}:[^\\\/]#', $basename)) {
342            $basename = substr($basename, 2);
343        }
344
345        // Remove colon for standalone drive letter names.
346        if (preg_match('#^[a-zA-Z]{1}:$#', $basename)) {
347            $basename = rtrim($basename, ':');
348        }
349
350        return $basename;
351        // @codeCoverageIgnoreEnd
352    }
353}
354