Completed
Push — id3-metadata-objects ( 3af9ca...3a53b8 )
by Daniel
13:25
created

Metadata::write()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * This file is part of the Metadata package.
4
 *
5
 * @author Daniel Schröder <[email protected]>
6
 */
7
8
namespace GravityMedia\Metadata\ID3v2;
9
10
use GravityMedia\Metadata\Exception\RuntimeException;
11
use GravityMedia\Metadata\ID3v2\Enum\Flag;
12
use GravityMedia\Metadata\ID3v2\Enum\Version;
13
use GravityMedia\Metadata\Metadata\ExtendedHeaderInterface;
14
use GravityMedia\Metadata\Metadata\HeaderInterface;
15
use GravityMedia\Metadata\Metadata\MetadataInterface;
16
use GravityMedia\Metadata\Metadata\TagInterface;
17
use GravityMedia\Stream\Enum\ByteOrder;
18
use GravityMedia\Stream\Reader\CharReader;
19
use GravityMedia\Stream\Reader\LongReader;
20
use GravityMedia\Stream\Reader\ShortReader;
21
use GravityMedia\Stream\Stream;
22
use GravityMedia\Stream\StreamInterface;
23
24
/**
25
 * ID3v2 metadata
26
 *
27
 * @package GravityMedia\Metadata\ID3v2
28
 */
