org.eclipse.swt.internal.Converter.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swt.internal.Converter.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.internal;

import java.io.*;
import java.nio.*;
import java.nio.charset.*;

import org.eclipse.swt.internal.gtk.*;

/**
 * About this class:
 * #################
 * This class implements the conversions between unicode characters
 * and the platform supported representation for characters.
 *
 * Note that, unicode characters which can not be found in the platform
 * encoding will be converted to an arbitrary platform specific character.
 *
 * This class is tested via: org.eclipse.swt.tests.gtk.Test_GtkTextEncoding
 *
 * About JNI & string conversion:
 * #############################
 * - Regular JNI String conversion usually uses a modified UTF-8, see:  https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
 * - And in JNI, normally (env*)->GetStringUTFChars(..) is used to convert a javaString into a C string.
 *     See: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetStringUTFChars
 *
 * However, the modified UTF-8 only works well with C system functions as it doesn't contain embedded nulls
 * and is null terminated.
 *
 * But because the modified UTF-8 only supports up to 3 bytes (and not up to 4 as regular UTF-8), characters
 * that require 4 bytes (e.g emojos) are not translated properly from Java to C.
 *
 * To work around this issue, we convert the Java string to a byte array on the Java side manually and then  pass it to C.
 *   See: http://stackoverflow.com/questions/32205446/getting-true-utf-8-characters-in-java-jni
 *
 * Note:
 * Java uses UTF-16 Wide characters internally to represent a string.
 * C uses UTF-8 Multibyte characters (null terminated) to represent a string.
 *
 * About encoding on Linux/Gtk & it's relevance to SWT:
 * ####################################################
 *
 * UTF-* = variable length encoding.
 *
 * UTF-8 = minimum is 8 bits, max is 6 bytes, but rarely goes beyond 4 bytes. Gtk & most of web uses this.
 * UTF-16 = minimum is 16 bits. Java's string are stored this way.
 *          UTF-16 can be
 *            Big    Endian  : 65 = 00000000  01000001    # Human friendly, reads left to right.
 *            Little Endian  : 65 = 01000001  00000000    # Intel x86 and also AMD64 / x86-64 series of processors use the little-endian [1]
 *                                               #  i.e, we in SWT often have to deal with UTF-16 LE
 * Some terminology:
 * - "Code point" is the numerical value of unicode character.
 * - All of UTF-* have the same letter to code-point mapping,
 *   but UTF-8/16/32 have different "back-ends".
 *
 *   Illustration:
 *   (char) = (code point) = (back end).
 *       A =     65       = 01000001          UTF-8
 *                     = 00000000  01000001 UTF-16 BE
 *                     = 01000001  00000000 UTF-16 LE
 *
 * - Byte Order Marks (BOM) are a few bytes at the start of a *file* indicating which endianess is used.
 *   Problem: Gtk/webkit often don't give us BOM's.
 *   (further reading *3)
 *
 * - We can reliably encode character to a backend (A -> UTF-8/16), but the other way round is
 *   guess work since byte order marks are often missing and UTF-16 bits are technically valid UTF-8.
 *   (see Converter.heuristic for details).
 *   We could improve our heuristic by using something like http://jchardet.sourceforge.net/.
 *
 * - Glib has some conversion functions:
 *   g_utf16_to_utf8
 *   g_utf8_to_utf16
 *
 * - So does java: (e.g null terminated UTF-8)
 *   ("myString" + '\0').getBytes(StandardCharsets.UTF-8)
 *
 * - I suggest using Java functions where possible to avoid memory leaks.
 *   (Yes, they happen and are big-pain-in-the-ass to find https://bugs.eclipse.org/bugs/show_bug.cgi?id=533995)
 *
 *
 * Learning about encoding:
 * #########################
 * I suggest the following 3 videos to understand ASCII/UTF-8/UTF-16[LE|BE]/UTF-32 encoding:
 * Overview: https://www.youtube.com/watch?v=MijmeoH9LT4
 * Details:
 * Part-1: https://www.youtube.com/watch?v=B1Sf1IhA0j4
 * Part-2: https://www.youtube.com/watch?v=-oYfv794R9s
 * Part-3: https://www.youtube.com/watch?v=vLBtrd9Ar28
 *
 * Also read all of this:
 * http://kunststube.net/encoding/
 * and this:
 * https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/
 *
 * And lastly, good utf-8 reference: https://en.wikipedia.org/wiki/UTF-8#Description
 *
 * You should now be a master of encoding. I wish you luck on your journey.
 *
 * [1] https://en.wikipedia.org/wiki/Endianness
 * [2] https://en.wikipedia.org/wiki/Byte_order_mark
 * [3] BOM's: http://unicode.org/faq/utf_bom.html#BOM
 */
