• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.metadata.id3;
17 
18 import androidx.annotation.Nullable;
19 import com.google.android.exoplayer2.C;
20 import com.google.android.exoplayer2.metadata.Metadata;
21 import com.google.android.exoplayer2.metadata.MetadataDecoder;
22 import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
23 import com.google.android.exoplayer2.util.Assertions;
24 import com.google.android.exoplayer2.util.Log;
25 import com.google.android.exoplayer2.util.ParsableBitArray;
26 import com.google.android.exoplayer2.util.ParsableByteArray;
27 import com.google.android.exoplayer2.util.Util;
28 import java.io.UnsupportedEncodingException;
29 import java.nio.ByteBuffer;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.List;
33 import java.util.Locale;
34 
35 /**
36  * Decodes ID3 tags.
37  */
38 public final class Id3Decoder implements MetadataDecoder {
39 
40   /**
41    * A predicate for determining whether individual frames should be decoded.
42    */
43   public interface FramePredicate {
44 
45     /**
46      * Returns whether a frame with the specified parameters should be decoded.
47      *
48      * @param majorVersion The major version of the ID3 tag.
49      * @param id0 The first byte of the frame ID.
50      * @param id1 The second byte of the frame ID.
51      * @param id2 The third byte of the frame ID.
52      * @param id3 The fourth byte of the frame ID.
53      * @return Whether the frame should be decoded.
54      */
evaluate(int majorVersion, int id0, int id1, int id2, int id3)55     boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3);
56 
57   }
58 
59   /** A predicate that indicates no frames should be decoded. */
60   public static final FramePredicate NO_FRAMES_PREDICATE =
61       (majorVersion, id0, id1, id2, id3) -> false;
62 
63   private static final String TAG = "Id3Decoder";
64 
65   /** The first three bytes of a well formed ID3 tag header. */
66   public static final int ID3_TAG = 0x00494433;
67   /**
68    * Length of an ID3 tag header.
69    */
70   public static final int ID3_HEADER_LENGTH = 10;
71 
72   private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080;
73   private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040;
74   private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020;
75   private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008;
76   private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004;
77   private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040;
78   private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002;
79   private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001;
80 
81   private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
82   private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
83   private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
84   private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
85 
86   @Nullable private final FramePredicate framePredicate;
87 
Id3Decoder()88   public Id3Decoder() {
89     this(null);
90   }
91 
92   /**
93    * @param framePredicate Determines which frames are decoded. May be null to decode all frames.
94    */
Id3Decoder(@ullable FramePredicate framePredicate)95   public Id3Decoder(@Nullable FramePredicate framePredicate) {
96     this.framePredicate = framePredicate;
97   }
98 
99   @Override
100   @Nullable
decode(MetadataInputBuffer inputBuffer)101   public Metadata decode(MetadataInputBuffer inputBuffer) {
102     ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
103     Assertions.checkArgument(
104         buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
105     return decode(buffer.array(), buffer.limit());
106   }
107 
108   /**
109    * Decodes ID3 tags.
110    *
111    * @param data The bytes to decode ID3 tags from.
112    * @param size Amount of bytes in {@code data} to read.
113    * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could
114    *     not be decoded.
115    */
116   @Nullable
decode(byte[] data, int size)117   public Metadata decode(byte[] data, int size) {
118     List<Id3Frame> id3Frames = new ArrayList<>();
119     ParsableByteArray id3Data = new ParsableByteArray(data, size);
120 
121     Id3Header id3Header = decodeHeader(id3Data);
122     if (id3Header == null) {
123       return null;
124     }
125 
126     int startPosition = id3Data.getPosition();
127     int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
128     int framesSize = id3Header.framesSize;
129     if (id3Header.isUnsynchronized) {
130       framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
131     }
132     id3Data.setLimit(startPosition + framesSize);
133 
134     boolean unsignedIntFrameSizeHack = false;
135     if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) {
136       if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) {
137         unsignedIntFrameSizeHack = true;
138       } else {
139         Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion);
140         return null;
141       }
142     }
143 
144     while (id3Data.bytesLeft() >= frameHeaderSize) {
145       Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
146           frameHeaderSize, framePredicate);
147       if (frame != null) {
148         id3Frames.add(frame);
149       }
150     }
151 
152     return new Metadata(id3Frames);
153   }
154 
155   /**
156    * @param data A {@link ParsableByteArray} from which the header should be read.
157    * @return The parsed header, or null if the ID3 tag is unsupported.
158    */
159   @Nullable
decodeHeader(ParsableByteArray data)160   private static Id3Header decodeHeader(ParsableByteArray data) {
161     if (data.bytesLeft() < ID3_HEADER_LENGTH) {
162       Log.w(TAG, "Data too short to be an ID3 tag");
163       return null;
164     }
165 
166     int id = data.readUnsignedInt24();
167     if (id != ID3_TAG) {
168       Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id));
169       return null;
170     }
171 
172     int majorVersion = data.readUnsignedByte();
173     data.skipBytes(1); // Skip minor version.
174     int flags = data.readUnsignedByte();
175     int framesSize = data.readSynchSafeInt();
176 
177     if (majorVersion == 2) {
178       boolean isCompressed = (flags & 0x40) != 0;
179       if (isCompressed) {
180         Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme");
181         return null;
182       }
183     } else if (majorVersion == 3) {
184       boolean hasExtendedHeader = (flags & 0x40) != 0;
185       if (hasExtendedHeader) {
186         int extendedHeaderSize = data.readInt(); // Size excluding size field.
187         data.skipBytes(extendedHeaderSize);
188         framesSize -= (extendedHeaderSize + 4);
189       }
190     } else if (majorVersion == 4) {
191       boolean hasExtendedHeader = (flags & 0x40) != 0;
192       if (hasExtendedHeader) {
193         int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
194         data.skipBytes(extendedHeaderSize - 4);
195         framesSize -= extendedHeaderSize;
196       }
197       boolean hasFooter = (flags & 0x10) != 0;
198       if (hasFooter) {
199         framesSize -= 10;
200       }
201     } else {
202       Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
203       return null;
204     }
205 
206     // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
207     boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
208     return new Id3Header(majorVersion, isUnsynchronized, framesSize);
209   }
210 
211   private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion,
212       int frameHeaderSize, boolean unsignedIntFrameSizeHack) {
213     int startPosition = id3Data.getPosition();
214     try {
215       while (id3Data.bytesLeft() >= frameHeaderSize) {
216         // Read the next frame header.
217         int id;
218         long frameSize;
219         int flags;
220         if (majorVersion >= 3) {
221           id = id3Data.readInt();
222           frameSize = id3Data.readUnsignedInt();
223           flags = id3Data.readUnsignedShort();
224         } else {
225           id = id3Data.readUnsignedInt24();
226           frameSize = id3Data.readUnsignedInt24();
227           flags = 0;
228         }
229         // Validate the frame header and skip to the next one.
230         if (id == 0 && frameSize == 0 && flags == 0) {
231           // We've reached zero padding after the end of the final frame.
232           return true;
233         } else {
234           if (majorVersion == 4 && !unsignedIntFrameSizeHack) {
235             // Parse the data size as a synchsafe integer, as per the spec.
236             if ((frameSize & 0x808080L) != 0) {
237               return false;
238             }
239             frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
240                 | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
241           }
242           boolean hasGroupIdentifier = false;
243           boolean hasDataLength = false;
244           if (majorVersion == 4) {
245             hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
246             hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
247           } else if (majorVersion == 3) {
248             hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
249             // A V3 frame has data length if and only if it's compressed.
250             hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
251           }
252           int minimumFrameSize = 0;
253           if (hasGroupIdentifier) {
254             minimumFrameSize++;
255           }
256           if (hasDataLength) {
257             minimumFrameSize += 4;
258           }
259           if (frameSize < minimumFrameSize) {
260             return false;
261           }
262           if (id3Data.bytesLeft() < frameSize) {
263             return false;
264           }
265           id3Data.skipBytes((int) frameSize); // flags
266         }
267       }
268       return true;
269     } finally {
270       id3Data.setPosition(startPosition);
271     }
272   }
273 
274   @Nullable
decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, int frameHeaderSize, @Nullable FramePredicate framePredicate)275   private static Id3Frame decodeFrame(
276       int majorVersion,
277       ParsableByteArray id3Data,
278       boolean unsignedIntFrameSizeHack,
279       int frameHeaderSize,
280       @Nullable FramePredicate framePredicate) {
281     int frameId0 = id3Data.readUnsignedByte();
282     int frameId1 = id3Data.readUnsignedByte();
283     int frameId2 = id3Data.readUnsignedByte();
284     int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
285 
286     int frameSize;
287     if (majorVersion == 4) {
288       frameSize = id3Data.readUnsignedIntToInt();
289       if (!unsignedIntFrameSizeHack) {
290         frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
291             | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
292       }
293     } else if (majorVersion == 3) {
294       frameSize = id3Data.readUnsignedIntToInt();
295     } else /* id3Header.majorVersion == 2 */ {
296       frameSize = id3Data.readUnsignedInt24();
297     }
298 
299     int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0;
300     if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
301         && flags == 0) {
302       // We must be reading zero padding at the end of the tag.
303       id3Data.setPosition(id3Data.limit());
304       return null;
305     }
306 
307     int nextFramePosition = id3Data.getPosition() + frameSize;
308     if (nextFramePosition > id3Data.limit()) {
309       Log.w(TAG, "Frame size exceeds remaining tag data");
310       id3Data.setPosition(id3Data.limit());
311       return null;
312     }
313 
314     if (framePredicate != null
315         && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) {
316       // Filtered by the predicate.
317       id3Data.setPosition(nextFramePosition);
318       return null;
319     }
320 
321     // Frame flags.
322     boolean isCompressed = false;
323     boolean isEncrypted = false;
324     boolean isUnsynchronized = false;
325     boolean hasDataLength = false;
326     boolean hasGroupIdentifier = false;
327     if (majorVersion == 3) {
328       isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
329       isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0;
330       hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
331       // A V3 frame has data length if and only if it's compressed.
332       hasDataLength = isCompressed;
333     } else if (majorVersion == 4) {
334       hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
335       isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0;
336       isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0;
337       isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0;
338       hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
339     }
340 
341     if (isCompressed || isEncrypted) {
342       Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
343       id3Data.setPosition(nextFramePosition);
344       return null;
345     }
346 
347     if (hasGroupIdentifier) {
348       frameSize--;
349       id3Data.skipBytes(1);
350     }
351     if (hasDataLength) {
352       frameSize -= 4;
353       id3Data.skipBytes(4);
354     }
355     if (isUnsynchronized) {
356       frameSize = removeUnsynchronization(id3Data, frameSize);
357     }
358 
359     try {
360       Id3Frame frame;
361       if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
362           && (majorVersion == 2 || frameId3 == 'X')) {
363         frame = decodeTxxxFrame(id3Data, frameSize);
364       } else if (frameId0 == 'T') {
365         String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
366         frame = decodeTextInformationFrame(id3Data, frameSize, id);
367       } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X'
368           && (majorVersion == 2 || frameId3 == 'X')) {
369         frame = decodeWxxxFrame(id3Data, frameSize);
370       } else if (frameId0 == 'W') {
371         String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
372         frame = decodeUrlLinkFrame(id3Data, frameSize, id);
373       } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
374         frame = decodePrivFrame(id3Data, frameSize);
375       } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
376           && (frameId3 == 'B' || majorVersion == 2)) {
377         frame = decodeGeobFrame(id3Data, frameSize);
378       } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
379           : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
380         frame = decodeApicFrame(id3Data, frameSize, majorVersion);
381       } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
382           && (frameId3 == 'M' || majorVersion == 2)) {
383         frame = decodeCommentFrame(id3Data, frameSize);
384       } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {
385         frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
386             frameHeaderSize, framePredicate);
387       } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
388         frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
389             frameHeaderSize, framePredicate);
390       } else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') {
391         frame = decodeMlltFrame(id3Data, frameSize);
392       } else {
393         String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
394         frame = decodeBinaryFrame(id3Data, frameSize, id);
395       }
396       if (frame == null) {
397         Log.w(TAG, "Failed to decode frame: id="
398             + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize="
399             + frameSize);
400       }
401       return frame;
402     } catch (UnsupportedEncodingException e) {
403       Log.w(TAG, "Unsupported character encoding");
404       return null;
405     } finally {
406       id3Data.setPosition(nextFramePosition);
407     }
408   }
409 
410   @Nullable
decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)411   private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
412       throws UnsupportedEncodingException {
413     if (frameSize < 1) {
414       // Frame is malformed.
415       return null;
416     }
417 
418     int encoding = id3Data.readUnsignedByte();
419     String charset = getCharsetName(encoding);
420 
421     byte[] data = new byte[frameSize - 1];
422     id3Data.readBytes(data, 0, frameSize - 1);
423 
424     int descriptionEndIndex = indexOfEos(data, 0, encoding);
425     String description = new String(data, 0, descriptionEndIndex, charset);
426 
427     int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
428     int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
429     String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
430 
431     return new TextInformationFrame("TXXX", description, value);
432   }
433 
434   @Nullable
decodeTextInformationFrame( ParsableByteArray id3Data, int frameSize, String id)435   private static TextInformationFrame decodeTextInformationFrame(
436       ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException {
437     if (frameSize < 1) {
438       // Frame is malformed.
439       return null;
440     }
441 
442     int encoding = id3Data.readUnsignedByte();
443     String charset = getCharsetName(encoding);
444 
445     byte[] data = new byte[frameSize - 1];
446     id3Data.readBytes(data, 0, frameSize - 1);
447 
448     int valueEndIndex = indexOfEos(data, 0, encoding);
449     String value = new String(data, 0, valueEndIndex, charset);
450 
451     return new TextInformationFrame(id, null, value);
452   }
453 
454   @Nullable
decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)455   private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
456       throws UnsupportedEncodingException {
457     if (frameSize < 1) {
458       // Frame is malformed.
459       return null;
460     }
461 
462     int encoding = id3Data.readUnsignedByte();
463     String charset = getCharsetName(encoding);
464 
465     byte[] data = new byte[frameSize - 1];
466     id3Data.readBytes(data, 0, frameSize - 1);
467 
468     int descriptionEndIndex = indexOfEos(data, 0, encoding);
469     String description = new String(data, 0, descriptionEndIndex, charset);
470 
471     int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
472     int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
473     String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1");
474 
475     return new UrlLinkFrame("WXXX", description, url);
476   }
477 
decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize, String id)478   private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,
479       String id) throws UnsupportedEncodingException {
480     byte[] data = new byte[frameSize];
481     id3Data.readBytes(data, 0, frameSize);
482 
483     int urlEndIndex = indexOfZeroByte(data, 0);
484     String url = new String(data, 0, urlEndIndex, "ISO-8859-1");
485 
486     return new UrlLinkFrame(id, null, url);
487   }
488 
decodePrivFrame(ParsableByteArray id3Data, int frameSize)489   private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize)
490       throws UnsupportedEncodingException {
491     byte[] data = new byte[frameSize];
492     id3Data.readBytes(data, 0, frameSize);
493 
494     int ownerEndIndex = indexOfZeroByte(data, 0);
495     String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1");
496 
497     int privateDataStartIndex = ownerEndIndex + 1;
498     byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length);
499 
500     return new PrivFrame(owner, privateData);
501   }
502 
decodeGeobFrame(ParsableByteArray id3Data, int frameSize)503   private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize)
504       throws UnsupportedEncodingException {
505     int encoding = id3Data.readUnsignedByte();
506     String charset = getCharsetName(encoding);
507 
508     byte[] data = new byte[frameSize - 1];
509     id3Data.readBytes(data, 0, frameSize - 1);
510 
511     int mimeTypeEndIndex = indexOfZeroByte(data, 0);
512     String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1");
513 
514     int filenameStartIndex = mimeTypeEndIndex + 1;
515     int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);
516     String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset);
517 
518     int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
519     int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
520     String description =
521         decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset);
522 
523     int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
524     byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);
525 
526     return new GeobFrame(mimeType, filename, description, objectData);
527   }
528 
decodeApicFrame(ParsableByteArray id3Data, int frameSize, int majorVersion)529   private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
530       int majorVersion) throws UnsupportedEncodingException {
531     int encoding = id3Data.readUnsignedByte();
532     String charset = getCharsetName(encoding);
533 
534     byte[] data = new byte[frameSize - 1];
535     id3Data.readBytes(data, 0, frameSize - 1);
536 
537     String mimeType;
538     int mimeTypeEndIndex;
539     if (majorVersion == 2) {
540       mimeTypeEndIndex = 2;
541       mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1"));
542       if ("image/jpg".equals(mimeType)) {
543         mimeType = "image/jpeg";
544       }
545     } else {
546       mimeTypeEndIndex = indexOfZeroByte(data, 0);
547       mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"));
548       if (mimeType.indexOf('/') == -1) {
549         mimeType = "image/" + mimeType;
550       }
551     }
552 
553     int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
554 
555     int descriptionStartIndex = mimeTypeEndIndex + 2;
556     int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
557     String description = new String(data, descriptionStartIndex,
558         descriptionEndIndex - descriptionStartIndex, charset);
559 
560     int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
561     byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length);
562 
563     return new ApicFrame(mimeType, description, pictureType, pictureData);
564   }
565 
566   @Nullable
decodeCommentFrame(ParsableByteArray id3Data, int frameSize)567   private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
568       throws UnsupportedEncodingException {
569     if (frameSize < 4) {
570       // Frame is malformed.
571       return null;
572     }
573 
574     int encoding = id3Data.readUnsignedByte();
575     String charset = getCharsetName(encoding);
576 
577     byte[] data = new byte[3];
578     id3Data.readBytes(data, 0, 3);
579     String language = new String(data, 0, 3);
580 
581     data = new byte[frameSize - 4];
582     id3Data.readBytes(data, 0, frameSize - 4);
583 
584     int descriptionEndIndex = indexOfEos(data, 0, encoding);
585     String description = new String(data, 0, descriptionEndIndex, charset);
586 
587     int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
588     int textEndIndex = indexOfEos(data, textStartIndex, encoding);
589     String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset);
590 
591     return new CommentFrame(language, description, text);
592   }
593 
decodeChapterFrame( ParsableByteArray id3Data, int frameSize, int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, @Nullable FramePredicate framePredicate)594   private static ChapterFrame decodeChapterFrame(
595       ParsableByteArray id3Data,
596       int frameSize,
597       int majorVersion,
598       boolean unsignedIntFrameSizeHack,
599       int frameHeaderSize,
600       @Nullable FramePredicate framePredicate)
601       throws UnsupportedEncodingException {
602     int framePosition = id3Data.getPosition();
603     int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
604     String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,
605         "ISO-8859-1");
606     id3Data.setPosition(chapterIdEndIndex + 1);
607 
608     int startTime = id3Data.readInt();
609     int endTime = id3Data.readInt();
610     long startOffset = id3Data.readUnsignedInt();
611     if (startOffset == 0xFFFFFFFFL) {
612       startOffset = C.POSITION_UNSET;
613     }
614     long endOffset = id3Data.readUnsignedInt();
615     if (endOffset == 0xFFFFFFFFL) {
616       endOffset = C.POSITION_UNSET;
617     }
618 
619     ArrayList<Id3Frame> subFrames = new ArrayList<>();
620     int limit = framePosition + frameSize;
621     while (id3Data.getPosition() < limit) {
622       Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
623           frameHeaderSize, framePredicate);
624       if (frame != null) {
625         subFrames.add(frame);
626       }
627     }
628 
629     Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
630     subFrames.toArray(subFrameArray);
631     return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);
632   }
633 
decodeChapterTOCFrame( ParsableByteArray id3Data, int frameSize, int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, @Nullable FramePredicate framePredicate)634   private static ChapterTocFrame decodeChapterTOCFrame(
635       ParsableByteArray id3Data,
636       int frameSize,
637       int majorVersion,
638       boolean unsignedIntFrameSizeHack,
639       int frameHeaderSize,
640       @Nullable FramePredicate framePredicate)
641       throws UnsupportedEncodingException {
642     int framePosition = id3Data.getPosition();
643     int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
644     String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition,
645         "ISO-8859-1");
646     id3Data.setPosition(elementIdEndIndex + 1);
647 
648     int ctocFlags = id3Data.readUnsignedByte();
649     boolean isRoot = (ctocFlags & 0x0002) != 0;
650     boolean isOrdered = (ctocFlags & 0x0001) != 0;
651 
652     int childCount = id3Data.readUnsignedByte();
653     String[] children = new String[childCount];
654     for (int i = 0; i < childCount; i++) {
655       int startIndex = id3Data.getPosition();
656       int endIndex = indexOfZeroByte(id3Data.data, startIndex);
657       children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1");
658       id3Data.setPosition(endIndex + 1);
659     }
660 
661     ArrayList<Id3Frame> subFrames = new ArrayList<>();
662     int limit = framePosition + frameSize;
663     while (id3Data.getPosition() < limit) {
664       Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
665           frameHeaderSize, framePredicate);
666       if (frame != null) {
667         subFrames.add(frame);
668       }
669     }
670 
671     Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
672     subFrames.toArray(subFrameArray);
673     return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray);
674   }
675 
decodeMlltFrame(ParsableByteArray id3Data, int frameSize)676   private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) {
677     // See ID3v2.4.0 native frames subsection 4.6.
678     int mpegFramesBetweenReference = id3Data.readUnsignedShort();
679     int bytesBetweenReference = id3Data.readUnsignedInt24();
680     int millisecondsBetweenReference = id3Data.readUnsignedInt24();
681     int bitsForBytesDeviation = id3Data.readUnsignedByte();
682     int bitsForMillisecondsDeviation = id3Data.readUnsignedByte();
683 
684     ParsableBitArray references = new ParsableBitArray();
685     references.reset(id3Data);
686     int referencesBits = 8 * (frameSize - 10);
687     int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation;
688     int referencesCount = referencesBits / bitsPerReference;
689     int[] bytesDeviations = new int[referencesCount];
690     int[] millisecondsDeviations = new int[referencesCount];
691     for (int i = 0; i < referencesCount; i++) {
692       int bytesDeviation = references.readBits(bitsForBytesDeviation);
693       int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation);
694       bytesDeviations[i] = bytesDeviation;
695       millisecondsDeviations[i] = millisecondsDeviation;
696     }
697 
698     return new MlltFrame(
699         mpegFramesBetweenReference,
700         bytesBetweenReference,
701         millisecondsBetweenReference,
702         bytesDeviations,
703         millisecondsDeviations);
704   }
705 
decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id)706   private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
707       String id) {
708     byte[] frame = new byte[frameSize];
709     id3Data.readBytes(frame, 0, frameSize);
710 
711     return new BinaryFrame(id, frame);
712   }
713 
714   /**
715    * Performs in-place removal of unsynchronization for {@code length} bytes starting from
716    * {@link ParsableByteArray#getPosition()}
717    *
718    * @param data Contains the data to be processed.
719    * @param length The length of the data to be processed.
720    * @return The length of the data after processing.
721    */
removeUnsynchronization(ParsableByteArray data, int length)722   private static int removeUnsynchronization(ParsableByteArray data, int length) {
723     byte[] bytes = data.data;
724     int startPosition = data.getPosition();
725     for (int i = startPosition; i + 1 < startPosition + length; i++) {
726       if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
727         int relativePosition = i - startPosition;
728         System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2);
729         length--;
730       }
731     }
732     return length;
733   }
734 
735   /**
736    * Maps encoding byte from ID3v2 frame to a Charset.
737    *
738    * @param encodingByte The value of encoding byte from ID3v2 frame.
739    * @return Charset name.
740    */
getCharsetName(int encodingByte)741   private static String getCharsetName(int encodingByte) {
742     switch (encodingByte) {
743       case ID3_TEXT_ENCODING_UTF_16:
744         return "UTF-16";
745       case ID3_TEXT_ENCODING_UTF_16BE:
746         return "UTF-16BE";
747       case ID3_TEXT_ENCODING_UTF_8:
748         return "UTF-8";
749       case ID3_TEXT_ENCODING_ISO_8859_1:
750       default:
751         return "ISO-8859-1";
752     }
753   }
754 
getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2, int frameId3)755   private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2,
756       int frameId3) {
757     return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
758         : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
759   }
760 
indexOfEos(byte[] data, int fromIndex, int encoding)761   private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
762     int terminationPos = indexOfZeroByte(data, fromIndex);
763 
764     // For single byte encoding charsets, we're done.
765     if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
766       return terminationPos;
767     }
768 
769     // Otherwise ensure an even index and look for a second zero byte.
770     while (terminationPos < data.length - 1) {
771       if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
772         return terminationPos;
773       }
774       terminationPos = indexOfZeroByte(data, terminationPos + 1);
775     }
776 
777     return data.length;
778   }
779 
indexOfZeroByte(byte[] data, int fromIndex)780   private static int indexOfZeroByte(byte[] data, int fromIndex) {
781     for (int i = fromIndex; i < data.length; i++) {
782       if (data[i] == (byte) 0) {
783         return i;
784       }
785     }
786     return data.length;
787   }
788 
delimiterLength(int encodingByte)789   private static int delimiterLength(int encodingByte) {
790     return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
791         ? 1 : 2;
792   }
793 
794   /**
795    * Copies the specified range of an array, or returns a zero length array if the range is invalid.
796    *
797    * @param data The array from which to copy.
798    * @param from The start of the range to copy (inclusive).
799    * @param to The end of the range to copy (exclusive).
800    * @return The copied data, or a zero length array if the range is invalid.
801    */
copyOfRangeIfValid(byte[] data, int from, int to)802   private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) {
803     if (to <= from) {
804       // Invalid or zero length range.
805       return Util.EMPTY_BYTE_ARRAY;
806     }
807     return Arrays.copyOfRange(data, from, to);
808   }
809 
810   /**
811    * Returns a string obtained by decoding the specified range of {@code data} using the specified
812    * {@code charsetName}. An empty string is returned if the range is invalid.
813    *
814    * @param data The array from which to decode the string.
815    * @param from The start of the range.
816    * @param to The end of the range (exclusive).
817    * @param charsetName The name of the Charset to use.
818    * @return The decoded string, or an empty string if the range is invalid.
819    * @throws UnsupportedEncodingException If the Charset is not supported.
820    */
decodeStringIfValid(byte[] data, int from, int to, String charsetName)821   private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName)
822       throws UnsupportedEncodingException {
823     if (to <= from || to > data.length) {
824       return "";
825     }
826     return new String(data, from, to - from, charsetName);
827   }
828 
829   private static final class Id3Header {
830 
831     private final int majorVersion;
832     private final boolean isUnsynchronized;
833     private final int framesSize;
834 
Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize)835     public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) {
836       this.majorVersion = majorVersion;
837       this.isUnsynchronized = isUnsynchronized;
838       this.framesSize = framesSize;
839     }
840 
841   }
842 
843 }
844