Home | History | Annotate | Download | only in configparse
      1 package com.android.server.wifi.configparse;
      2 
      3 import android.util.Log;
      4 
      5 import com.android.server.wifi.hotspot2.Utils;
      6 
      7 import java.io.IOException;
      8 import java.io.LineNumberReader;
      9 import java.nio.charset.Charset;
     10 import java.nio.charset.StandardCharsets;
     11 import java.util.ArrayList;
     12 import java.util.Arrays;
     13 import java.util.HashMap;
     14 import java.util.List;
     15 import java.util.Map;
     16 import java.util.concurrent.atomic.AtomicBoolean;
     17 
     18 public class MIMEContainer {
     19     private static final String Type = "Content-Type";
     20     private static final String Encoding = "Content-Transfer-Encoding";
     21 
     22     private static final String Boundary = "boundary=";
     23     private static final String CharsetTag = "charset=";
     24 
     25     private final boolean mLast;
     26     private final List<MIMEContainer> mMimeContainers;
     27     private final String mText;
     28 
     29     private final boolean mMixed;
     30     private final boolean mBase64;
     31     private final Charset mCharset;
     32     private final String mContentType;
     33 
     34     /**
     35      * Parse nested MIME content
     36      * @param in A reader to read MIME data from; Note that the charset should be ISO-8859-1 to
     37      *           ensure transparent octet to character mapping. This is because the content will
     38      *           be re-encoded using the correct charset once it is discovered.
     39      * @param boundary A boundary string for the MIME section that this container is in.
     40      *                 Pass null for the top level object.
     41      * @throws java.io.IOException
     42      */
     43     public MIMEContainer(LineNumberReader in, String boundary) throws IOException {
     44         Map<String,List<String>> headers = parseHeader(in);
     45 
     46         List<String> type = headers.get(Type);
     47         if (type == null || type.isEmpty()) {
     48             throw new IOException("Missing " + Type + " @ " + in.getLineNumber());
     49         }
     50 
     51         boolean multiPart = false;
     52         boolean mixed = false;
     53         String subBoundary = null;
     54         Charset charset = StandardCharsets.ISO_8859_1;
     55 
     56         mContentType = type.get(0);
     57 
     58         if (mContentType.startsWith("multipart/")) {
     59             multiPart = true;
     60 
     61             for (String attribute : type) {
     62                 if (attribute.startsWith(Boundary)) {
     63                     subBoundary = Utils.unquote(attribute.substring(Boundary.length()));
     64                 }
     65             }
     66 
     67             if (mContentType.endsWith("/mixed")) {
     68                 mixed = true;
     69             }
     70         }
     71         else if (mContentType.startsWith("text/")) {
     72             for (String attribute : type) {
     73                 if (attribute.startsWith(CharsetTag)) {
     74                     charset = Charset.forName(attribute.substring(CharsetTag.length()));
     75                 }
     76             }
     77         }
     78 
     79         mMixed = mixed;
     80         mCharset = charset;
     81 
     82         if (multiPart && subBoundary != null) {
     83             for (;;) {
     84                 String line = in.readLine();
     85                 if (line == null) {
     86                     throw new IOException("Unexpected EOF before first boundary @ " +
     87                             in.getLineNumber());
     88                 }
     89                 if (line.startsWith("--") && line.length() == subBoundary.length() + 2 &&
     90                         line.regionMatches(2, subBoundary, 0, subBoundary.length())) {
     91                     break;
     92                 }
     93             }
     94 
     95             mMimeContainers = new ArrayList<>();
     96             for (;;) {
     97                 MIMEContainer container = new MIMEContainer(in, subBoundary);
     98                 mMimeContainers.add(container);
     99                 if (container.isLast()) {
    100                     break;
    101                 }
    102             }
    103         }
    104         else {
    105             mMimeContainers = null;
    106         }
    107 
    108         List<String> encoding = headers.get(Encoding);
    109         boolean quoted = false;
    110         boolean base64 = false;
    111         if (encoding != null) {
    112             for (String text : encoding) {
    113                 if (text.equalsIgnoreCase("quoted-printable")) {
    114                     quoted = true;
    115                     break;
    116                 }
    117                 else if (text.equalsIgnoreCase("base64")) {
    118                     base64 = true;
    119                     break;
    120                 }
    121             }
    122         }
    123         mBase64 = base64;
    124 
    125         Log.d(Utils.hs2LogTag(getClass()),
    126                 String.format("%s MIME container, boundary '%s', type '%s', encoding %s",
    127                 multiPart ? "multipart" : "plain", boundary, mContentType, encoding));
    128 
    129         AtomicBoolean eof = new AtomicBoolean();
    130         mText = recode(getBody(in, boundary, quoted, eof), charset);
    131         mLast = eof.get();
    132     }
    133 
    134     public List<MIMEContainer> getMimeContainers() {
    135         return mMimeContainers;
    136     }
    137 
    138     public String getText() {
    139         return mText;
    140     }
    141 
    142     public boolean isMixed() {
    143         return mMixed;
    144     }
    145 
    146     public boolean isBase64() {
    147         return mBase64;
    148     }
    149 
    150     public String getContentType() {
    151         return mContentType;
    152     }
    153 
    154     private boolean isLast() {
    155         return mLast;
    156     }
    157 
    158     private void toString(StringBuilder sb, int nesting) {
    159         char[] indent = new char[nesting*4];
    160         Arrays.fill(indent, ' ');
    161         if (mBase64) {
    162             sb.append("base64, type ").append(mContentType).append('\n');
    163         }
    164         else if (mMimeContainers != null) {
    165             sb.append(indent).append("multipart/").append((mMixed ? "mixed" : "other" )).append('\n');
    166         }
    167         else {
    168             sb.append(indent).append(
    169                     String.format("%s, type %s",
    170                             mCharset,
    171                             mContentType)
    172             ).append('\n');
    173         }
    174 
    175         if (mMimeContainers != null) {
    176             for (MIMEContainer mimeContainer : mMimeContainers) {
    177                 mimeContainer.toString(sb, nesting + 1);
    178             }
    179         }
    180         sb.append(indent).append("Text: ");
    181         if (mText.length() < 100000) {
    182             sb.append("'").append(mText).append("'\n");
    183         }
    184         else {
    185             sb.append(mText.length()).append(" chars\n");
    186         }
    187     }
    188 
    189     @Override
    190     public String toString() {
    191         StringBuilder sb = new StringBuilder();
    192         toString(sb, 0);
    193         return sb.toString();
    194     }
    195 
    196     private static Map<String,List<String>> parseHeader(LineNumberReader in) throws IOException {
    197 
    198         StringBuilder value = null;
    199         String header = null;
    200 
    201         Map<String,List<String>> headers = new HashMap<>();
    202 
    203         for (;;) {
    204             String line = in.readLine();
    205             if ( line == null ) {
    206                 throw new IOException("Missing body @ " + in.getLineNumber());
    207             }
    208             else if (line.length() == 0) {
    209                 break;
    210             }
    211 
    212             if (line.charAt(0) <= ' ') {
    213                 if (value == null) {
    214                     throw new IOException("Illegal blank prefix in header line '" + line + "' @ " + in.getLineNumber());
    215                 }
    216                 value.append(' ').append(line.trim());
    217                 continue;
    218             }
    219 
    220             int nameEnd = line.indexOf(':');
    221             if (nameEnd < 0) {
    222                 throw new IOException("Bad header line: '" + line + "' @ " + in.getLineNumber());
    223             }
    224 
    225             if (header != null) {
    226                 String[] values = value.toString().split(";");
    227                 List<String> valueList = new ArrayList<>(values.length);
    228                 for (String segment : values) {
    229                     valueList.add(segment.trim());
    230                 }
    231                 headers.put(header, valueList);
    232                 //System.out.println("Header '" + header + "' = " + valueList);
    233             }
    234 
    235             header = line.substring(0, nameEnd);
    236             value = new StringBuilder();
    237             value.append(line.substring(nameEnd+1).trim());
    238         }
    239 
    240         if (header != null) {
    241             String[] values = value.toString().split(";");
    242             List<String> valueList = new ArrayList<>(values.length);
    243             for (String segment : values) {
    244                 valueList.add(segment.trim());
    245             }
    246             headers.put(header, valueList);
    247             //System.out.println("Header '" + header + "' = " + valueList);
    248         }
    249 
    250         return headers;
    251     }
    252 
    253     private static String getBody(LineNumberReader in, String boundary, boolean quoted, AtomicBoolean eof)
    254             throws IOException {
    255 
    256         StringBuilder text = new StringBuilder();
    257         for (;;) {
    258             String line = in.readLine();
    259             if (line == null) {
    260                 if (boundary != null) {
    261                     throw new IOException("Unexpected EOF file in body @ " + in.getLineNumber());
    262                 }
    263                 else {
    264                     return text.toString();
    265                 }
    266             }
    267             Boolean end = boundaryCheck(line, boundary);
    268             if (end != null) {
    269                 eof.set(end);
    270                 //System.out.println("Boundary " + boundary + ": " + end);
    271                 return text.toString();
    272             }
    273 
    274             if (quoted) {
    275                 if (line.endsWith("=")) {
    276                     text.append(unescape(line.substring(line.length() - 1), in.getLineNumber()));
    277                 }
    278                 else {
    279                     text.append(unescape(line, in.getLineNumber()));
    280                 }
    281             }
    282             else {
    283                 text.append(line);
    284             }
    285         }
    286     }
    287 
    288     private static String recode(String s, Charset charset) {
    289         if (charset.equals(StandardCharsets.ISO_8859_1) || charset.equals(StandardCharsets.US_ASCII)) {
    290             return s;
    291         }
    292 
    293         byte[] octets = s.getBytes(StandardCharsets.ISO_8859_1);
    294         return new String(octets, charset);
    295     }
    296 
    297     private static Boolean boundaryCheck(String line, String boundary) {
    298         if (line.startsWith("--") && line.regionMatches(2, boundary, 0, boundary.length())) {
    299             if (line.length() == boundary.length() + 2) {
    300                 return Boolean.FALSE;
    301             }
    302             else if (line.length() == boundary.length() + 4 && line.endsWith("--") ) {
    303                 return Boolean.TRUE;
    304             }
    305         }
    306         return null;
    307     }
    308 
    309     private static String unescape(String text, int line) throws IOException {
    310         StringBuilder sb = new StringBuilder();
    311         for (int n = 0; n < text.length(); n++) {
    312             char ch = text.charAt(n);
    313             if (ch > 127) {
    314                 throw new IOException("Bad codepoint " + (int)ch + " in quoted printable @ " + line);
    315             }
    316             if (ch == '=' && n < text.length() - 2) {
    317                 int h1 = fromStrictHex(text.charAt(n+1));
    318                 int h2 = fromStrictHex(text.charAt(n+2));
    319                 if (h1 >= 0 && h2 >= 0) {
    320                     sb.append((char)((h1 << 4) | h2));
    321                     n += 2;
    322                 }
    323                 else {
    324                     sb.append(ch);
    325                 }
    326             }
    327             else {
    328                 sb.append(ch);
    329             }
    330         }
    331         return sb.toString();
    332     }
    333 
    334     private static int fromStrictHex(char ch) {
    335         if (ch >= '0' && ch <= '9') {
    336             return ch - '0';
    337         }
    338         else if (ch >= 'A' && ch <= 'F') {
    339             return ch - 'A' + 10;
    340         }
    341         else {
    342             return -1;
    343         }
    344     }
    345 }
    346