public final class Converter {
    public static final byte[] NullByteArray = new byte[1];
    public static final byte[] EmptyByteArray = new byte[0];
    public static final char[] EmptyCharArray = new char[0];

    /**
     * Convert a "C" multibyte UTF-8 string byte array into a Java UTF-16 Wide character array.
     *
     * @param buffer - byte buffer with C bytes representing a string.
     * @return char array representing the string. Usually used for String construction like: new String(mbcsToWcs(..))
     */
    public static char[] mbcsToWcs(byte[] buffer) {
        long[] items_written = new long[1];
        long ptr = OS.g_utf8_to_utf16(buffer, buffer.length, null, items_written, null);
        if (ptr == 0)
            return EmptyCharArray;
        int length = (int) items_written[0];
        char[] chars = new char[length];
        C.memmove(chars, ptr, length * 2);
        OS.g_free(ptr);
        return chars;
    }

    /**
     * Convert a Java UTF-16 Wide character string into a C UTF-8 Multibyte byte array.
     *
     * This algorithm stops when it finds the first NULL character. I.e, if your Java String has embedded NULL
     * characters, then the returned string will only go up to the first NULL character.
     *
     *
     * @param string - a regular Java String
     * @param terminate - if <code>true</code> the byte buffer should be terminated with a null character.
     * @return byte array that can be passed to a native function.
     */
    public static byte[] wcsToMbcs(String string, boolean terminate) {
        int length = string.length();
        char[] buffer = new char[length];
        string.getChars(0, length, buffer, 0);
        return wcsToMbcs(buffer, terminate);
    }

    /**
     * Given a java String, convert it to a regular null terimnated C string,
     * to be used when calling a native C function.
     * @param string A java string.
     * @return a pointer to a C String. In C, this would be a 'char *'
     */
    public static byte[] javaStringToCString(String string) {
        return wcsToMbcs(string, true);
    }

    /**
     * This method takes a 'C' pointer (char *) or (gchar *), reads characters up to the terminating symbol '\0' and
     * converts it into a Java String.
     *
     * Note: In SWT we don't use JNI's native String functions because of the 3 vs 4 byte issue explained in Class description.
     * Instead we pass a character pointer from C to java and convert it to a String in Java manually.
     *
     * @param cCharPtr - A char * or a gchar *. Which will be freed up afterwards.
     * @param freecCharPtr - "true" means free up memory pointed to by cCharPtr.
     *          CAREFUL! If this string is part of a struct (ex GError), and a specialized
     *          free function (like g_error_free(..) is called on the whole struct, then you
     *          should not free up individual struct members with this function,
     *          as otherwise you can get unpredictable behavior).
     * @return a Java String object.
     */
    public static String cCharPtrToJavaString(long cCharPtr, boolean freecCharPtr) {
        int length = C.strlen(cCharPtr);
        byte[] buffer = new byte[length];
        C.memmove(buffer, cCharPtr, length);
        if (freecCharPtr) {
            OS.g_free(cCharPtr);
        }
        return new String(mbcsToWcs(buffer));
    }

