Skip to content

Commit 8f5a357

Browse files
committed
TIKA-4784: fix unicode in id3
1 parent b969f2a commit 8f5a357

6 files changed

Lines changed: 28 additions & 73 deletions

File tree

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ private ID3Comment getComment(byte[] data, int offset, int length) {
113113
return ID3v2Frame.getComment(data, offset, length);
114114
}
115115

116-
/**
117-
* Malformed comment frames decode to null and are skipped rather than
118-
* being added to the list, where they would trip up the consumers.
119-
*/
116+
/** Skips null comments (malformed frames) that would trip up consumers. */
120117
private void addComment(ID3Comment comment) {
121118
if (comment != null) {
122119
comments.add(comment);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,7 @@ private ID3Comment getComment(byte[] data, int offset, int length) {
9999
return ID3v2Frame.getComment(data, offset, length);
100100
}
101101

102-
/**
103-
* Malformed comment frames decode to null and are skipped rather than
104-
* being added to the list, where they would trip up the consumers.
105-
*/
102+
/** Skips null comments (malformed frames) that would trip up consumers. */
106103
private void addComment(ID3Comment comment) {
107104
if (comment != null) {
108105
comments.add(comment);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ private ID3Comment getComment(byte[] data, int offset, int length) {
105105
return ID3v2Frame.getComment(data, offset, length);
106106
}
107107

108-
/**
109-
* Malformed comment frames decode to null and are skipped rather than
110-
* being added to the list, where they would trip up the consumers.
111-
*/
108+
/** Skips null comments (malformed frames) that would trip up consumers. */
112109
private void addComment(ID3Comment comment) {
113110
if (comment != null) {
114111
comments.add(comment);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,9 @@ protected static String getTagString(byte[] data, int offset, int length) {
244244
}
245245

246246
/**
247-
* Decodes text in the frame's declared encoding.
248-
* <p>
249-
* Encoding {@code $01} is defined as UTF-16 <em>with</em> a BOM, but some taggers omit it.
250-
* Java's UTF-16 charset then falls back to big-endian, which silently turns little-endian
251-
* text into CJK mojibake (the ASCII 'T' {@code 0x54 0x00} decodes to U+5400). When the BOM
252-
* is missing, recover the byte order from the position of the NUL bytes instead, and keep
253-
* the big-endian fallback when the bytes carry no signal either way.
247+
* Decodes text in the frame's declared encoding. Encoding {@code $01} is UTF-16 with a BOM;
248+
* when a tagger omits it Java assumes big-endian and turns little-endian text into mojibake
249+
* ({@code 'T' 0x54 0x00} decodes to U+5400), so recover the byte order from the bytes.
254250
*/
255251
private static String decodeText(byte[] data, int offset, int length, TextEncoding encoding)
256252
throws UnsupportedEncodingException {
@@ -261,9 +257,7 @@ private static String decodeText(byte[] data, int offset, int length, TextEncodi
261257
return new String(data, offset, length, charset);
262258
}
263259

264-
/**
265-
* Does the text at the given offset start with a UTF-16 byte order mark?
266-
*/
260+
/** Does the text at the offset start with a UTF-16 BOM? */
267261
private static boolean hasBOM(byte[] data, int offset, int length) {
268262
if (length < 2) {
269263
return false;
@@ -274,10 +268,10 @@ private static boolean hasBOM(byte[] data, int offset, int length) {
274268
}
275269

276270
/**
277-
* Guesses the byte order of BOM-less UTF-16 text by counting which half of each code
278-
* unit is NUL. Characters below U+0100 - which dominate the tags this has to rescue -
279-
* encode as {@code 0x00 lo} big-endian and {@code lo 0x00} little-endian. Text with no
280-
* NUL bytes at all (eg CJK) gives no signal, so it stays on the big-endian default.
271+
* Byte order of BOM-less UTF-16, from which column holds each code unit's NUL: chars below
272+
* U+0100 dominate ID3 tags and encode as {@code 0x00 lo} (BE) or {@code lo 0x00} (LE). No
273+
* NULs (eg CJK) means no signal, so keep the big-endian default. Deterministic single-field
274+
* form of {@code Utf16ColumnFeatureExtractor} features 0/1.
281275
*/
282276
private static String guessUTF16ByteOrder(byte[] data, int offset, int length) {
283277
int bigEndian = 0;
@@ -295,11 +289,11 @@ private static String guessUTF16ByteOrder(byte[] data, int offset, int length) {
295289

296290
/**
297291
* Builds up the ID3 comment, by parsing and extracting
298-
* the comment string parts from the given data.
299-
* Returns null if the frame is too short or malformed to hold a comment.
292+
* the comment string parts from the given data, or null if the frame
293+
* is too short or malformed to hold a comment.
300294
*/
301295
protected static ID3Comment getComment(byte[] data, int offset, int length) {
302-
// A comment is at minimum an encoding flag and a 3 byte language
296+
// need at least an encoding flag + 3 byte language
303297
if (length < 4) {
304298
return null;
305299
}
@@ -328,7 +322,7 @@ protected static ID3Comment getComment(byte[] data, int offset, int length) {
328322
// Find where the description ends
329323
try {
330324
for (int i = descStart; i < end; i++) {
331-
// A double byte terminator needs two bytes to be present
325+
// a double byte terminator needs both bytes present
332326
if (encoding.doubleByte && i + 1 < end && data[i] == 0 && data[i + 1] == 0) {
333327
// Handle LE vs BE on low byte text
334328
if (i + 2 < end && data[i + 2] == 0) {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ public class ID3v2FrameTest {
4444
private static final byte[] BOM_LE = {(byte) 0xff, (byte) 0xfe};
4545
private static final byte[] BOM_BE = {(byte) 0xfe, (byte) 0xff};
4646

47-
/**
48-
* Latin text, which encodes to a NUL byte per character in UTF-16 and so carries a
49-
* byte-order signal, and CJK text, which does not.
50-
*/
47+
// LATIN has a NUL per char in UTF-16 (a byte-order signal); CJK does not
5148
private static final String LATIN = "Test Copyright";
5249
private static final String CJK = "日本語";
5350

@@ -88,21 +85,14 @@ public void testUTF16BEWithoutBOMFlag() {
8885
assertEquals(CJK, tagString(frame(UTF_16BE_FLAG, bytes(CJK, UTF_16BE))));
8986
}
9087

91-
/**
92-
* Encoding $01 promises a BOM. Taggers that omit it used to be decoded as big-endian,
93-
* turning little-endian text into CJK mojibake ('T' 0x54 0x00 becomes U+5400).
94-
*/
88+
// $01 promises a BOM; omitting it used to decode LE as BE mojibake ('T' 0x54 0x00 -> U+5400)
9589
@Test
9690
public void testUTF16WithoutBOMRecoversByteOrder() {
9791
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN, UTF_16LE))));
9892
assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN, UTF_16BE))));
9993
}
10094

101-
/**
102-
* A BOM-less frame whose characters are all above U+00FF has no NUL bytes and so gives
103-
* no byte-order signal at all. Nothing can rescue that, so the big-endian default of
104-
* Java's UTF-16 charset must be preserved rather than guessed away.
105-
*/
95+
// no NUL bytes (all chars above U+00FF) means no signal, so keep the big-endian default
10696
@Test
10797
public void testUTF16WithoutBOMKeepsBigEndianDefaultWhenNoSignal() {
10898
assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, bytes(CJK, UTF_16BE))));
@@ -118,19 +108,14 @@ public void testNullTerminationIsTrimmed() {
118108
assertEquals(LATIN, tagString(frame(ISO_8859_1_FLAG, bytes(LATIN, ISO_8859_1), singleNul)));
119109
}
120110

121-
/**
122-
* TIKA-1024: a frame holding nothing but a BOM decodes to the empty string.
123-
*/
111+
// TIKA-1024: a frame holding nothing but a BOM decodes to the empty string
124112
@Test
125113
public void testNakedBOM() {
126114
assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_LE)));
127115
assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_BE)));
128116
}
129117

130-
/**
131-
* COMM frames decode a description and a text part separately, so both have to recover
132-
* the byte order independently.
133-
*/
118+
// COMM decodes description and text separately, so each recovers byte order on its own
134119
@Test
135120
public void testCommentWithoutBOMRecoversByteOrder() throws IOException {
136121
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -165,11 +150,7 @@ public void testCommentWithBOM() throws IOException {
165150
assertEquals(LATIN, comment.getText());
166151
}
167152

168-
/**
169-
* A comment frame too short to hold an encoding flag and a 3 byte language, or one
170-
* carrying an unknown encoding flag, decodes to null rather than reading off the end
171-
* of the frame.
172-
*/
153+
// too short for flag + 3 byte language, or an unknown flag, decodes to null (no overrun)
173154
@Test
174155
public void testMalformedCommentsReturnNull() {
175156
assertNull(ID3v2Frame.getComment(new byte[0], 0, 0));
@@ -180,10 +161,7 @@ public void testMalformedCommentsReturnNull() {
180161
assertNull(ID3v2Frame.getComment(badFlag, 0, badFlag.length));
181162
}
182163

183-
/**
184-
* A double byte comment whose frame ends on a lone NUL has no room for a terminator,
185-
* and must not read past the end of the frame looking for one.
186-
*/
164+
// a double byte comment ending on a lone NUL must not overrun looking for a terminator
187165
@Test
188166
public void testCommentTruncatedOnTerminatorDoesNotOverrun() {
189167
byte[] data = {UTF_16_BOM_FLAG, 'e', 'n', 'g', 0};

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -368,21 +368,15 @@ public void testNakedUTF16BOM() throws Exception {
368368
assertEquals("", metadata.get(XMPDM.GENRE));
369369
}
370370

371-
/**
372-
* A malformed COMM frame must not take the whole parse down with it. Each of these
373-
* frame bodies used to abort the parse of otherwise readable audio, with an
374-
* ArrayIndexOutOfBounds, a StringIndexOutOfBounds or a NullPointerException.
375-
*/
371+
// each body used to abort the parse of readable audio (AIOOBE / SIOOBE / NPE)
376372
@Test
377373
public void testMalformedCommentFrameIsSkipped() throws Exception {
378374
byte[][] bodies = {
379375
new byte[0], // empty body
380376
new byte[]{1}, // no language
381377
new byte[]{0, 'e', 'n'}, // truncated language
382-
// 0x05 is not a defined ID3v2 text encoding
383-
new byte[]{5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'},
384-
// double byte encoding, but the frame ends before the two byte terminator
385-
new byte[]{1, 'e', 'n', 'g', 0}
378+
new byte[]{5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'}, // 0x05 = unknown encoding
379+
new byte[]{1, 'e', 'n', 'g', 0} // ends before the double byte terminator
386380
};
387381
for (byte[] body : bodies) {
388382
String name = "COMM body of length " + body.length;
@@ -391,14 +385,12 @@ public void testMalformedCommentFrameIsSkipped() throws Exception {
391385
new Mp3Parser().parse(tis, new BodyContentHandler(-1), metadata, new ParseContext());
392386
}
393387
assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE), name);
394-
// the audio behind the broken tag is still read
388+
// audio behind the broken tag is still read
395389
assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE), name);
396390
}
397391
}
398392

399-
/**
400-
* Wraps the audio of testMP3noid3.mp3 in an ID3v2.3 tag holding the single given frame.
401-
*/
393+
// wraps the audio of testMP3noid3.mp3 in an ID3v2.3 tag holding the single given frame
402394
private byte[] mp3WithFrame(String frameId, byte[] body) throws Exception {
403395
byte[] audio;
404396
try (TikaInputStream tis = getResourceAsStream("/test-documents/testMP3noid3.mp3")) {
@@ -417,7 +409,7 @@ private byte[] mp3WithFrame(String frameId, byte[] body) throws Exception {
417409
ByteArrayOutputStream mp3 = new ByteArrayOutputStream();
418410
mp3.write("ID3".getBytes(StandardCharsets.US_ASCII));
419411
mp3.write(new byte[]{3, 0, 0});
420-
// the tag size is synchsafe: seven bits per byte
412+
// tag size is synchsafe (7 bits/byte), unlike the plain frame size above
421413
int size = frames.length;
422414
mp3.write(new byte[]{(byte) ((size >>> 21) & 0x7f), (byte) ((size >>> 14) & 0x7f),
423415
(byte) ((size >>> 7) & 0x7f), (byte) (size & 0x7f)});

0 commit comments

Comments
 (0)