--- /dev/null
+package jrw;
+
+import java.util.*;
+import java.util.function.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+public class FormData extends HashMap<String, String> {
+ public static final int MAX_LENGTH = 1 << 20;
+
+ private static int htoi(byte hex) {
+ if((hex >= '0') && (hex <= '9'))
+ return(hex - '0');
+ if((hex >= 'A') && (hex <= 'F'))
+ return(hex - 'A' + 10);
+ if((hex >= 'a') && (hex <= 'f'))
+ return(hex - 'a' + 10);
+ return(0);
+ }
+
+ private static ByteBuffer unquoteb(ByteBuffer part) {
+ ByteBuffer ret = ByteBuffer.allocate(part.remaining());
+ while(part.remaining() > 0) {
+ int b = part.get() & 0xff;
+ if((b == '%') && (part.remaining() >= 2)) {
+ int n1 = htoi(part.get()), n2 = htoi(part.get());
+ ret.put((byte)((n1 << 4) | n2));
+ } else {
+ ret.put((byte)b);
+ }
+ }
+ ret.flip();
+ return(ret);
+ }
+
+ private static String unquote(ByteBuffer part) {
+ ByteBuffer dec = unquoteb(part);
+ try {
+ return(Http.UTF8.newDecoder().decode(dec.duplicate()).toString());
+ } catch(java.nio.charset.CharacterCodingException e) {
+ return(Http.LATIN1.decode(dec).toString());
+ }
+ }
+
+ private static String unquote(String part) {
+ if(part.indexOf('%') < 0)
+ return(part);
+ return(unquote(Http.UTF8.encode(CharBuffer.wrap(part))));
+ }
+
+ public static void parse(Map<? super String, ? super String> buf, ByteBuffer data) {
+ int p = data.position(), p2, p3;
+ while(p < data.limit()) {
+ for(p2 = p; (p2 < data.limit()) && (data.get(p2) != '&'); p2++);
+ for(p3 = p; (p3 < p2) && (data.get(p3) != '='); p3++);
+ if(p3 < p2) {
+ buf.put(unquote((ByteBuffer)data.duplicate().position(p).limit(p3)),
+ unquote((ByteBuffer)data.duplicate().position(p3 + 1).limit(p2)));
+ }
+ p = p2 + 1;
+ }
+ }
+
+ public static void parse(Map<? super String, ? super String> buf, String data) {
+ int p = 0;
+ while(true) {
+ int p2 = data.indexOf('&', p);
+ String part = (p2 < 0) ? data.substring(p) : data.substring(p, p2);
+ int p3 = part.indexOf('=');
+ if(p3 >= 0)
+ buf.put(unquote(part.substring(0, p3)), unquote(part.substring(p3 + 1)));
+ if(p2 < 0)
+ break;
+ p = p2 + 1;
+ }
+ }
+
+ public static FormData read(Request req) {
+ FormData ret = new FormData();
+ String query = (String)req.env.get("QUERY_STRING");
+ if(query != null)
+ parse(ret, query);
+ if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
+ int max = MAX_LENGTH;
+ String clen = req.ihead("Content-Length", null);
+ if(clen != null) {
+ try {
+ max = Math.min(max, Integer.parseInt(clen));
+ } catch(NumberFormatException e) {
+ }
+ }
+ ReadableByteChannel in = (ReadableByteChannel)req.env.get("jagi.input");
+ if(in instanceof SelectableChannel) {
+ try {
+ ((SelectableChannel)in).configureBlocking(true);
+ } catch(IOException e) {
+ }
+ }
+ ByteBuffer buf = ByteBuffer.allocate(65536);
+ while(buf.position() < max) {
+ if(buf.remaining() == 0) {
+ ByteBuffer n = ByteBuffer.allocate(Math.min(buf.capacity() * 2, max));
+ buf.flip();
+ n.put(buf);
+ buf = n;
+ }
+ try {
+ int rv = in.read(buf);
+ if(rv <= 0)
+ break;
+ } catch(IOException e) {
+ break;
+ }
+ }
+ buf.flip();
+ parse(ret, buf);
+ }
+ return(ret);
+ }
+
+ public static FormData get(Request req) {
+ FormData ret = (FormData)req.env.get(FormData.class);
+ if(ret == null)
+ req.env.put(FormData.class, ret = read(req));
+ return(ret);
+ }
+
+ static class Collector extends ByteArrayOutputStream {
+ final FormData form = new FormData();
+ final Request req;
+
+ Collector(Request req) {
+ this.req = req;
+ req.env.put(FormData.class, this.form);
+ String query = (String)req.env.get("QUERY_STRING");
+ if(query != null)
+ parse(form, query);
+ }
+
+ public void write(int b) {
+ if(count < MAX_LENGTH)
+ super.write(b);
+ }
+
+ public void write(byte[] buf, int off, int len) {
+ len = Math.min(len, MAX_LENGTH - count);
+ if(len > 0)
+ super.write(buf, off, len);
+ }
+
+ public void close() {
+ }
+ }
+
+ public static Map<Object, Object> feed(Request req, Handler next) {
+ Map<Object, Object> resp = new HashMap<>();
+ if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
+ resp.put("jagi.status", "feed-input");
+ resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
+ resp.put("jagi.input-sink", new Collector(req));
+ } else {
+ read(req);
+ resp.put("jagi.status", "chain");
+ resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
+ }
+ return(resp);
+ }
+}
--- /dev/null
+package jrw;
+
+import java.util.*;
+
+public class Request {
+ public final Map<Object, Object> env;
+ public final Map<Object, Object> resp = new HashMap<>();
+
+ public Request(Map<Object, Object> env) {
+ this.env = env;
+ }
+
+ public String ihead(String name, String def) {
+ StringBuilder buf = new StringBuilder();
+ buf.append("HTTP_");
+ for(int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if(c == '-')
+ buf.append('_');
+ else if((c >= 'a') && (c <= 'z'))
+ buf.append((char)(c + ('A' - 'a')));
+ else
+ buf.append(c);
+ }
+ Object ret = env.get(buf.toString());
+ if(ret instanceof String)
+ return((String)ret);
+ return(def);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void ohead(String name, Object val, boolean repl) {
+ name = "http." + name;
+ if(repl) {
+ resp.put(name, val);
+ } else {
+ Object cur = resp.get(name);
+ if(cur == null)
+ resp.put(name, val);
+ else if(cur instanceof Collection)
+ ((Collection)cur).add(val);
+ else
+ resp.put(name, new ArrayList<Object>(Arrays.asList(cur, val)));
+ }
+ }
+
+ public Map<Object, Object> response() {
+ return(resp);
+ }
+}