    /**
     * Convert a Java UTF-16 Wide character array into a C UTF-8 Multibyte byte array.
     *
     * This algorithm stops when it finds the first NULL character. I.e, if your Java String has embedded NULL
     * characters, then the returned string will only go up to the first NULL character.
     *
     * @param chars - a regular Java String
     * @param terminate - if <code>true</code> the byte buffer should be terminated with a null character.
     * @return byte array that can be passed to a native function.
     */
    public static byte[] wcsToMbcs(char[] chars, boolean terminate) {
        long[] items_read = new long[1], items_written = new long[1];
        /*
        * Note that g_utf16_to_utf8()  stops converting
        * when it finds the first NULL.
        */
        long ptr = OS.g_utf16_to_utf8(chars, chars.length, items_read, items_written, null);
        if (ptr == 0)
            return terminate ? NullByteArray : EmptyByteArray;
        int written = (int) items_written[0];
        byte[] bytes = new byte[written + (terminate ? 1 : 0)];
        C.memmove(bytes, ptr, written);
        OS.g_free(ptr);
        return bytes;
    }

    /**
     * Convert a Java UTF-16 Wide character into a single C UTF-8 Multibyte character
     * that you can pass to a native function.
     * @param ch - Java UTF-16 wide character.
     * @return C UTF-8 Multibyte character.
     */
    public static char wcsToMbcs(char ch) {
        int key = ch & 0xFFFF;
        if (key <= 0x7F)
            return ch;
        byte[] buffer = wcsToMbcs(new char[] { ch }, false);
        if (buffer.length == 1)
            return (char) buffer[0];
        if (buffer.length == 2) {
            return (char) (((buffer[0] & 0xFF) << 8) | (buffer[1] & 0xFF));
        }
        return 0;
    }

    /**
     * Convert C UTF-8 Multibyte character into a Java UTF-16 Wide character.
     *
     * @param ch - C Multibyte UTF-8 character
     * @return Java UTF-16 Wide character
     */
    public static char mbcsToWcs(char ch) {
        int key = ch & 0xFFFF;
        if (key <= 0x7F)
            return ch;
        byte[] buffer;
        if (key <= 0xFF) {
            buffer = new byte[1];
            buffer[0] = (byte) key;
        } else {
            buffer = new byte[2];
            buffer[0] = (byte) ((key >> 8) & 0xFF);
            buffer[1] = (byte) (key & 0xFF);
        }
        char[] result = mbcsToWcs(buffer);
        if (result.length == 0)
            return 0;
        return result[0];
    }

