Initial commit.
[jrw.git] / src / jrw / FormData.java
1 package jrw;
2
3 import java.util.*;
4 import java.util.function.*;
5 import java.io.*;
6 import java.nio.*;
7 import java.nio.channels.*;
8
9 public class FormData extends HashMap<String, String> {
10     public static final int MAX_LENGTH = 1 << 20;
11
12     private static int htoi(byte hex) {
13         if((hex >= '0') && (hex <= '9'))
14             return(hex - '0');
15         if((hex >= 'A') && (hex <= 'F'))
16             return(hex - 'A' + 10);
17         if((hex >= 'a') && (hex <= 'f'))
18             return(hex - 'a' + 10);
19         return(0);
20     }
21
22     private static ByteBuffer unquoteb(ByteBuffer part) {
23         ByteBuffer ret = ByteBuffer.allocate(part.remaining());
24         while(part.remaining() > 0) {
25             int b = part.get() & 0xff;
26             if((b == '%') && (part.remaining() >= 2)) {
27                 int n1 = htoi(part.get()), n2 = htoi(part.get());
28                 ret.put((byte)((n1 << 4) | n2));
29             } else {
30                 ret.put((byte)b);
31             }
32         }
33         ret.flip();
34         return(ret);
35     }
36
37     private static String unquote(ByteBuffer part) {
38         ByteBuffer dec = unquoteb(part);
39         try {
40             return(Http.UTF8.newDecoder().decode(dec.duplicate()).toString());
41         } catch(java.nio.charset.CharacterCodingException e) {
42             return(Http.LATIN1.decode(dec).toString());
43         }
44     }
45
46     private static String unquote(String part) {
47         if(part.indexOf('%') < 0)
48             return(part);
49         return(unquote(Http.UTF8.encode(CharBuffer.wrap(part))));
50     }
51
52     public static void parse(Map<? super String, ? super String> buf, ByteBuffer data) {
53         int p = data.position(), p2, p3;
54         while(p < data.limit()) {
55             for(p2 = p; (p2 < data.limit()) && (data.get(p2) != '&'); p2++);
56             for(p3 = p; (p3 < p2) && (data.get(p3) != '='); p3++);
57             if(p3 < p2) {
58                 buf.put(unquote((ByteBuffer)data.duplicate().position(p).limit(p3)),
59                         unquote((ByteBuffer)data.duplicate().position(p3 + 1).limit(p2)));
60             }
61             p = p2 + 1;
62         }
63     }
64
65     public static void parse(Map<? super String, ? super String> buf, String data) {
66         int p = 0;
67         while(true) {
68             int p2 = data.indexOf('&', p);
69             String part = (p2 < 0) ? data.substring(p) : data.substring(p, p2);
70             int p3 = part.indexOf('=');
71             if(p3 >= 0)
72                 buf.put(unquote(part.substring(0, p3)), unquote(part.substring(p3 + 1)));
73             if(p2 < 0)
74                 break;
75             p = p2 + 1;
76         }
77     }
78
79     public static FormData read(Request req) {
80         FormData ret = new FormData();
81         String query = (String)req.env.get("QUERY_STRING");
82         if(query != null)
83             parse(ret, query);
84         if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
85             int max = MAX_LENGTH;
86             String clen = req.ihead("Content-Length", null);
87             if(clen != null) {
88                 try {
89                     max = Math.min(max, Integer.parseInt(clen));
90                 } catch(NumberFormatException e) {
91                 }
92             }
93             ReadableByteChannel in = (ReadableByteChannel)req.env.get("jagi.input");
94             if(in instanceof SelectableChannel) {
95                 try {
96                     ((SelectableChannel)in).configureBlocking(true);
97                 } catch(IOException e) {
98                 }
99             }
100             ByteBuffer buf = ByteBuffer.allocate(65536);
101             while(buf.position() < max) {
102                 if(buf.remaining() == 0) {
103                     ByteBuffer n = ByteBuffer.allocate(Math.min(buf.capacity() * 2, max));
104                     buf.flip();
105                     n.put(buf);
106                     buf = n;
107                 }
108                 try {
109                     int rv = in.read(buf);
110                     if(rv <= 0)
111                         break;
112                 } catch(IOException e) {
113                     break;
114                 }
115             }
116             buf.flip();
117             parse(ret, buf);
118         }
119         return(ret);
120     }
121
122     public static FormData get(Request req) {
123         FormData ret = (FormData)req.env.get(FormData.class);
124         if(ret == null)
125             req.env.put(FormData.class, ret = read(req));
126         return(ret);
127     }
128
129     static class Collector extends ByteArrayOutputStream {
130         final FormData form = new FormData();
131         final Request req;
132
133         Collector(Request req) {
134             this.req = req;
135             req.env.put(FormData.class, this.form);
136             String query = (String)req.env.get("QUERY_STRING");
137             if(query != null)
138                 parse(form, query);
139         }
140
141         public void write(int b) {
142             if(count < MAX_LENGTH)
143                 super.write(b);
144         }
145
146         public void write(byte[] buf, int off, int len) {
147             len = Math.min(len, MAX_LENGTH - count);
148             if(len > 0)
149                 super.write(buf, off, len);
150         }
151
152         public void close() {
153         }
154     }
155
156     public static Map<Object, Object> feed(Request req, Handler next) {
157         Map<Object, Object> resp = new HashMap<>();
158         if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
159             resp.put("jagi.status", "feed-input");
160             resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
161             resp.put("jagi.input-sink", new Collector(req));
162         } else {
163             read(req);
164             resp.put("jagi.status", "chain");
165             resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
166         }
167         return(resp);
168     }
169 }