/*
* java.util.Properties.java modified by Kevin Gaudin to allow usage of enums as keys.
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kidozen.client.crash;
import android.content.Context;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.Map;
/**
* Stores a crash reports data with {@link org.acra.ReportField} enum values as keys.
* This is basically the source of {@link java.util.Properties} adapted to extend an
* EnumMap instead of Hashtable and with a few tweaks to avoid losing crazy
* amounts of android time in the generation of a date comment when storing to
* file.
*/
final class CrashReportPersister {
private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, KEY_DONE = 4, IGNORE = 5;
private static final String LINE_SEPARATOR = "\n";
private final Context context;
CrashReportPersister(Context context) {
this.context = context;
}
/**
* Loads properties from the specified {@code InputStream}. The encoding is
* ISO8859-1.
*
* @param fileName Name of the report file from which to load the CrashData.
* @return CrashReportData read from the supplied InputStream.
* @throws java.io.IOException if error occurs during reading from the {@code InputStream}.
*/
public CrashReportData load(String fileName) throws IOException {
final FileInputStream in = context.openFileInput(fileName);
if (in == null) {
throw new IllegalArgumentException("Invalid crash report fileName : " + fileName);
}
try {
final BufferedInputStream bis = new BufferedInputStream(in, CrashConstants.DEFAULT_BUFFER_SIZE_IN_BYTES);
bis.mark(Integer.MAX_VALUE);
final boolean isEbcdic = isEbcdic(bis);
bis.reset();
if (!isEbcdic) {
return load(new InputStreamReader(bis, "ISO8859-1")); //$NON-NLS-1$
} else {
return load(new InputStreamReader(bis)); //$NON-NLS-1$
}
} finally {
in.close();
}
}
/**
* Stores the mappings in this Properties to the specified OutputStream,
* putting the specified comment at the beginning. The output from this
* method is suitable for being read by the load() method.
*
* @param crashData CrashReportData to save.
* @param fileName Name of the file to which to store the CrashReportData.
* @throws java.io.IOException if the CrashReportData could not be written to the OutputStream.
*/
public void store(CrashReportData crashData, String fileName) throws IOException {
final OutputStream out = context.openFileOutput(fileName, Context.MODE_PRIVATE);
try {
final StringBuilder buffer = new StringBuilder(200);
final OutputStreamWriter writer = new OutputStreamWriter(out, "ISO8859_1"); //$NON-NLS-1$
for (final Map.Entry<ReportField, String> entry : crashData.entrySet()) {
final String key = entry.getKey().toString();
dumpString(buffer, key, true);
buffer.append('=');
dumpString(buffer, entry.getValue(), false);
buffer.append(LINE_SEPARATOR);
writer.write(buffer.toString());
buffer.setLength(0);
}
writer.flush();
} finally {
out.close();
}
}
private boolean isEbcdic(BufferedInputStream in) throws IOException {
byte b;
while ((b = (byte) in.read()) != -1) {
if (b == 0x23 || b == 0x0a || b == 0x3d) {// ascii: newline/#/=
return false;
}
if (b == 0x15) {// EBCDIC newline
return true;
}
}
// we found no ascii newline, '#', neither '=', relative safe to
// consider it
// as non-ascii, the only exception will be a single line with only
// key(no value and '=')
// in this case, it should be no harm to read it in default charset
return false;
}
/**
* Loads properties from the specified InputStream. The properties are of
* the form <code>key=value</code>, one property per line. It may be not
* encode as 'ISO-8859-1'.The {@code Properties} file is interpreted
* according to the following rules:
* <ul>
* <li>Empty lines are ignored.</li>
* <li>Lines starting with either a "#" or a "!" are comment lines and are
* ignored.</li>
* <li>A backslash at the end of the line escapes the following newline
* character ("\r", "\n", "\r\n"). If there's a whitespace after the
* backslash it will just escape that whitespace instead of concatenating
* the lines. This does not apply to comment lines.</li>
* <li>A property line consists of the key, the space between the key and
* the value, and the value. The key goes up to the first whitespace, "=" or
* ":" that is not escaped. The space between the key and the value contains
* either one whitespace, one "=" or one ":" and any number of additional
* whitespaces before and after that character. The value starts with the
* first character after the space between the key and the value.</li>
* <li>Following escape sequences are recognized: "\ ", "\\", "\r", "\n",
* "\!", "\#", "\t", "\b", "\f", and "\uXXXX" (unicode character).</li>
* </ul>
*
* @param reader Reader from which to read the properties of this CrashReportData.
* @return CrashReportData read from the supplied Reader.
* @throws java.io.IOException if the properties could not be read.
* @since 1.6
*/
private synchronized CrashReportData load(Reader reader) throws IOException {
int mode = NONE, unicode = 0, count = 0;
char nextChar, buf[] = new char[40];
int offset = 0, keyLength = -1, intVal;
boolean firstChar = true;
final CrashReportData crashData = new CrashReportData();
final BufferedReader br = new BufferedReader(reader, CrashConstants.DEFAULT_BUFFER_SIZE_IN_BYTES);
while (true) {
intVal = br.read();
if (intVal == -1) {
break;
}
nextChar = (char) intVal;
if (offset == buf.length) {
final char[] newBuf = new char[buf.length * 2];
System.arraycopy(buf, 0, newBuf, 0, offset);
buf = newBuf;
}
if (mode == UNICODE) {
final int digit = Character.digit(nextChar, 16);
if (digit >= 0) {
unicode = (unicode << 4) + digit;
if (++count < 4) {
continue;
}
} else if (count <= 4) {
// luni.09=Invalid Unicode sequence: illegal character
throw new IllegalArgumentException("luni.09");
}
mode = NONE;
buf[offset++] = (char) unicode;
if (nextChar != '\n' && nextChar != '\u0085') {
continue;
}
}
if (mode == SLASH) {
mode = NONE;
switch (nextChar) {
case '\r':
mode = CONTINUE; // Look for a following \n
continue;
case '\u0085':
case '\n':
mode = IGNORE; // Ignore whitespace on the next line
continue;
case 'b':
nextChar = '\b';
break;
case 'f':
nextChar = '\f';
break;
case 'n':
nextChar = '\n';
break;
case 'r':
nextChar = '\r';
break;
case 't':
nextChar = '\t';
break;
case 'u':
mode = UNICODE;
unicode = count = 0;
continue;
}
} else {
switch (nextChar) {
case '#':
case '!':
if (firstChar) {
while (true) {
intVal = br.read();
if (intVal == -1) {
break;
}
nextChar = (char) intVal; // & 0xff
// not
// required
if (nextChar == '\r' || nextChar == '\n' || nextChar == '\u0085') {
break;
}
}
continue;
}
break;
case '\n':
if (mode == CONTINUE) { // Part of a \r\n sequence
mode = IGNORE; // Ignore whitespace on the next line
continue;
}
// fall into the next case
case '\u0085':
case '\r':
mode = NONE;
firstChar = true;
if (offset > 0 || (offset == 0 && keyLength == 0)) {
if (keyLength == -1) {
keyLength = offset;
}
final String temp = new String(buf, 0, offset);
crashData.put(Enum.valueOf(ReportField.class, temp.substring(0, keyLength)), temp.substring(keyLength));
}
keyLength = -1;
offset = 0;
continue;
case '\\':
if (mode == KEY_DONE) {
keyLength = offset;
}
mode = SLASH;
continue;
case ':':
case '=':
if (keyLength == -1) { // if parsing the key
mode = NONE;
keyLength = offset;
continue;
}
break;
}
if (Character.isWhitespace(nextChar)) {
if (mode == CONTINUE) {
mode = IGNORE;
}
// if key length == 0 or value length == 0
if (offset == 0 || offset == keyLength || mode == IGNORE) {
continue;
}
if (keyLength == -1) { // if parsing the key
mode = KEY_DONE;
continue;
}
}
if (mode == IGNORE || mode == CONTINUE) {
mode = NONE;
}
}
firstChar = false;
if (mode == KEY_DONE) {
keyLength = offset;
mode = NONE;
}
buf[offset++] = nextChar;
}
if (mode == UNICODE && count <= 4) {
// luni.08=Invalid Unicode sequence: expected format \\uxxxx
throw new IllegalArgumentException("luni.08");
}
if (keyLength == -1 && offset > 0) {
keyLength = offset;
}
if (keyLength >= 0) {
final String temp = new String(buf, 0, offset);
final ReportField key = Enum.valueOf(ReportField.class, temp.substring(0, keyLength));
String value = temp.substring(keyLength);
if (mode == SLASH) {
value += "\u0000";
}
crashData.put(key, value);
}
return crashData;
}
/**
* Constructs a new {@code Properties} object.
*
* @param buffer StringBuilder to populate with the supplied property.
* @param string String to append to the buffer.
* @param key Whether the String is a key value or not.
*/
private void dumpString(StringBuilder buffer, String string, boolean key) {
int i = 0;
if (!key && i < string.length() && string.charAt(i) == ' ') {
buffer.append("\\ "); //$NON-NLS-1$
i++;
}
for (; i < string.length(); i++) {
char ch = string.charAt(i);
switch (ch) {
case '\t':
buffer.append("\\t"); //$NON-NLS-1$
break;
case '\n':
buffer.append("\\n"); //$NON-NLS-1$
break;
case '\f':
buffer.append("\\f"); //$NON-NLS-1$
break;
case '\r':
buffer.append("\\r"); //$NON-NLS-1$
break;
default:
if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) {
buffer.append('\\');
}
if (ch >= ' ' && ch <= '~') {
buffer.append(ch);
} else {
final String hex = Integer.toHexString(ch);
buffer.append("\\u"); //$NON-NLS-1$
for (int j = 0; j < 4 - hex.length(); j++) {
buffer.append("0"); //$NON-NLS-1$
}
buffer.append(hex);
}
}
}
}
}