Skip to content

Commit b969f2a

Browse files
committed
TIKA-4784: fix unicode in id3
1 parent 6f2aa85 commit b969f2a

6 files changed

Lines changed: 356 additions & 13 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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public ID3v22Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
7272
copyright = getTagString(tag.data, 0, tag.data.length);
7373
break;
7474
case "COM":
75-
comments.add(getComment(tag.data, 0, tag.data.length));
75+
addComment(getComment(tag.data, 0, tag.data.length));
7676
break;
7777
case "TRK":
7878
trackNumber = getTagString(tag.data, 0, tag.data.length);
@@ -113,6 +113,16 @@ 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+
*/
120+
private void addComment(ID3Comment comment) {
121+
if (comment != null) {
122+
comments.add(comment);
123+
}
124+
}
125+
116126
public boolean getTagsPresent() {
117127
return true;
118128
}

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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public ID3v23Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
7373
copyright = getTagString(tag.data, 0, tag.data.length);
7474
break;
7575
case "COMM":
76-
comments.add(getComment(tag.data, 0, tag.data.length));
76+
addComment(getComment(tag.data, 0, tag.data.length));
7777
break;
7878
case "TRCK":
7979
trackNumber = getTagString(tag.data, 0, tag.data.length);
@@ -99,6 +99,16 @@ 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+
*/
106+
private void addComment(ID3Comment comment) {
107+
if (comment != null) {
108+
comments.add(comment);
109+
}
110+
}
111+
102112
public boolean getTagsPresent() {
103113
return true;
104114
}

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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public ID3v24Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
7979
copyright = getTagString(tag.data, 0, tag.data.length);
8080
break;
8181
case "COMM":
82-
comments.add(getComment(tag.data, 0, tag.data.length));
82+
addComment(getComment(tag.data, 0, tag.data.length));
8383
break;
8484
case "TRCK":
8585
trackNumber = getTagString(tag.data, 0, tag.data.length);
@@ -105,6 +105,16 @@ 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+
*/
112+
private void addComment(ID3Comment comment) {
113+
if (comment != null) {
114+
comments.add(comment);
115+
}
116+
}
117+
108118
public boolean getTagsPresent() {
109119
return true;
110120
}

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: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,25 +230,80 @@ protected static String getTagString(byte[] data, int offset, int length) {
230230
// (return empty string), because new String(..)
231231
// gives different results on different JVMs
232232
if (encoding.encoding.equals("UTF-16") && actualLength == 2 &&
233-
((data[offset] == (byte) 0xff && data[offset + 1] == (byte) 0xfe) ||
234-
(data[offset] == (byte) 0xfe && data[offset + 1] == (byte) 0xff))) {
233+
hasBOM(data, offset, actualLength)) {
235234
return "";
236235
}
237236

238237
try {
239238
// Build the base string
240-
return new String(data, offset, actualLength, encoding.encoding);
239+
return decodeText(data, offset, actualLength, encoding);
241240
} catch (UnsupportedEncodingException e) {
242241
throw new RuntimeException("Core encoding " + encoding.encoding + " is not available",
243242
e);
244243
}
245244
}
246245

246+
/**
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.
254+
*/
255+
private static String decodeText(byte[] data, int offset, int length, TextEncoding encoding)
256+
throws UnsupportedEncodingException {
257+
String charset = encoding.encoding;
258+
if ("UTF-16".equals(charset) && !hasBOM(data, offset, length)) {
259+
charset = guessUTF16ByteOrder(data, offset, length);
260+
}
261+
return new String(data, offset, length, charset);
262+
}
263+
264+
/**
265+
* Does the text at the given offset start with a UTF-16 byte order mark?
266+
*/
267+
private static boolean hasBOM(byte[] data, int offset, int length) {
268+
if (length < 2) {
269+
return false;
270+
}
271+
int first = data[offset] & 0xff;
272+
int second = data[offset + 1] & 0xff;
273+
return (first == 0xff && second == 0xfe) || (first == 0xfe && second == 0xff);
274+
}
275+
276+
/**
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.
281+
*/
282+
private static String guessUTF16ByteOrder(byte[] data, int offset, int length) {
283+
int bigEndian = 0;
284+
int littleEndian = 0;
285+
for (int i = 0; i + 1 < length; i += 2) {
286+
if (data[offset + i] == 0) {
287+
bigEndian++;
288+
}
289+
if (data[offset + i + 1] == 0) {
290+
littleEndian++;
291+
}
292+
}
293+
return littleEndian > bigEndian ? "UTF-16LE" : "UTF-16BE";
294+
}
295+
247296
/**
248297
* Builds up the ID3 comment, by parsing and extracting
249298
* the comment string parts from the given data.
299+
* Returns null if the frame is too short or malformed to hold a comment.
250300
*/
251301
protected static ID3Comment getComment(byte[] data, int offset, int length) {
302+
// A comment is at minimum an encoding flag and a 3 byte language
303+
if (length < 4) {
304+
return null;
305+
}
306+
252307
// Comments must have an encoding
253308
int encodingFlag = data[offset];
254309
if (encodingFlag >= 0 && encodingFlag < encodings.length) {
@@ -264,36 +319,38 @@ protected static ID3Comment getComment(byte[] data, int offset, int length) {
264319
String lang = getString(data, offset + 1, 3);
265320

266321
// After that we have [Desc]\0(\0)[Text]
322+
int end = offset + length;
267323
int descStart = offset + 4;
268324
int textStart = -1;
269325
String description = null;
270326
String text = null;
271327

272328
// Find where the description ends
273329
try {
274-
for (int i = descStart; i < offset + length; i++) {
275-
if (encoding.doubleByte && data[i] == 0 && data[i + 1] == 0) {
330+
for (int i = descStart; i < end; i++) {
331+
// A double byte terminator needs two bytes to be present
332+
if (encoding.doubleByte && i + 1 < end && data[i] == 0 && data[i + 1] == 0) {
276333
// Handle LE vs BE on low byte text
277-
if (i + 2 < offset + length && data[i + 1] == 0 && data[i + 2] == 0) {
334+
if (i + 2 < end && data[i + 2] == 0) {
278335
i++;
279336
}
280337
textStart = i + 2;
281-
description = new String(data, descStart, i - descStart, encoding.encoding);
338+
description = decodeText(data, descStart, i - descStart, encoding);
282339
break;
283340
}
284341
if (!encoding.doubleByte && data[i] == 0) {
285342
textStart = i + 1;
286-
description = new String(data, descStart, i - descStart, encoding.encoding);
343+
description = decodeText(data, descStart, i - descStart, encoding);
287344
break;
288345
}
289346
}
290347

291348
// Did we find the end?
292349
if (textStart > -1) {
293-
text = new String(data, textStart, offset + length - textStart, encoding.encoding);
350+
text = decodeText(data, textStart, end - textStart, encoding);
294351
} else {
295352
// Assume everything is the text
296-
text = new String(data, descStart, offset + length - descStart, encoding.encoding);
353+
text = decodeText(data, descStart, end - descStart, encoding);
297354
}
298355

299356
// Return

0 commit comments

Comments
 (0)