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}