Passed
Push — master ( 601cfd...7bcbf1 )
by Michiel
22:46
created

ManifestTask::hashFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
namespace Phing\Task\System;
21
22
use Phing\Exception\BuildException;
23
use Phing\Io\File;
24
use Phing\Project;
25
use Phing\Task;
26
use Phing\Type\Element\FileSetAware;
27
28
/**
29
 * ManifestTask
30
 *
31
 * Generates a simple Manifest file with optional checksums.
32
 *
33
 *
34
 * Manifest schema:
35
 * ...
36
 * path/to/file     CHECKSUM    [CHECKSUM2]     [CHECKSUM3]
37
 * path/to/secondfile       CHECKSUM    [CHECKSUM2]     [CHECKSUM3]
38
 * ...
39
 *
40
 * Example usage:
41
 * <manifest checksum="crc32" file="${dir_build}/Manifest">
42
 *      <fileset refid="files_build" />
43
 * </manifest>
44
 *
45
 * <manifest checksum="md5,adler32,sha256" file="${dir_build}/Manifest">
46
 *      <fileset refid="files_build" />
47
 * </manifest>
48
 *
49
 * @author David Persson <davidpersson at qeweurope dot org>
50
 *
51
 * @package phing.tasks.ext
52
 *
53
 * @since 2.3.1
54
 */
