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; } } }