001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024
025/**
026 * Small class to decode and encode UTF-8 characters. Decoding also keeps
027 * track of encoding errors. Processes one UTF-8 character at a time.
028 * The decoding method reports encoding validity in a field.
029 *
030 * Char. number range  |        UTF-8 octet sequence
031 *       (hexadecimal)    |              (binary)
032 *    --------------------+---------------------------------------------
033 *    0000 0000-0000 007F | 0xxxxxxx
034 *    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
035 *    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
036 *    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
037 *
038 * @author nicl
039 */
040public class UTF8 {
041
042    /** Complete or partial UTF-8 character, depending on conversion errors. */
043    public int utf8_c;
044
045    /** Bytes read in the decoding process. */
046    public byte[] chars_read;
047
048    /** UTF-8 validity status on last read character. */
049    public boolean bValidChar = false;
050
051    /**
052     * Given a character and an input stream returns the next decoded UTF-8
053     * character. The encoded UTF-8 character is between 1 and 4 bytes long.
054     * In order to preserve the validity and character value, the character is
055     * returned by the method and its validity is available through the
056     * bValidChar {link #bValidChar} field.
057     * @param c initial character
058     * @param in input stream used to read extra UTF-8 encoded data
059     * @return UTF-8 character or -1
060     * @throws IOException if an I/O error occurs while reading
061     */
062    public int readUtf8(int c, InputStream in) throws IOException {
063        ByteArrayOutputStream charsOut = new ByteArrayOutputStream(4);
064        byte utf8_read;
065        byte utf8_octets;
066        utf8_c = 0;
067        bValidChar = false;
068        if ((c & 0x80) == 0x00) {
069            // US-ASCII/UTF-8: 0000 0000-0000 007F | 0xxxxxxx
070            bValidChar = true;
071            utf8_c = c;
072        } else {
073            utf8_read = 1;
074            bValidChar = true;
075            if ((c & 0xE0) == 0xC0) {
076                // UTF-8: 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
077                utf8_c = c & 0x1F;
078                utf8_octets = 2;
079            } else if ((c & 0xF0) == 0xE0) {
080                // UTF-8: 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
081                utf8_c = c & 0x0F;
082                utf8_octets = 3;
083            } else if ((c & 0xF8) == 0xF0) {
084                // UTF-8: 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
085                utf8_c = c & 0x07;
086                utf8_octets = 4;
087            } else {
088                // Invalid UTF-8 octet.
089                utf8_c = 0;
090                utf8_read = 0;
091                utf8_octets = 0;
092                bValidChar = false;
093            }
094            // Read the remaining octets.
095            while (bValidChar && utf8_read < utf8_octets) {
096                c = in.read();
097                if (c == -1) {
098                    // EOF.
099                    bValidChar = false;
100                    chars_read = charsOut.toByteArray();
101                    return -1;
102                } else {
103                    charsOut.write(c);
104                    if ((c & 0xC0) == 0x80) {
105                        utf8_c = (utf8_c << 6) | (c & 0x3F);
106                        ++utf8_read;
107                    } else {
108                        // Invalid UTF-8 octet.
109                        bValidChar = false;
110                    }
111                }
112            }
113            // Correctly encoded.
114            if (utf8_read == utf8_octets) {
115                switch (utf8_octets) {
116                case 2:
117                    if (utf8_c < 0x00000080) {
118                        // Incorrectly encoded value.
119                        bValidChar = false;
120                    }
121                    break;
122                case 3:
123                    if (utf8_c < 0x00000800) {
124                        // Incorrectly encoded value.
125                        bValidChar = false;
126                    }
127                    break;
128                case 4:
129                    if (utf8_c < 0x00010000) {
130                        // Incorrectly encoded value.
131                        bValidChar = false;
132                    }
133                    break;
134                }
135            }
136            c = utf8_c;
137        }
138        chars_read = charsOut.toByteArray();
139        return c;
140    }
141
142    /**
143     * UTF-8 encodes a character and outputs in onto the stream.
144     * Returns the number of bytes used to encode the character.
145     * @param c character to UTF-8 encode
146     * @param out UTF-8 output stream
147     * @return the number of bytes used to encode the character
148     * @throws IOException if an I/O error occurs while writing
149     */
150    public int writeUtf8(int c, OutputStream out) throws IOException {
151        byte utf8_write = 1;
152        byte utf8_octets;
153        int shift;
154        int b;
155        if (c < 0x00000080) {
156            // US-ASCII/UTF-8: 0000 0000-0000 007F | 0xxxxxxx
157            out.write(c);
158            utf8_octets = 1;
159            shift = 0;
160        } else if (c < 0x00000800) {
161            // UTF-8: 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
162            b = (c >> 6) | 0xC0;
163            out.write(b);
164            utf8_octets = 2;
165            shift = 0;
166        } else if (c < 0x00010000) {
167            // UTF-8: 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
168            b = (c >> 12) | 0xE0;
169            out.write(b);
170            utf8_octets = 3;
171            shift = 6;
172        } else if (c < 0x00110000) {
173            // UTF-8: 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
174            b = (c >> 18) | 0xF0;
175            out.write(b);
176            utf8_octets = 4;
177            shift = 12;
178        } else {
179            throw new IOException("Character (0x" + Integer.toHexString(c) + ") not UTF-8 encodable!");
180        }
181        while (utf8_write < utf8_octets) {
182            b = ((c >> shift) & 0x3F) | 0x80;
183            out.write(b);
184            shift -= 6;
185            ++utf8_write;
186        }
187        return utf8_write;
188    }
189
190}