ID3v2FrameTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.mp3;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_16BE;
import static java.nio.charset.StandardCharsets.UTF_16LE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import org.junit.jupiter.api.Test;
import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
/**
* Tests the shared ID3v2 text decoding used by every ID3v2.2/2.3/2.4 text frame.
*/
public class ID3v2FrameTest {
private static final byte ISO_8859_1_FLAG = 0;
private static final byte UTF_16_BOM_FLAG = 1;
private static final byte UTF_16BE_FLAG = 2;
private static final byte UTF_8_FLAG = 3;
private static final byte[] BOM_LE = {(byte) 0xff, (byte) 0xfe};
private static final byte[] BOM_BE = {(byte) 0xfe, (byte) 0xff};
// LATIN has a NUL per char in UTF-16 (a byte-order signal); CJK does not
private static final String LATIN = "Test Copyright";
private static final String CJK = "���������";
private static byte[] frame(byte encodingFlag, byte[]... parts) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(encodingFlag);
for (byte[] part : parts) {
out.write(part, 0, part.length);
}
return out.toByteArray();
}
private static byte[] bytes(String text, Charset charset) {
return text.getBytes(charset);
}
private static String tagString(byte[] data) {
return ID3v2Frame.getTagString(data, 0, data.length);
}
@Test
public void testSingleByteEncodings() {
assertEquals(LATIN, tagString(frame(ISO_8859_1_FLAG, bytes(LATIN, ISO_8859_1))));
assertEquals(CJK, tagString(frame(UTF_8_FLAG, bytes(CJK, UTF_8))));
}
@Test
public void testUTF16WithBOM() {
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, BOM_LE, bytes(LATIN, UTF_16LE))));
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, BOM_BE, bytes(LATIN, UTF_16BE))));
assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, BOM_LE, bytes(CJK, UTF_16LE))));
assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, BOM_BE, bytes(CJK, UTF_16BE))));
}
@Test
public void testUTF16BEWithoutBOMFlag() {
assertEquals(LATIN, tagString(frame(UTF_16BE_FLAG, bytes(LATIN, UTF_16BE))));
assertEquals(CJK, tagString(frame(UTF_16BE_FLAG, bytes(CJK, UTF_16BE))));
}
// $01 promises a BOM; omitting it used to decode LE as BE mojibake ('T' 0x54 0x00 -> U+5400)
@Test
public void testUTF16WithoutBOMRecoversByteOrder() {
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN, UTF_16LE))));
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN, UTF_16BE))));
}
// no NUL bytes (all chars above U+00FF) means no signal, so keep the big-endian default
@Test
public void testUTF16WithoutBOMKeepsBigEndianDefaultWhenNoSignal() {
assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, bytes(CJK, UTF_16BE))));
}
@Test
public void testNullTerminationIsTrimmed() {
byte[] doubleNul = {0, 0};
byte[] singleNul = {0};
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN, UTF_16LE), doubleNul)));
assertEquals(LATIN,
tagString(frame(UTF_16_BOM_FLAG, BOM_LE, bytes(LATIN, UTF_16LE), doubleNul)));
assertEquals(LATIN, tagString(frame(ISO_8859_1_FLAG, bytes(LATIN, ISO_8859_1), singleNul)));
}
// TIKA-1024: a frame holding nothing but a BOM decodes to the empty string
@Test
public void testNakedBOM() {
assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_LE)));
assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_BE)));
}
// COMM decodes description and text separately, so each recovers byte order on its own
@Test
public void testCommentWithoutBOMRecoversByteOrder() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(UTF_16_BOM_FLAG);
out.write(bytes("eng", ISO_8859_1));
out.write(bytes("Desc", UTF_16LE));
out.write(new byte[]{0, 0});
out.write(bytes(LATIN, UTF_16LE));
byte[] data = out.toByteArray();
ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
assertEquals("eng", comment.getLanguage());
assertEquals("Desc", comment.getDescription());
assertEquals(LATIN, comment.getText());
}
@Test
public void testCommentWithBOM() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(UTF_16_BOM_FLAG);
out.write(bytes("eng", ISO_8859_1));
out.write(BOM_LE);
out.write(bytes("Desc", UTF_16LE));
out.write(new byte[]{0, 0});
out.write(BOM_LE);
out.write(bytes(LATIN, UTF_16LE));
byte[] data = out.toByteArray();
ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
assertEquals("eng", comment.getLanguage());
assertEquals("Desc", comment.getDescription());
assertEquals(LATIN, comment.getText());
}
// too short for flag + 3 byte language, or an unknown flag, decodes to null (no overrun)
@Test
public void testMalformedCommentsReturnNull() {
assertNull(ID3v2Frame.getComment(new byte[0], 0, 0));
assertNull(ID3v2Frame.getComment(new byte[]{UTF_16_BOM_FLAG}, 0, 1));
assertNull(ID3v2Frame.getComment(new byte[]{ISO_8859_1_FLAG, 'e', 'n'}, 0, 3));
// 0x05 is not a defined ID3v2 text encoding
byte[] badFlag = {5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'};
assertNull(ID3v2Frame.getComment(badFlag, 0, badFlag.length));
}
// a double byte comment ending on a lone NUL must not overrun looking for a terminator
@Test
public void testCommentTruncatedOnTerminatorDoesNotOverrun() {
byte[] data = {UTF_16_BOM_FLAG, 'e', 'n', 'g', 0};
ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
assertEquals("eng", comment.getLanguage());
}
}