/** * This method encodes name/value pairs and files into a byte array using the multipart/form-data * encoding. The boundary is returned as part of <var>ct_hdr</var>. <br> * Example: * * <PRE> * NVPair[] opts = { new NVPair("option", "doit") }; * NVPair[] file = { new NVPair("comment", "comment.txt") }; * NVPair[] hdrs = new NVPair[1]; * byte[] data = Codecs.mpFormDataEncode(opts, file, hdrs); * con.Post("/cgi-bin/handle-it", data, hdrs); * </PRE> * * <VAR>data</VAR> will look something like the following: * * <PRE> * -----------------------------114975832116442893661388290519 * Content-Disposition: form-data; name="option" * * doit * -----------------------------114975832116442893661388290519 * Content-Disposition: form-data; name="comment"; filename="comment.txt" * Content-Type: text/plain * * Gnus and Gnats are not Gnomes. * -----------------------------114975832116442893661388290519-- * </PRE> * * where the "Gnus and Gnats ..." is the contents of the file <VAR>comment.txt</VAR> in the * current directory. * * <p>If no elements are found in the parameters then a zero-length byte[] is returned and the * content-type is set to <var>application/octet-string</var> (because a multipart must always * have at least one part. * * <p>For files an attempt is made to discover the content-type, and if found a Content-Type * header will be added to that part. The content type is retrieved using * java.net.URLConnection.guessContentTypeFromName() - see java.net.URLConnection.setFileNameMap() * for how to modify that map. Note that under JDK 1.1 by default the map seems to be empty. If * you experience troubles getting the server to accept the data then make sure the fileNameMap is * returning a content-type for each file (this may mean you'll have to set your own). * * @param opts the simple form-data to encode (may be null); for each NVPair the name refers to * the 'name' attribute to be used in the header of the part, and the value is contents of the * part. null elements in the array are ingored. * @param files the files to encode (may be null); for each NVPair the name refers to the 'name' * attribute to be used in the header of the part, and the value is the actual filename (the * file will be read and it's contents put in the body of that part). null elements in the * array are ingored. * @param ct_hdr this returns a new NVPair in the 0'th element which contains name = * "Content-Type", value = "multipart/form-data; boundary=..." (the reason this parameter is * an array is because a) that's the only way to simulate pass-by-reference and b) you need an * array for the headers parameter to the Post() or Put() anyway). The exception to this is * that if no opts or files are given the type is set to "application/octet-stream" instead. * @param mangler the filename mangler, or null if no mangling is to be done. This allows you to * change the name used in the <var>filename</var> attribute of the Content-Disposition * header. Note: the mangler will be invoked twice for each filename. * @return an encoded byte array containing all the opts and files. * @exception IOException If any file operation fails. */ public static final byte[] mpFormDataEncode( NVPair[] opts, NVPair[] files, NVPair[] ct_hdr, FilenameMangler mangler) throws IOException { byte[] boundary = Boundary.getBytes("8859_1"), cont_disp = ContDisp.getBytes("8859_1"), cont_type = ContType.getBytes("8859_1"), filename = FileName.getBytes("8859_1"); int len = 0, hdr_len = boundary.length + cont_disp.length + 1 + 2 + 2; // \r\n -- bnd \r\n C-D: ..; n=".." \r\n \r\n if (opts == null) opts = dummy; if (files == null) files = dummy; // Calculate the length of the data for (int idx = 0; idx < opts.length; idx++) { if (opts[idx] == null) continue; len += hdr_len + opts[idx].getName().length() + opts[idx].getValue().length(); } for (int idx = 0; idx < files.length; idx++) { if (files[idx] == null) continue; File file = new File(files[idx].getValue()); String fname = file.getName(); if (mangler != null) fname = mangler.mangleFilename(fname, files[idx].getName()); if (fname != null) { len += hdr_len + files[idx].getName().length() + filename.length; len += fname.length() + file.length(); String ct = CT.getContentType(file.getName()); if (ct != null) len += cont_type.length + ct.length(); } } if (len == 0) { ct_hdr[0] = new NVPair("Content-Type", "application/octet-stream"); return new byte[0]; } len -= 2; // first CR LF is not written len += boundary.length + 2 + 2; // \r\n -- bnd -- \r\n // Now fill array byte[] res = new byte[len]; int pos = 0; NewBound: for (int new_c = 0x30303030; new_c != 0x7A7A7A7A; new_c++) { pos = 0; // modify boundary in hopes that it will be unique while (!BoundChar.get(new_c & 0xff)) new_c += 0x00000001; while (!BoundChar.get(new_c >> 8 & 0xff)) new_c += 0x00000100; while (!BoundChar.get(new_c >> 16 & 0xff)) new_c += 0x00010000; while (!BoundChar.get(new_c >> 24 & 0xff)) new_c += 0x01000000; boundary[40] = (byte) (new_c & 0xff); boundary[42] = (byte) (new_c >> 8 & 0xff); boundary[44] = (byte) (new_c >> 16 & 0xff); boundary[46] = (byte) (new_c >> 24 & 0xff); int off = 2; int[] bnd_cmp = Util.compile_search(boundary); for (int idx = 0; idx < opts.length; idx++) { if (opts[idx] == null) continue; System.arraycopy(boundary, off, res, pos, boundary.length - off); pos += boundary.length - off; off = 0; int start = pos; System.arraycopy(cont_disp, 0, res, pos, cont_disp.length); pos += cont_disp.length; int nlen = opts[idx].getName().length(); System.arraycopy(opts[idx].getName().getBytes("8859_1"), 0, res, pos, nlen); pos += nlen; res[pos++] = (byte) '"'; res[pos++] = (byte) '\r'; res[pos++] = (byte) '\n'; res[pos++] = (byte) '\r'; res[pos++] = (byte) '\n'; int vlen = opts[idx].getValue().length(); System.arraycopy(opts[idx].getValue().getBytes("8859_1"), 0, res, pos, vlen); pos += vlen; if ((pos - start) >= boundary.length && Util.findStr(boundary, bnd_cmp, res, start, pos) != -1) continue NewBound; } for (int idx = 0; idx < files.length; idx++) { if (files[idx] == null) continue; File file = new File(files[idx].getValue()); String fname = file.getName(); if (mangler != null) fname = mangler.mangleFilename(fname, files[idx].getName()); if (fname == null) continue; System.arraycopy(boundary, off, res, pos, boundary.length - off); pos += boundary.length - off; off = 0; int start = pos; System.arraycopy(cont_disp, 0, res, pos, cont_disp.length); pos += cont_disp.length; int nlen = files[idx].getName().length(); System.arraycopy(files[idx].getName().getBytes("8859_1"), 0, res, pos, nlen); pos += nlen; System.arraycopy(filename, 0, res, pos, filename.length); pos += filename.length; nlen = fname.length(); System.arraycopy(fname.getBytes("8859_1"), 0, res, pos, nlen); pos += nlen; res[pos++] = (byte) '"'; String ct = CT.getContentType(file.getName()); if (ct != null) { System.arraycopy(cont_type, 0, res, pos, cont_type.length); pos += cont_type.length; System.arraycopy(ct.getBytes("8859_1"), 0, res, pos, ct.length()); pos += ct.length(); } res[pos++] = (byte) '\r'; res[pos++] = (byte) '\n'; res[pos++] = (byte) '\r'; res[pos++] = (byte) '\n'; nlen = (int) file.length(); FileInputStream fin = new FileInputStream(file); while (nlen > 0) { int got = fin.read(res, pos, nlen); nlen -= got; pos += got; } fin.close(); if ((pos - start) >= boundary.length && Util.findStr(boundary, bnd_cmp, res, start, pos) != -1) continue NewBound; } break NewBound; } System.arraycopy(boundary, 0, res, pos, boundary.length); pos += boundary.length; res[pos++] = (byte) '-'; res[pos++] = (byte) '-'; res[pos++] = (byte) '\r'; res[pos++] = (byte) '\n'; if (pos != len) throw new Error("Calculated " + len + " bytes but wrote " + pos + " bytes!"); /* the boundary parameter should be quoted (rfc-2046, section 5.1.1) * but too many script authors are not capable of reading specs... * So, I give up and don't quote it. */ ct_hdr[0] = new NVPair( "Content-Type", "multipart/form-data; boundary=" + new String(boundary, 4, boundary.length - 4, "8859_1")); return res; }
/** * This method decodes a multipart/form-data encoded string. The boundary is parsed from the * <var>cont_type</var> parameter, which must be of the form 'multipart/form-data; boundary=...'. * Any encoded files are created in the directory specified by <var>dir</var> using the encoded * filename. * * <p><em>Note:</em> Does not handle nested encodings (yet). * * <p>Examples: If you're receiving a multipart/form-data encoded response from a server you could * use something like: * * <PRE> * NVPair[] opts = Codecs.mpFormDataDecode(resp.getData(), * resp.getHeader("Content-type"), "."); * </PRE> * * If you're using this in a Servlet to decode the body of a request from a client you could use * something like: * * <PRE> * byte[] body = new byte[req.getContentLength()]; * new DataInputStream(req.getInputStream()).readFully(body); * NVPair[] opts = Codecs.mpFormDataDecode(body, req.getContentType(), * "."); * </PRE> * * (where 'req' is the HttpServletRequest). * * <p>Assuming the data received looked something like: * * <PRE> * -----------------------------114975832116442893661388290519 * Content-Disposition: form-data; name="option" * * doit * -----------------------------114975832116442893661388290519 * Content-Disposition: form-data; name="comment"; filename="comment.txt" * * Gnus and Gnats are not Gnomes. * -----------------------------114975832116442893661388290519-- * </PRE> * * you would get one file called <VAR>comment.txt</VAR> in the current directory, and opts would * contain two elements: {"option", "doit"} and {"comment", "comment.txt"} * * @param data the form-data to decode. * @param cont_type the content type header (must contain the boundary string). * @param dir the directory to create the files in. * @param mangler the filename mangler, or null if no mangling is to be done. This is invoked just * before each file is created and written, thereby allowing you to control the names of the * files. * @return an array of name/value pairs, one for each part; the name is the 'name' attribute given * in the Content-Disposition header; the value is either the name of the file if a filename * attribute was found, or the contents of the part. * @exception IOException If any file operation fails. * @exception ParseException If an error during parsing occurs. */ public static final NVPair[] mpFormDataDecode( byte[] data, String cont_type, String dir, FilenameMangler mangler) throws IOException, ParseException { // Find and extract boundary string String bndstr = Util.getParameter("boundary", cont_type); if (bndstr == null) throw new ParseException("'boundary' parameter not found in Content-type: " + cont_type); byte[] srtbndry = ("--" + bndstr + "\r\n").getBytes("8859_1"), boundary = ("\r\n--" + bndstr + "\r\n").getBytes("8859_1"), endbndry = ("\r\n--" + bndstr + "--").getBytes("8859_1"); // setup search routines int[] bs = Util.compile_search(srtbndry), bc = Util.compile_search(boundary), be = Util.compile_search(endbndry); // let's start parsing the actual data int start = Util.findStr(srtbndry, bs, data, 0, data.length); if (start == -1) // didn't even find the start throw new ParseException("Starting boundary not found: " + new String(srtbndry, "8859_1")); start += srtbndry.length; NVPair[] res = new NVPair[10]; boolean done = false; int idx; for (idx = 0; !done; idx++) { // find end of this part int end = Util.findStr(boundary, bc, data, start, data.length); if (end == -1) // must be the last part { end = Util.findStr(endbndry, be, data, start, data.length); if (end == -1) throw new ParseException("Ending boundary not found: " + new String(endbndry, "8859_1")); done = true; } // parse header(s) String hdr, name = null, value, filename = null, cont_disp = null; while (true) { int next = findEOL(data, start) + 2; if (next - 2 <= start) break; // empty line -> end of headers hdr = new String(data, start, next - 2 - start, "8859_1"); start = next; // handle line continuation byte ch; while (next < data.length - 1 && ((ch = data[next]) == ' ' || ch == '\t')) { next = findEOL(data, start) + 2; hdr += new String(data, start, next - 2 - start, "8859_1"); start = next; } if (!hdr.regionMatches(true, 0, "Content-Disposition", 0, 19)) continue; Vector pcd = Util.parseHeader(hdr.substring(hdr.indexOf(':') + 1)); HttpHeaderElement elem = Util.getElement(pcd, "form-data"); if (elem == null) throw new ParseException("Expected 'Content-Disposition: form-data' in line: " + hdr); NVPair[] params = elem.getParams(); name = filename = null; for (int pidx = 0; pidx < params.length; pidx++) { if (params[pidx].getName().equalsIgnoreCase("name")) name = params[pidx].getValue(); if (params[pidx].getName().equalsIgnoreCase("filename")) filename = params[pidx].getValue(); } if (name == null) throw new ParseException("'name' parameter not found in header: " + hdr); cont_disp = hdr; } start += 2; if (start > end) throw new ParseException("End of header not found at offset " + end); if (cont_disp == null) throw new ParseException("Missing 'Content-Disposition' header at offset " + start); // handle data for this part if (filename != null) // It's a file { if (mangler != null) filename = mangler.mangleFilename(filename, name); if (filename != null && filename.length() > 0) { File file = new File(dir, filename); FileOutputStream out = new FileOutputStream(file); out.write(data, start, end - start); out.close(); } value = filename; } else // It's simple data { value = new String(data, start, end - start, "8859_1"); } if (idx >= res.length) res = Util.resizeArray(res, idx + 10); res[idx] = new NVPair(name, value); start = end + boundary.length; } return Util.resizeArray(res, idx); }