This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4784-id3-utf16-bom
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/TIKA-4784-id3-utf16-bom by 
this push:
     new 8f5a35780a TIKA-4784: fix unicode in id3
8f5a35780a is described below

commit 8f5a35780a65d48337d8d0920b34f75f1d7cf5c4
Author: tallison <[email protected]>
AuthorDate: Fri Jul 10 13:32:54 2026 -0400

    TIKA-4784: fix unicode in id3
---
 .../org/apache/tika/parser/mp3/ID3v22Handler.java  |  5 +--
 .../org/apache/tika/parser/mp3/ID3v23Handler.java  |  5 +--
 .../org/apache/tika/parser/mp3/ID3v24Handler.java  |  5 +--
 .../org/apache/tika/parser/mp3/ID3v2Frame.java     | 30 ++++++++----------
 .../org/apache/tika/parser/mp3/ID3v2FrameTest.java | 36 +++++-----------------
 .../org/apache/tika/parser/mp3/Mp3ParserTest.java  | 20 ++++--------
 6 files changed, 28 insertions(+), 73 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
index eed4d31079..a5f4626a14 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
@@ -113,10 +113,7 @@ public class ID3v22Handler implements ID3Tags {
         return ID3v2Frame.getComment(data, offset, length);
     }
 
-    /**
-     * Malformed comment frames decode to null and are skipped rather than
-     * being added to the list, where they would trip up the consumers.
-     */
+    /** Skips null comments (malformed frames) that would trip up consumers. */
     private void addComment(ID3Comment comment) {
         if (comment != null) {
             comments.add(comment);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
index 959b6519a6..62342d74d0 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
@@ -99,10 +99,7 @@ public class ID3v23Handler implements ID3Tags {
         return ID3v2Frame.getComment(data, offset, length);
     }
 
-    /**
-     * Malformed comment frames decode to null and are skipped rather than
-     * being added to the list, where they would trip up the consumers.
-     */
+    /** Skips null comments (malformed frames) that would trip up consumers. */
     private void addComment(ID3Comment comment) {
         if (comment != null) {
             comments.add(comment);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
index 5b2de1d052..45af637a9a 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
@@ -105,10 +105,7 @@ public class ID3v24Handler implements ID3Tags {
         return ID3v2Frame.getComment(data, offset, length);
     }
 
-    /**
-     * Malformed comment frames decode to null and are skipped rather than
-     * being added to the list, where they would trip up the consumers.
-     */
+    /** Skips null comments (malformed frames) that would trip up consumers. */
     private void addComment(ID3Comment comment) {
         if (comment != null) {
             comments.add(comment);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
index d1dee624ca..3840a79b86 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
@@ -244,13 +244,9 @@ public class ID3v2Frame implements MP3Frame {
     }
 
     /**
-     * Decodes text in the frame's declared encoding.
-     * <p>
-     * Encoding {@code $01} is defined as UTF-16 <em>with</em> a BOM, but some 
taggers omit it.
-     * Java's UTF-16 charset then falls back to big-endian, which silently 
turns little-endian
-     * text into CJK mojibake (the ASCII 'T' {@code 0x54 0x00} decodes to 
U+5400). When the BOM
-     * is missing, recover the byte order from the position of the NUL bytes 
instead, and keep
-     * the big-endian fallback when the bytes carry no signal either way.
+     * Decodes text in the frame's declared encoding. Encoding {@code $01} is 
UTF-16 with a BOM;
+     * when a tagger omits it Java assumes big-endian and turns little-endian 
text into mojibake
+     * ({@code 'T' 0x54 0x00} decodes to U+5400), so recover the byte order 
from the bytes.
      */
     private static String decodeText(byte[] data, int offset, int length, 
TextEncoding encoding)
             throws UnsupportedEncodingException {
@@ -261,9 +257,7 @@ public class ID3v2Frame implements MP3Frame {
         return new String(data, offset, length, charset);
     }
 
-    /**
-     * Does the text at the given offset start with a UTF-16 byte order mark?
-     */
+    /** Does the text at the offset start with a UTF-16 BOM? */
     private static boolean hasBOM(byte[] data, int offset, int length) {
         if (length < 2) {
             return false;
@@ -274,10 +268,10 @@ public class ID3v2Frame implements MP3Frame {
     }
 
     /**
-     * Guesses the byte order of BOM-less UTF-16 text by counting which half 
of each code
-     * unit is NUL. Characters below U+0100 - which dominate the tags this has 
to rescue -
-     * encode as {@code 0x00 lo} big-endian and {@code lo 0x00} little-endian. 
Text with no
-     * NUL bytes at all (eg CJK) gives no signal, so it stays on the 
big-endian default.
+     * Byte order of BOM-less UTF-16, from which column holds each code unit's 
NUL: chars below
+     * U+0100 dominate ID3 tags and encode as {@code 0x00 lo} (BE) or {@code 
lo 0x00} (LE). No
+     * NULs (eg CJK) means no signal, so keep the big-endian default. 
Deterministic single-field
+     * form of {@code Utf16ColumnFeatureExtractor} features 0/1.
      */
     private static String guessUTF16ByteOrder(byte[] data, int offset, int 
length) {
         int bigEndian = 0;
@@ -295,11 +289,11 @@ public class ID3v2Frame implements MP3Frame {
 
     /**
      * Builds up the ID3 comment, by parsing and extracting
-     * the comment string parts from the given data.
-     * Returns null if the frame is too short or malformed to hold a comment.
+     * the comment string parts from the given data, or null if the frame
+     * is too short or malformed to hold a comment.
      */
     protected static ID3Comment getComment(byte[] data, int offset, int 
length) {
-        // A comment is at minimum an encoding flag and a 3 byte language
+        // need at least an encoding flag + 3 byte language
         if (length < 4) {
             return null;
         }
@@ -328,7 +322,7 @@ public class ID3v2Frame implements MP3Frame {
         // Find where the description ends
         try {
             for (int i = descStart; i < end; i++) {
-                // A double byte terminator needs two bytes to be present
+                // a double byte terminator needs both bytes present
                 if (encoding.doubleByte && i + 1 < end && data[i] == 0 && 
data[i + 1] == 0) {
                     // Handle LE vs BE on low byte text
                     if (i + 2 < end && data[i + 2] == 0) {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
index 6091d6031c..8fee5d672c 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
@@ -44,10 +44,7 @@ public class ID3v2FrameTest {
     private static final byte[] BOM_LE = {(byte) 0xff, (byte) 0xfe};
     private static final byte[] BOM_BE = {(byte) 0xfe, (byte) 0xff};
 
-    /**
-     * Latin text, which encodes to a NUL byte per character in UTF-16 and so 
carries a
-     * byte-order signal, and CJK text, which does not.
-     */
+    // 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 = "日本語";
 
@@ -88,21 +85,14 @@ public class ID3v2FrameTest {
         assertEquals(CJK, tagString(frame(UTF_16BE_FLAG, bytes(CJK, 
UTF_16BE))));
     }
 
-    /**
-     * Encoding $01 promises a BOM. Taggers that omit it used to be decoded as 
big-endian,
-     * turning little-endian text into CJK mojibake ('T' 0x54 0x00 becomes 
U+5400).
-     */
+    // $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))));
     }
 
-    /**
-     * A BOM-less frame whose characters are all above U+00FF has no NUL bytes 
and so gives
-     * no byte-order signal at all. Nothing can rescue that, so the big-endian 
default of
-     * Java's UTF-16 charset must be preserved rather than guessed away.
-     */
+    // 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))));
@@ -118,19 +108,14 @@ public class ID3v2FrameTest {
         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.
-     */
+    // 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 frames decode a description and a text part separately, so both 
have to recover
-     * the byte order independently.
-     */
+    // COMM decodes description and text separately, so each recovers byte 
order on its own
     @Test
     public void testCommentWithoutBOMRecoversByteOrder() throws IOException {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -165,11 +150,7 @@ public class ID3v2FrameTest {
         assertEquals(LATIN, comment.getText());
     }
 
-    /**
-     * A comment frame too short to hold an encoding flag and a 3 byte 
language, or one
-     * carrying an unknown encoding flag, decodes to null rather than reading 
off the end
-     * of the frame.
-     */
+    // 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));
@@ -180,10 +161,7 @@ public class ID3v2FrameTest {
         assertNull(ID3v2Frame.getComment(badFlag, 0, badFlag.length));
     }
 
-    /**
-     * A double byte comment whose frame ends on a lone NUL has no room for a 
terminator,
-     * and must not read past the end of the frame looking for one.
-     */
+    // 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};
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index 45af3e7679..bbe9936e2e 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -368,21 +368,15 @@ public class Mp3ParserTest extends TikaTest {
         assertEquals("", metadata.get(XMPDM.GENRE));
     }
 
-    /**
-     * A malformed COMM frame must not take the whole parse down with it. Each 
of these
-     * frame bodies used to abort the parse of otherwise readable audio, with 
an
-     * ArrayIndexOutOfBounds, a StringIndexOutOfBounds or a 
NullPointerException.
-     */
+    // each body used to abort the parse of readable audio (AIOOBE / SIOOBE / 
NPE)
     @Test
     public void testMalformedCommentFrameIsSkipped() throws Exception {
         byte[][] bodies = {
                 new byte[0],                                  // empty body
                 new byte[]{1},                                // no language
                 new byte[]{0, 'e', 'n'},                      // truncated 
language
-                // 0x05 is not a defined ID3v2 text encoding
-                new byte[]{5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'},
-                // double byte encoding, but the frame ends before the two 
byte terminator
-                new byte[]{1, 'e', 'n', 'g', 0}
+                new byte[]{5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'}, // 
0x05 = unknown encoding
+                new byte[]{1, 'e', 'n', 'g', 0}               // ends before 
the double byte terminator
         };
         for (byte[] body : bodies) {
             String name = "COMM body of length " + body.length;
@@ -391,14 +385,12 @@ public class Mp3ParserTest extends TikaTest {
                 new Mp3Parser().parse(tis, new BodyContentHandler(-1), 
metadata, new ParseContext());
             }
             assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE), 
name);
-            // the audio behind the broken tag is still read
+            // audio behind the broken tag is still read
             assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE), name);
         }
     }
 
-    /**
-     * Wraps the audio of testMP3noid3.mp3 in an ID3v2.3 tag holding the 
single given frame.
-     */
+    // wraps the audio of testMP3noid3.mp3 in an ID3v2.3 tag holding the 
single given frame
     private byte[] mp3WithFrame(String frameId, byte[] body) throws Exception {
         byte[] audio;
         try (TikaInputStream tis = 
getResourceAsStream("/test-documents/testMP3noid3.mp3")) {
@@ -417,7 +409,7 @@ public class Mp3ParserTest extends TikaTest {
         ByteArrayOutputStream mp3 = new ByteArrayOutputStream();
         mp3.write("ID3".getBytes(StandardCharsets.US_ASCII));
         mp3.write(new byte[]{3, 0, 0});
-        // the tag size is synchsafe: seven bits per byte
+        // tag size is synchsafe (7 bits/byte), unlike the plain frame size 
above
         int size = frames.length;
         mp3.write(new byte[]{(byte) ((size >>> 21) & 0x7f), (byte) ((size >>> 
14) & 0x7f),
                 (byte) ((size >>> 7) & 0x7f), (byte) (size & 0x7f)});

Reply via email to