blob: 10ad45660cd7697b30fef321b5de1172cff5b465 [file] [log] [blame]
package com.android.server.wifi.configparse;
import android.util.Log;
import com.android.server.wifi.hotspot2.Utils;
import java.io.IOException;
import java.io.LineNumberReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
public class MIMEContainer {
private static final String Type = "Content-Type";
private static final String Encoding = "Content-Transfer-Encoding";
private static final String Boundary = "boundary=";
private static final String CharsetTag = "charset=";
private final boolean mLast;
private final List<MIMEContainer> mMimeContainers;
private final String mText;
private final boolean mMixed;
private final boolean mBase64;
private final Charset mCharset;
private final String mContentType;
/**
* Parse nested MIME content
* @param in A reader to read MIME data from; Note that the charset should be ISO-8859-1 to
* ensure transparent octet to character mapping. This is because the content will
* be re-encoded using the correct charset once it is discovered.
* @param boundary A boundary string for the MIME section that this container is in.
* Pass null for the top level object.
* @throws java.io.IOException
*/
public MIMEContainer(LineNumberReader in, String boundary) throws IOException {
Map<String,List<String>> headers = parseHeader(in);
List<String> type = headers.get(Type);
if (type == null || type.isEmpty()) {
throw new IOException("Missing " + Type + " @ " + in.getLineNumber());
}
boolean multiPart = false;
boolean mixed = false;
String subBoundary = null;
Charset charset = StandardCharsets.ISO_8859_1;
mContentType = type.get(0);
if (mContentType.startsWith("multipart/")) {
multiPart = true;
for (String attribute : type) {
if (attribute.startsWith(Boundary)) {
subBoundary = Utils.unquote(attribute.substring(Boundary.length()));
}
}
if (mContentType.endsWith("/mixed")) {
mixed = true;
}
}
else if (mContentType.startsWith("text/")) {
for (String attribute : type) {
if (attribute.startsWith(CharsetTag)) {
charset = Charset.forName(attribute.substring(CharsetTag.length()));
}
}
}
mMixed = mixed;
mCharset = charset;
if (multiPart && subBoundary != null) {
for (;;) {
String line = in.readLine();
if (line == null) {
throw new IOException("Unexpected EOF before first boundary @ " +
in.getLineNumber());
}
if (line.startsWith("--") && line.length() == subBoundary.length() + 2 &&
line.regionMatches(2, subBoundary, 0, subBoundary.length())) {
break;
}
}
mMimeContainers = new ArrayList<>();
for (;;) {
MIMEContainer container = new MIMEContainer(in, subBoundary);
mMimeContainers.add(container);
if (container.isLast()) {
break;
}
}
}
else {
mMimeContainers = null;
}
List<String> encoding = headers.get(Encoding);
boolean quoted = false;
boolean base64 = false;
if (encoding != null) {
for (String text : encoding) {
if (text.equalsIgnoreCase("quoted-printable")) {
quoted = true;
break;
}
else if (text.equalsIgnoreCase("base64")) {
base64 = true;
break;
}
}
}
mBase64 = base64;
Log.d(Utils.hs2LogTag(getClass()),
String.format("%s MIME container, boundary '%s', type '%s', encoding %s",
multiPart ? "multipart" : "plain", boundary, mContentType, encoding));
AtomicBoolean eof = new AtomicBoolean();
mText = recode(getBody(in, boundary, quoted, eof), charset);
mLast = eof.get();
}
public List<MIMEContainer> getMimeContainers() {
return mMimeContainers;
}
public String getText() {
return mText;
}
public boolean isMixed() {
return mMixed;
}
public boolean isBase64() {
return mBase64;
}
public String getContentType() {
return mContentType;
}
private boolean isLast() {
return mLast;
}
private void toString(StringBuilder sb, int nesting) {
char[] indent = new char[nesting*4];
Arrays.fill(indent, ' ');
if (mBase64) {
sb.append("base64, type ").append(mContentType).append('\n');
}
else if (mMimeContainers != null) {
sb.append(indent).append("multipart/").append((mMixed ? "mixed" : "other" )).append('\n');
}
else {
sb.append(indent).append(
String.format("%s, type %s",
mCharset,
mContentType)
).append('\n');
}
if (mMimeContainers != null) {
for (MIMEContainer mimeContainer : mMimeContainers) {
mimeContainer.toString(sb, nesting + 1);
}
}
sb.append(indent).append("Text: ");
if (mText.length() < 100000) {
sb.append("'").append(mText).append("'\n");
}
else {
sb.append(mText.length()).append(" chars\n");
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(sb, 0);
return sb.toString();
}
private static Map<String,List<String>> parseHeader(LineNumberReader in) throws IOException {
StringBuilder value = null;
String header = null;
Map<String,List<String>> headers = new HashMap<>();
for (;;) {
String line = in.readLine();
if ( line == null ) {
throw new IOException("Missing body @ " + in.getLineNumber());
}
else if (line.length() == 0) {
break;
}
if (line.charAt(0) <= ' ') {
if (value == null) {
throw new IOException("Illegal blank prefix in header line '" + line + "' @ " + in.getLineNumber());
}
value.append(' ').append(line.trim());
continue;
}
int nameEnd = line.indexOf(':');
if (nameEnd < 0) {
throw new IOException("Bad header line: '" + line + "' @ " + in.getLineNumber());
}
if (header != null) {
String[] values = value.toString().split(";");
List<String> valueList = new ArrayList<>(values.length);
for (String segment : values) {
valueList.add(segment.trim());
}
headers.put(header, valueList);
//System.out.println("Header '" + header + "' = " + valueList);
}
header = line.substring(0, nameEnd);
value = new StringBuilder();
value.append(line.substring(nameEnd+1).trim());
}
if (header != null) {
String[] values = value.toString().split(";");
List<String> valueList = new ArrayList<>(values.length);
for (String segment : values) {
valueList.add(segment.trim());
}
headers.put(header, valueList);
//System.out.println("Header '" + header + "' = " + valueList);
}
return headers;
}
private static String getBody(LineNumberReader in, String boundary, boolean quoted, AtomicBoolean eof)
throws IOException {
StringBuilder text = new StringBuilder();
for (;;) {
String line = in.readLine();
if (line == null) {
if (boundary != null) {
throw new IOException("Unexpected EOF file in body @ " + in.getLineNumber());
}
else {
return text.toString();
}
}
Boolean end = boundaryCheck(line, boundary);
if (end != null) {
eof.set(end);
//System.out.println("Boundary " + boundary + ": " + end);
return text.toString();
}
if (quoted) {
if (line.endsWith("=")) {
text.append(unescape(line.substring(line.length() - 1), in.getLineNumber()));
}
else {
text.append(unescape(line, in.getLineNumber()));
}
}
else {
text.append(line);
}
}
}
private static String recode(String s, Charset charset) {
if (charset.equals(StandardCharsets.ISO_8859_1) || charset.equals(StandardCharsets.US_ASCII)) {
return s;
}
byte[] octets = s.getBytes(StandardCharsets.ISO_8859_1);
return new String(octets, charset);
}
private static Boolean boundaryCheck(String line, String boundary) {
if (line.startsWith("--") && line.regionMatches(2, boundary, 0, boundary.length())) {
if (line.length() == boundary.length() + 2) {
return Boolean.FALSE;
}
else if (line.length() == boundary.length() + 4 && line.endsWith("--") ) {
return Boolean.TRUE;
}
}
return null;
}
private static String unescape(String text, int line) throws IOException {
StringBuilder sb = new StringBuilder();
for (int n = 0; n < text.length(); n++) {
char ch = text.charAt(n);
if (ch > 127) {
throw new IOException("Bad codepoint " + (int)ch + " in quoted printable @ " + line);
}
if (ch == '=' && n < text.length() - 2) {
int h1 = fromStrictHex(text.charAt(n+1));
int h2 = fromStrictHex(text.charAt(n+2));
if (h1 >= 0 && h2 >= 0) {
sb.append((char)((h1 << 4) | h2));
n += 2;
}
else {
sb.append(ch);
}
}
else {
sb.append(ch);
}
}
return sb.toString();
}
private static int fromStrictHex(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
}
else {
return -1;
}
}
}