Timestamps::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * this file is part of pipelines
5
 *
6
 * Copyright (c) 2017-2019 Tom Klingenberg <[email protected]>
7
 *
8
 * terms specific to this file (the "this software and associated
9
 * documentation files"):
10
 *
11
 * Copyright (c) 2015 Jordi Boggiano
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is furnished
18
 * to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
 * THE SOFTWARE.
30
 */
31
namespace Ktomk\Pipelines\PharBuild;
32
33
class Timestamps
34
{
35
    private $contents;
36
37
    /**
38
     * @param string $file path to the phar file to use
39
     */
40 1
    public function __construct($file)
41
    {
42 1
        $this->contents = file_get_contents($file);
43
    }
44
45
    /**
46
     * Updates each file's unix timestamps in the PHAR
47
     *
48
     * The PHAR signature can then be produced in a reproducible manner.
49
     *
50
     * @param int|\DateTimeInterface|string $timestamp Date string or DateTime or unix timestamp to use
51
     *
52
     * @throws \LogicException
53
     * @throws \RuntimeException
54
     *
55
     * @return void
56
     */
57 1
    public function updateTimestamps($timestamp = null)
58
    {
59 1
        if ($timestamp instanceof \DateTime || $timestamp instanceof \DateTimeInterface) {
60 1
            $timestamp = $timestamp->getTimestamp();
61 1
        } elseif (is_string($timestamp)) {
62 1
            $timestamp = strtotime($timestamp);
63 1
        } elseif (!is_int($timestamp)) {
64 1
            $timestamp = strtotime('1984-12-24T00:00:00Z');
65
        }
66
67
        // detect manifest offset / end of stub
68 1
        if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) {
69
            throw new \RuntimeException('Could not detect the stub\'s end in the phar'); // @codeCoverageIgnore
70
        }
71
72
        // set starting position and skip past manifest length
73 1
        $pos = $match[0][1] + strlen($match[0][0]);
74 1
        $stubEnd = $pos + $this->readUint($pos, 4);
75 1
        $pos += 4;
76
77 1
        $numFiles = $this->readUint($pos, 4);
78 1
        $pos += 4;
79
80
        // skip API version (YOLO)
81 1
        $pos += 2;
82
83
        // skip PHAR flags
84 1
        $pos += 4;
85
86 1
        $aliasLength = $this->readUint($pos, 4);
87 1
        $pos += 4 + $aliasLength;
88
89 1
        $metadataLength = $this->readUint($pos, 4);
90 1
        $pos += 4 + $metadataLength;
91
92 1
        while ($pos < $stubEnd) {
93 1
            $filenameLength = $this->readUint($pos, 4);
94 1
            $pos += 4 + $filenameLength;
95
96
            // skip filesize
97 1
            $pos += 4;
98
99
            // update timestamp to a fixed value
100 1
            $timeStampBytes = pack('L', $timestamp);
101 1
            $this->contents[$pos/**/] = $timeStampBytes[0];
102 1
            $this->contents[$pos + 1] = $timeStampBytes[1];
103 1
            $this->contents[$pos + 2] = $timeStampBytes[2];
104 1
            $this->contents[$pos + 3] = $timeStampBytes[3];
105
106
            // skip timestamp, compressed file size and crc32 checksum
107 1
            $pos += 3*4;
108
109
            // update or skip file flags - see Bug #77022, use 0644 over 0666
110
            //                           - see Bug #79082, use 0644 over 0664
111 1
            $fileFlags = $this->readUint($pos, 4);
112 1
            $permission = $fileFlags & 0x000001FF;
113 1
            if ($permission !== 0644) {
114
                // @codeCoverageIgnoreStart
115
                $permission = 0644;
116
                $compression = $fileFlags & 0xFFFFF000;
117
                $permissionBytes = pack('L', $permission | $compression);
118
                $this->contents[$pos/**/] = $permissionBytes[0];
119
                $this->contents[$pos + 1] = $permissionBytes[1];
120
                $this->contents[$pos + 2] = $permissionBytes[2];
121
                $this->contents[$pos + 3] = $permissionBytes[3];
122
                // @codeCoverageIgnoreEnd
123
            }
124 1
            $pos += 4;
125
126 1
            $metadataLength = $this->readUint($pos, 4);
127 1
            $pos += 4 + $metadataLength;
128
129 1
            $numFiles--;
130
        }
131
132 1
        if ($numFiles !== 0) {
133
            throw new \LogicException('All files were not processed, something must have gone wrong'); // @codeCoverageIgnore
134
        }
135
    }
136
137
    /**
138
     * Saves the updated phar file, optionally with an updated signature.
139
     *
140
     * @param  string $path
141
     * @param  int $signatureAlgo One of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512
142
     * @return bool
143
     * @throws \UnexpectedValueException
144
     */
145 1
    public function save($path, $signatureAlgo)
146
    {
147 1
        $pos = $this->determineSignatureBegin();
148
149 1
        $algos = array(
150
            \Phar::MD5 => 'md5',
151
            \Phar::SHA1 => 'sha1',
152
            \Phar::SHA256 => 'sha256',
153
            \Phar::SHA512 => 'sha512',
154
        );
155
156 1
        if (!isset($algos[$signatureAlgo])) {
157
            throw new \UnexpectedValueException('Invalid hash algorithm given: '.$signatureAlgo.' expected one of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512'); // @codeCoverageIgnore
158
        }
159 1
        $algo = $algos[$signatureAlgo];
160
161
        // re-sign phar
162
        //           signature
163 1
        $signature = hash($algo, substr($this->contents, 0, $pos), true)
164
            // sig type
165 1
            . pack('L', $signatureAlgo)
166
            // ohai Greg & Marcus
167
            . 'GBMB';
168
169 1
        $this->contents = substr($this->contents, 0, $pos) . $signature;
170
171 1
        return (bool) file_put_contents($path, $this->contents);
172
    }
173
174
    /**
175
     * @param $pos
176
     * @param int $bytes
177
     *
178
     * @return mixed
179
     */
180 1
    private function readUint($pos, $bytes)
181
    {
182 1
        $res = /** @scrutinizer ignore-call */ unpack('V', substr($this->contents, $pos, $bytes));
183
184 1
        return $res[1];
185
    }
186
187
    /**
188
     * Determine the beginning of the signature.
189
     *
190
     * @return int
191
     * @throws \LogicException
192
     * @throws \RuntimeException
193
     */
194 1
    private function determineSignatureBegin()
195
    {
196
        // detect signature position
197 1
        if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) {
198
            throw new \RuntimeException('Could not detect the stub\'s end in the phar'); // @codeCoverageIgnore
199
        }
200
201
        // set starting position and skip past manifest length
202 1
        $pos = $match[0][1] + strlen($match[0][0]);
203 1
        $manifestEnd = $pos + 4 + $this->readUint($pos, 4);
204
205 1
        $pos += 4;
206 1
        $numFiles = $this->readUint($pos, 4);
207
208 1
        $pos += 4;
209
210
        // skip API version (YOLO)
211 1
        $pos += 2;
212
213
        // skip PHAR flags
214 1
        $pos += 4;
215
216 1
        $aliasLength = $this->readUint($pos, 4);
217 1
        $pos += 4 + $aliasLength;
218
219 1
        $metadataLength = $this->readUint($pos, 4);
220 1
        $pos += 4 + $metadataLength;
221
222 1
        $compressedSizes = 0;
223 1
        while (($numFiles > 0) && ($pos < $manifestEnd - 24)) {
224 1
            $filenameLength = $this->readUint($pos, 4);
225 1
            $pos += 4 + $filenameLength;
226
227
            // skip filesize and timestamp
228 1
            $pos += 2*4;
229
230 1
            $compressedSizes += $this->readUint($pos, 4);
231
            // skip compressed file size, crc32 checksum and file flags
232 1
            $pos += 3*4;
233
234 1
            $metadataLength = $this->readUint($pos, 4);
235 1
            $pos += 4 + $metadataLength;
236
237 1
            $numFiles--;
238
        }
239
240 1
        if ($numFiles !== 0) {
241
            throw new \LogicException('All files were not processed, something must have gone wrong'); // @codeCoverageIgnore
242
        }
243
244 1
        return $manifestEnd + $compressedSizes;
245
    }
246
}
247