29
class Metadata implements MetadataInterface
30
{
31
    /**
32
     * @var StreamInterface
33
     */
34
    protected $stream;
35
36
    /**
37
     * Create stream provider object from stream.
38
     *
39
     * @param StreamInterface $stream
40
     */
41
    public function __construct(StreamInterface $stream)
42
    {
43
        $this->stream = $stream;
44
    }
45
46
    /**
47
     * {@inheritdoc}
48
     */
49 View Code Duplication
    public function exists()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
50
    {
51
        if ($this->stream->getSize() < 10) {
52
            return false;
53
        }
54
55
        $this->stream->seek(0);
56
57
        return 'ID3' === $this->stream->read(3);
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function strip()
64
    {
65
        return $this;
66
    }
67
68
    /**
69
     * Decode the given 28-bit synchsafe integer to regular 32-bit integer.
70
     *
71
     * @param int $data
72
     *
73
     * @return int
74
     */
75
    protected function decodeSynchsafe32($data)
76
    {
77
        return ($data & 0x7f) | ($data & 0x7f00) >> 1 |
78
        ($data & 0x7f0000) >> 2 | ($data & 0x7f000000) >> 3;
79
    }
80
81
    /**
82
     * Decode unsynchronisation.
83
     *
84
     * @param string $data
85
     *
86
     * @return string
87
     */
88
    protected function decodeUnsynchronisation($data)
89
    {
90
        return preg_replace('/\xff\x00\x00/', "\xff\x00", preg_replace('/\xff\x00(?=[\xe0-\xff])/', "\xff", $data));
91
    }
92
93
    /**
94
     * Read header from stream.
95
     *
96
     * @param StreamInterface $stream
97
     *
98
     * @return HeaderInterface
99
     */
100
    protected function readHeaderFromStream(StreamInterface $stream)
101
    {
102
        $charReader = new CharReader($stream);
103
104
        $longReader = new LongReader($stream);
105
        $longReader->setByteOrder(ByteOrder::BIG_ENDIAN);
106
107
        switch ($charReader->read()) {
108
            case 2:
109
                $version = Version::VERSION_22;
110
                break;
111
            case 3:
112
                $version = Version::VERSION_23;
113
                break;
114
            case 4:
115
                $version = Version::VERSION_24;
116
                break;
117
            default:
118
                throw new RuntimeException('Invalid version.');
119
        }
120
121
        $header = new Header($version);
122
        $header->setRevision($charReader->read());
123
124
        $flags = $charReader->read();
125
        switch ($version) {
126
            case Version::VERSION_22:
127
                $header->setFlags([
128
                    Flag::FLAG_UNSYNCHRONISATION => (bool)($flags & 0x80),
129
                    Flag::FLAG_COMPRESSION => (bool)($flags & 0x40)
130
                ]);
131
                break;
132 View Code Duplication
            case Version::VERSION_23:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
133
                $header->setFlags([
134
                    Flag::FLAG_UNSYNCHRONISATION => (bool)($flags & 0x80),
135
                    Flag::FLAG_EXTENDED_HEADER => (bool)($flags & 0x40),
136
                    Flag::FLAG_EXPERIMENTAL_INDICATOR => (bool)($flags & 0x20)
137
                ]);
138
                break;
139 View Code Duplication
            case Version::VERSION_24:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
140
                $header->setFlags([
141
                    Flag::FLAG_UNSYNCHRONISATION => (bool)($flags & 0x80),
142
                    Flag::FLAG_EXTENDED_HEADER => (bool)($flags & 0x40),
143
                    Flag::FLAG_EXPERIMENTAL_INDICATOR => (bool)($flags & 0x20),
144
                    Flag::FLAG_FOOTER_PRESENT => (bool)($flags & 0x10)
145
                ]);
146
                break;
147
        }
148
149
        $header->setSize($this->decodeSynchsafe32($longReader->read()));
150
151
        return $header;
152
    }
153
154
    /**
155
     * Read extended header from stream.
156
     *
157
     * @param StreamInterface $stream
158
     * @param HeaderInterface $header
159
     *
160
     * @return ExtendedHeaderInterface
161
     */
162
    protected function readExtendedHeaderFromStream(StreamInterface $stream, HeaderInterface $header)
163
    {
164
        $charReader = new CharReader($stream);
165
166
        $shortReader = new ShortReader($stream);
167
        $shortReader->setByteOrder(ByteOrder::BIG_ENDIAN);
168
169
        $longReader = new LongReader($stream);
170
        $longReader->setByteOrder(ByteOrder::BIG_ENDIAN);
171
172
        $extendedHeader = new ExtendedHeader();
173
174
        switch ($header->getVersion()) {
175
            case Version::VERSION_23:
176
                $extendedHeader->setSize($longReader->read());
177
178
                $flags = $shortReader->read();
179
                $extendedHeader->setFlags([
180
                    Flag::FLAG_CRC_DATA_PRESENT => (bool)($flags & 0x8000)
181
                ]);
182
183
                $extendedHeader->setPadding($longReader->read());
184
185
                if ($extendedHeader->isFlagEnabled(Flag::FLAG_CRC_DATA_PRESENT)) {
186
                    $extendedHeader->setCrc32($longReader->read());
187
                }
188
189
                break;
190
            case Version::VERSION_24:
191
                $extendedHeader->setSize($this->decodeSynchsafe32($longReader->read()));
192
193
                $stream->seek(1, SEEK_CUR);
194
                $flags = $charReader->read();
195
                $extendedHeader->setFlags([
196
                    Flag::FLAG_TAG_IS_AN_UPDATE => (bool)($flags & 0x40),
197
                    Flag::FLAG_CRC_DATA_PRESENT => (bool)($flags & 0x20),
198
                    Flag::FLAG_TAG_RESTRICTIONS => (bool)($flags & 0x10)
199
                ]);
200
201
                if ($extendedHeader->isFlagEnabled(Flag::FLAG_TAG_IS_AN_UPDATE)) {
202
                    $stream->seek(1, SEEK_CUR);
203
                }
204
205
                if ($extendedHeader->isFlagEnabled(Flag::FLAG_CRC_DATA_PRESENT)) {
206
                    $stream->seek(1, SEEK_CUR);
207
                    $extendedHeader->setCrc32(
208
                        $charReader->read() * (0xfffffff + 1) + $this->decodeSynchsafe32($longReader->read())
209
                    );
210
                }
211
212
                if ($extendedHeader->isFlagEnabled(Flag::FLAG_TAG_RESTRICTIONS)) {
213
                    $stream->seek(1, SEEK_CUR);
214
                    $extendedHeader->setRestrictions($charReader->read());
215
                }
216
217
                break;
218
        }
219
220
        return $extendedHeader;
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226
    public function read()
227
    {
228
        if (!$this->exists()) {
229
            return null;
230
        }
231
232
        $this->stream->seek(3);
233
234
        $header = $this->readHeaderFromStream($this->stream);
235
        $tag = new Tag($header);
236
237
        $this->stream->seek(10);
238
239
        /**
240
         * The ID3v2 tag size is the sum of the byte length of the extended
241
         * header, the padding and the frames after unsynchronisation. If a
242
         * footer is present this equals to ('total size' - 20) bytes, otherwise
243
         * ('total size' - 10) bytes.
244
         */
245
246
        $data = $this->stream->read($header->getSize());
247
        if ($header->isFlagEnabled(Flag::FLAG_COMPRESSION)) {
248
            $data = gzuncompress($data);
249
        }
250
251
        if ($header->isFlagEnabled(Flag::FLAG_UNSYNCHRONISATION)) {
252
            $data = $this->decodeUnsynchronisation($data);
253
        }
254
255
        $resource = fopen('php://temp', 'r+b');
256
        $stream = Stream::fromResource($resource);
257
        $stream->write($data);
258
        $stream->rewind();
259
260
        $size = $stream->getSize();
261
        if ($header->isFlagEnabled(Flag::FLAG_EXTENDED_HEADER)) {
262
            $extendedHeader = $this->readExtendedHeaderFromStream($stream, $header);
263
            $tag->setExtendedHeader($extendedHeader);
264
265
            $size -= $extendedHeader->getSize();
266
        }
267
268
        if ($header->isFlagEnabled(Flag::FLAG_EXTENDED_HEADER)) {
269
            // TODO: Read footer from stream
270
271
            $size -= 10;
272
        }
273
274
        $longReader = new LongReader($stream);
275
        $longReader->setByteOrder(ByteOrder::BIG_ENDIAN);
276
277
        while ($size > 0) {
278
            if (0 === ord($stream->read(1))) {
279
                break;
280
            }
281
282
            $stream->seek(-1, SEEK_CUR);
283
284
            $frameName = $stream->read(4);
0 ignored issues
show
Unused Code introduced by
$frameName is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
285
            //var_dump($frameName);
286
287
            $frameSize = $longReader->read();
288
            //var_dump($frameSize);
289
290
            $size -= $frameSize;
291
        }
292
293
        return $tag;
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299
    public function write(TagInterface $tag)
300
    {
301
        return $this;
302
    }
303
}
304