55
class ManifestTask extends Task
56
{
57
    use FileSetAware;
58
59
    public $taskname = 'manifest';
60
61
    /**
62
     * Action
63
     *
64
     * "w" for reading in files from fileSet
65
     * and writing manifest
66
     *
67
     * or
68
     *
69
     * "r" for reading in files from fileSet
70
     * and checking against manifest
71
     *
72
     * @var string "r" or "w"
73
     */
74
    private $action = 'w';
75
76
    /**
77
     * Enable/Disable checksuming or/and select algorithm
78
     * true defaults to md5
79
     * false disables checksuming
80
     * string "md5,sha256,..." enables generation of multiple checksums
81
     * string "sha256" generates sha256 checksum only
82
     *
83
     * @var bool|string
84
     */
85
    private $checksum = false;
86
87
    /**
88
     * A string used in hashing method
89
     *
90
     * @var string
91
     */
92
    private $salt = '';
93
94
    /**
95
     * Holds some data collected during runtime
96
     *
97
     * @var array
98
     */
99
    private $meta = ['totalFileCount' => 0, 'totalFileSize' => 0];
100
101
    /**
102
     * @var File The target file passed in the buildfile.
103
     */
104
    private $file;
105
106
    /**
107
     * The setter for the attribute "file".
108
     * This is where the manifest will be written to/read from
109
     *
110
     * @param File $file Path to readable file
111
     *
112
     * @return void
113
     */
114
    public function setFile(File $file)
115
    {
116
        $this->file = $file;
117
    }
118
119
    /**
120
     * The setter for the attribute "checksum"
121
     *
122
     * @param mixed $mixed
123
     *
124
     * @return void
125
     */
126
    public function setChecksum($mixed)
127
    {
128
        if (is_string($mixed)) {
129
            $data = [strtolower($mixed)];
130
131
            if (strpos($data[0], ',')) {
132
                $data = explode(',', $mixed);
133
            }
134
135
            $this->checksum = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type array<integer,string> or string[] is incompatible with the declared type boolean|string of property $checksum.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
136
        } elseif ($mixed === true) {
137
            $this->checksum = ['md5'];
138
        }
139
    }
140
141
    /**
142
     * The setter for the optional attribute "salt"
143
     *
144
     * @param string $string
145
     *
146
     * @return void
147
     */
148
    public function setSalt($string)
149
    {
150
        $this->salt = $string;
151
    }
152
153
    /**
154
     * The init method: Do init steps.
155
     *
156
     * {@inheritdoc}
157
     *
158
     * @internal nothing to do here
159
     */
160
    public function init()
161
    {
162
    }
163
164
    /**
165
     * Delegate the work.
166
     *
167
     * {@inheritdoc}
168
     */
169
    public function main()
170
    {
171
        $this->validateAttributes();
172
173
        if ($this->action == 'w') {
174
            $this->write();
175
        } elseif ($this->action == 'r') {
176
            $this->read();
177
        }
178
    }
179
180
    /**
181
     * Creates Manifest file
182
     * Writes to $this->file
183
     *
184
     * @throws BuildException
185
     */
186
    private function write()
187
    {
188
        $project = $this->getProject();
189
190
        if (!touch($this->file->getPath())) {
191
            throw new BuildException("Unable to write to " . $this->file->getPath() . ".");
192
        }
193
194
        $this->log("Writing to " . $this->file->__toString(), Project::MSG_INFO);
195
196
        if (is_array($this->checksum)) {
0 ignored issues
show
introduced by
The condition is_array($this->checksum) is always false.
Loading history...
197
            $this->log("Using " . implode(', ', $this->checksum) . " for checksuming.", Project::MSG_INFO);
198
        }
199
200
        $manifest = [];
201
202
        foreach ($this->filesets as $fs) {
203
            $dir = $fs->getDir($this->project)->getPath();
204
205
            $ds = $fs->getDirectoryScanner($project);
206
            $fromDir = $fs->getDir($project);
0 ignored issues
show
Unused Code introduced by
The assignment to $fromDir is dead and can be removed.
Loading history...
207
            $srcFiles = $ds->getIncludedFiles();
0 ignored issues
show
Unused Code introduced by
The assignment to $srcFiles is dead and can be removed.
Loading history...
208
            $srcDirs = $ds->getIncludedDirectories();
0 ignored issues
show
Unused Code introduced by
The assignment to $srcDirs is dead and can be removed.
Loading history...
209
210
            foreach ($ds->getIncludedFiles() as $file_path) {
211
                $line = $file_path;
212
                if ($this->checksum) {
213
                    foreach ($this->checksum as $algo) {
0 ignored issues
show
Bug introduced by
The expression $this->checksum of type string|true is not traversable.
Loading history...
214
                        if (!$hash = $this->hashFile($dir . '/' . $file_path, $algo)) {
215
                            throw new BuildException("Hashing $dir/$file_path with $algo failed!");
216
                        }
217
218
                        $line .= "\t" . $hash;
219
                    }
220
                }
221
                $line .= "\n";
222
                $manifest[] = $line;
223
                $this->log("Adding file " . $file_path, Project::MSG_VERBOSE);
224
                $this->meta['totalFileCount']++;
225
                $this->meta['totalFileSize'] += filesize($dir . '/' . $file_path);
226
            }
227
        }
228
229
        file_put_contents($this->file, $manifest);
230
231
        $this->log(
232
            "Done. Total files: " . $this->meta['totalFileCount'] . ". Total file size: " . $this->meta['totalFileSize'] . " bytes.",
233
            Project::MSG_INFO
234
        );
235
    }
236
237
    /**
238
     * @todo implement
239
     */
240
    private function read()
241
    {
242
        throw new BuildException("Checking against manifest not yet supported.");
243
    }
244
245
    /**
246
     * Wrapper method for hash generation
247
     * Automatically selects extension
248
     * Falls back to built-in functions
249
     *
250
     * @link http://www.php.net/mhash
251
     * @link http://www.php.net/hash
252
     *
253
     * @param string $msg The string that should be hashed
254
     * @param string $algo Algorithm
255
     *
256
     * @return mixed  String on success, false if $algo is not available
257
     */
258
    private function hash($msg, $algo)
259
    {
260
        if (extension_loaded('hash')) {
261
            $algo = strtolower($algo);
262
263
            if (in_array($algo, hash_algos())) {
264
                return hash($algo, $this->salt . $msg);
265
            }
266
        }
267
268
        if (extension_loaded('mhash')) {
269
            $algo = strtoupper($algo);
270
271
            if (defined('MHASH_' . $algo)) {
272
                return mhash('MHASH_' . $algo, $this->salt . $msg);
0 ignored issues
show
Bug introduced by
'MHASH_' . $algo of type string is incompatible with the type integer expected by parameter $algo of mhash(). ( Ignorable by Annotation )

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

272
                return mhash(/** @scrutinizer ignore-type */ 'MHASH_' . $algo, $this->salt . $msg);
Loading history...
273
            }
274
        }
275
276
        switch (strtolower($algo)) {
277
            case 'md5':
278
                return md5($this->salt . $msg);
279
            case 'crc32':
280
                return abs(crc32($this->salt . $msg));
281
        }
282
283
        return false;
284
    }
285
286
    /**
287
     * Hash a file's contents
288
     *
289
     * @param string $file
290
     * @param string $algo
291
     *
292
     * @return mixed  String on success, false if $algo is not available
293
     */
294
    private function hashFile($file, $algo)
295
    {
296
        if (!file_exists($file)) {
297
            return false;
298
        }
299
300
        $msg = file_get_contents($file);
301
302
        return $this->hash($msg, $algo);
303
    }
304
305
    /**
306
     * Validates attributes coming in from XML
307
     *
308
     * @return void
309
     *
310
     * @throws BuildException
311
     */
312
    protected function validateAttributes()
313
    {
314
        if ($this->action != 'r' && $this->action != 'w') {
315
            throw new BuildException("'action' attribute has non valid value. Use 'r' or 'w'");
316
        }
317
318
        if (empty($this->salt)) {
319
            $this->log("No salt provided. Specify one with the 'salt' attribute.", Project::MSG_WARN);
320
        }
321
322
        if (null === $this->file && count($this->filesets) === 0) {
323
            throw new BuildException("Specify at least sources and destination - a file or a fileset.");
324
        }
325
326
        if (null !== $this->file && $this->file->exists() && $this->file->isDirectory()) {
327
            throw new BuildException("Destination file cannot be a directory.");
328
        }
329
    }
330
}
331