    /**
     * Given a byte array with unknown encoding, try to decode it via (relatively simple) heuristic.
     * This is useful when we're not provided the encoding by OS/library.<br>
     *
     * Current implementation only supports standard java charsets but can be extended as needed.
     * This method could be improved by using http://jchardet.sourceforge.net/ <br>
     *
     * Run time is O(a * n) where a is a constant that varies depending on the size of input n, but roughly 1-20)
     *
     * @param bytes raw bits from the OS.
     * @return String based on the most pop
     */
    public static String byteToStringViaHeuristic(byte[] bytes) {
        /*
         * Technical notes:
         * - Given a sequence of bytes, UTF-8 and UTF-16 cannot determined deterministically (1*).
         * - However, UTF-16 has a lot of null bytes when code points are mostly in the 0-255 range (using only 2nd byte),
         *   a byte sequence with many null bytes is likely UTF-16.
         * - Valid UTF-8 technically can contain null bytes, but it's rare.
         *
         * Some times it can get confused if it receives two non-null bytes. e.g (E with two dots on top) = (UTF-16  [01,04])
         * It can either mean a valid set of UTF-8 characters or a single UTF-16 character.
         * This issue typically only occurs for very short sequences 1-5 characters of very special characters).
         * Improving the heuristic for such corner cases is complicated. We'd have to implement a mechanism
         * that would be aware of character frequencies and assign a score to the probability of each mapping.
         *
         * [1] https://softwareengineering.stackexchange.com/questions/187169/how-to-detect-the-encoding-of-a-file
         */
        // Base cases
        if ((bytes.length == 0) || (bytes.length == 1 && bytes[0] == 0)) {
            return "";
        }

        // Test if it's valid UTF-8.
        // Note, ASCII is a subset of UTF-8.
        try {
            CharsetDecoder charDecoder = StandardCharsets.UTF_8.newDecoder();
            charDecoder.onMalformedInput(CodingErrorAction.REPORT);
            charDecoder.onUnmappableCharacter(CodingErrorAction.REPORT);
            String text = charDecoder.decode(ByteBuffer.wrap(bytes)).toString();

            // No exception thrown means that we have valid UTF-8 "bit string". However, valid UTF-8 bit string doesn't mean it's the corect decoding.
            // We have assert correctness via an educated guess
            boolean probablyUTF8 = true;

            {
                // Problem 1: It might be UTF-16 since at the binary level UTF-16 can be valid UTF-8. (null is a valid utf-8 character).
                // Solution: Count nulls to try to guess if it's UTF-16.
                // Verified via
                //       org.eclipse.swt.tests.gtk.Test_GtkConverter.test_HeuristicUTF16_letters()
                //       org.eclipse.swt.tests.gtk.Test_GtkConverter.test_HeuristicUTF16_letter()
                double nullBytePercentageForUtf16 = 0.01; // if more than this % null bytes, then it's probably utf-16.
                int nullCount = 0;
                for (byte b : bytes) {
                    if (b == 0)
                        nullCount++;
                }
                double nullPercentage = (double) nullCount / (double) bytes.length;
                if (nullPercentage > nullBytePercentageForUtf16) {
                    probablyUTF8 = false;
                }
            }

            // Problem 2: Valid UTF-8 bit string can map to invalid code points (i.e undefined unicode)
            // Solution 2: verify that every character is a valid code point.
            if (probablyUTF8) {
                char[] chars = text.toCharArray();

                for (int i = 0; i < chars.length; i++) {
                    int codePoint = Character.codePointAt(chars, i);
                    if (!Character.isValidCodePoint(codePoint)) {
                        probablyUTF8 = false;
                        break;
                    }
                }
            }

            // Problem 3: Short 2-byte sequences are very ambiguous.
            //            E.g Unicode Hyphen U+2010 (which looks like a '-') ( which btw different from the ascii U+002D  '-' Hyphen-Minus)
            //                can be miss-understood as 16 (Synchronous Idle) & 32 (Space).
            // Solution: Unless we have two valid alphabet characters, it's probably a single utf-16 character.
            //           However, this leads to the problem that single non-alphabetic unicode characters are not recognized correctly.
            //           Below code is left in case recognizing alphabetic characters is of higher priority than exotic unicode once.
            //      if (probablyUTF8) {
            //         if (bytes.length == 2) {
            //            char [] chars = text.toCharArray();
            //            for (int i = 0; i < chars.length; i++) {
            //               int codePoint = Character.codePointAt(chars, i);
            //               if (!Character.isAlphabetic(codePoint)) {
            //                  probablyUTF8 = false;
            //                  break;
            //               }
            //            }
            //         }
            //      }

            if (!probablyUTF8) {
                return new String(bytes, StandardCharsets.UTF_16LE);
            } else {
                return text;
            }
        } catch (CharacterCodingException e) {
        }

        // Invalid UTF-8. Try other character sets.
        Charset[] commonWebCharSets = new Charset[] { StandardCharsets.UTF_16LE, StandardCharsets.ISO_8859_1,
                StandardCharsets.UTF_16BE, StandardCharsets.UTF_16 };
        for (Charset setToTry : commonWebCharSets) {
            try {
                CharsetDecoder charDecoder = setToTry.newDecoder();
                charDecoder.onMalformedInput(CodingErrorAction.REPORT);
                charDecoder.onUnmappableCharacter(CodingErrorAction.REPORT);
                return charDecoder.decode(ByteBuffer.wrap(bytes)).toString();
            } catch (CharacterCodingException e) {
            }
        }

        // Could not determine encoding.
        // Return error string with stack trace to help users determine which function lead to a failed decoding.
        StringWriter sw = new StringWriter();
        new Throwable("").printStackTrace(new PrintWriter(sw));
        return "SWT: Failed to decode byte buffer. Encoding is not ASCII/UTF-8/UTF-16[LE|BE|BOM]/ISO_8859_1. Stack trace:\n"
                + sw.toString();
    }

}