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