All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nulabinc.backlog4j.internal.http.MimeHelper Maven / Gradle / Ivy

The newest version!
package com.nulabinc.backlog4j.internal.http;

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public final class MimeHelper {

    private static final String DISPOSITION_FILENAME = "filename";

    private static final String MIME_SPECIALS = "()<>@,;:\\\"/[]?=" + "\t ";

    private static final String WHITE = " \t\n\r";

    private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();

    private static final byte[] HEX_DECODE = new byte[0x80];

    static {
        for (int i = 0; i < HEX_DIGITS.length; i++) {
            HEX_DECODE[HEX_DIGITS[i]] = (byte) i;
            HEX_DECODE[Character.toLowerCase(HEX_DIGITS[i])] = (byte) i;
        }
    }

    private MimeHelper() {
    }

    /**
     * Decodes a filename from the Content-Disposition header value according to
     * RFC 2183 and RFC 2231.
     * 

* See RFC 2231 for * details. * * @param value the header value to decode * @return the filename */ public static String decodeContentDispositionFilename(String value) { Map params = new HashMap<>(); decodeContentDisposition(value, params); return params.get(DISPOSITION_FILENAME); } /** * Decodes the Content-Disposition header value according to RFC 2183 and * RFC 2231. *

* Does not deal with continuation lines. *

* See RFC 2231 for * details. * * @param value the header value to decode * @param params the map of parameters to fill * @return the disposition */ public static String decodeContentDisposition(String value, Map params) { try { HeaderTokenizer tokenizer = new HeaderTokenizer(value); // get the first token, which must be an ATOM Token token = tokenizer.next(); if (token.getType() != Token.ATOM) { return null; } String disposition = token.getValue(); // value ignored in this method // the remainder is the parameters String remainder = tokenizer.getRemainder(); if (remainder != null) { getParameters(remainder, params); } return disposition; } catch (ParseException e) { return null; } } protected static class ParseException extends Exception { private static final long serialVersionUID = 1L; public ParseException() { super(); } public ParseException(String message) { super(message); } } /* * From geronimo-javamail_1.4_spec-1.7.1. Token */ protected static class Token { // Constant values from J2SE 1.4 API Docs (Constant values) public static final int ATOM = -1; public static final int COMMENT = -3; public static final int EOF = -4; public static final int QUOTEDSTRING = -2; private final int type; private final String value; public Token(int type, String value) { this.type = type; this.value = value; } public int getType() { return type; } public String getValue() { return value; } } /* * Tweaked from geronimo-javamail_1.4_spec-1.7.1. HeaderTokenizer */ protected static class HeaderTokenizer { private static final Token EOF = new Token(Token.EOF, null); private final String header; private final String delimiters; private final boolean skipComments; private int pos; public HeaderTokenizer(String header) { this(header, MIME_SPECIALS, true); } protected HeaderTokenizer(String header, String delimiters, boolean skipComments) { this.header = header; this.delimiters = delimiters; this.skipComments = skipComments; } public String getRemainder() { return header.substring(pos); } public Token next() throws ParseException { return readToken(); } /** * Read an ATOM token from the parsed header. * * @return A token containing the value of the atom token. */ private Token readAtomicToken() { // skip to next delimiter int start = pos; while (++pos < header.length()) { // break on the first non-atom character. char ch = header.charAt(pos); if (delimiters.indexOf(header.charAt(pos)) != -1 || ch < 32 || ch >= 127) { break; } } return new Token(Token.ATOM, header.substring(start, pos)); } /** * Read the next token from the header. * * @return The next token from the header. White space is skipped, and * comment tokens are also skipped if indicated. */ private Token readToken() throws ParseException { if (pos >= header.length()) { return EOF; } else { char c = header.charAt(pos); // comment token...read and skip over this if (c == '(') { Token comment = readComment(); if (skipComments) { return readToken(); } else { return comment; } // quoted literal } else if (c == '\"') { return readQuotedString(); // white space, eat this and find a real token. } else if (WHITE.indexOf(c) != -1) { eatWhiteSpace(); return readToken(); // either a CTL or special. These characters have a // self-defining token type. } else if (c < 32 || c >= 127 || delimiters.indexOf(c) != -1) { pos++; return new Token(c, String.valueOf(c)); } else { // start of an atom, parse it off. return readAtomicToken(); } } } /** * Extract a substring from the header string and apply any * escaping/folding rules to the string. * * @param start The starting offset in the header. * @param end The header end offset + 1. * @return The processed string value. */ private String getEscapedValue(int start, int end) throws ParseException { StringBuilder value = new StringBuilder(); for (int i = start; i < end; i++) { char ch = header.charAt(i); // is this an escape character? if (ch == '\\') { i++; if (i == end) { throw new ParseException("Invalid escape character"); } value.append(header.charAt(i)); } else if (ch == '\r') { // line breaks are ignored, except for naked '\n' // characters, which are consider parts of linear // whitespace. // see if this is a CRLF sequence, and skip the second if it // is. if (i < end - 1 && header.charAt(i + 1) == '\n') { i++; } } else { // just append the ch value. value.append(ch); } } return value.toString(); } /** * Read a comment from the header, applying nesting and escape rules to * the content. * * @return A comment token with the token value. */ private Token readComment() throws ParseException { int start = pos + 1; int nesting = 1; boolean requiresEscaping = false; // skip to end of comment/string while (++pos < header.length()) { char ch = header.charAt(pos); if (ch == ')') { nesting--; if (nesting == 0) { break; } } else if (ch == '(') { nesting++; } else if (ch == '\\') { pos++; requiresEscaping = true; } else if (ch == '\r') { // we need to process line breaks also requiresEscaping = true; } } if (nesting != 0) { throw new ParseException("Unbalanced comments"); } String value; if (requiresEscaping) { value = getEscapedValue(start, pos); } else { value = header.substring(start, pos++); } return new Token(Token.COMMENT, value); } /** * Parse out a quoted string from the header, applying escaping rules to * the value. * * @return The QUOTEDSTRING token with the value. * @throws ParseException */ private Token readQuotedString() throws ParseException { int start = pos + 1; boolean requiresEscaping = false; // skip to end of comment/string while (++pos < header.length()) { char ch = header.charAt(pos); if (ch == '"') { String value; if (requiresEscaping) { value = getEscapedValue(start, pos++); } else { value = header.substring(start, pos++); } return new Token(Token.QUOTEDSTRING, value); } else if (ch == '\\') { pos++; requiresEscaping = true; } else if (ch == '\r') { // we need to process line breaks also requiresEscaping = true; } } throw new ParseException("Missing '\"'"); } /** * Skip white space in the token string. */ private void eatWhiteSpace() { // skip to end of whitespace while (++pos < header.length() && WHITE.indexOf(header.charAt(pos)) != -1) { // just read } } } /* * Tweaked from geronimo-javamail_1.4_spec-1.7.1. ParameterList */ private static Map getParameters(String list, Map params) throws ParseException { HeaderTokenizer tokenizer = new HeaderTokenizer(list); while (true) { Token token = tokenizer.next(); switch (token.getType()) { case Token.EOF: // the EOF token terminates parsing. return params; case ';': // each new parameter is separated by a semicolon, including // the first, which separates // the parameters from the main part of the header. // the next token needs to be a parameter name token = tokenizer.next(); // allow a trailing semicolon on the parameters. if (token.getType() == Token.EOF) { return params; } if (token.getType() != Token.ATOM) { throw new ParseException("Invalid parameter name: " + token.getValue()); } // get the parameter name as a lower case version for better // mapping. String name = token.getValue().toLowerCase(Locale.ENGLISH); token = tokenizer.next(); // parameters are name=value, so we must have the "=" here. if (token.getType() != '=') { throw new ParseException("Missing '='"); } // now the value, which may be an atom or a literal token = tokenizer.next(); if (token.getType() != Token.ATOM && token.getType() != Token.QUOTEDSTRING) { throw new ParseException("Invalid parameter value: " + token.getValue()); } String value = token.getValue(); // we might have to do some additional decoding. A name that // ends with "*" is marked as being encoded, so if requested, we // decode the value. if (name.endsWith("*")) { name = name.substring(0, name.length() - 1); value = decodeRFC2231value(value); } params.put(name, value); break; default: throw new ParseException("Missing ';'"); } } } private static String decodeRFC2231value(String value) { int q1 = value.indexOf('\''); if (q1 == -1) { // missing charset return value; } String mimeCharset = value.substring(0, q1); int q2 = value.indexOf('\'', q1 + 1); if (q2 == -1) { // missing language return value; } byte[] bytes = fromHex(value.substring(q2 + 1)); try { return new String(bytes, Charset.forName(mimeCharset)); } catch (UnsupportedCharsetException e) { // incorrect encoding return value; } } private static byte[] fromHex(String data) { ByteArrayOutputStream out = new ByteArrayOutputStream(data.length()); for (int i = 0; i < data.length(); ) { char c = data.charAt(i++); if (c == '%') { if (i > data.length() - 2) { break; // unterminated sequence } byte b1 = HEX_DECODE[data.charAt(i++) & 0x7f]; byte b2 = HEX_DECODE[data.charAt(i++) & 0x7f]; out.write((b1 << 4) | b2); } else { out.write((byte) c); } } return out.toByteArray(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy