1 module hunt.http.QuotedCSV;
2 
3 import hunt.text.Common;
4 import hunt.util.Common;
5 import hunt.util.StringBuilder;
6 
7 import std.array;
8 import std.conv;
9 import std.container.array;
10 
11 /**
12  * Implements a quoted comma separated list of values
13  * in accordance with RFC7230.
14  * OWS is removed and quoted characters ignored for parsing.
15  *
16  * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
17  * @see "https://tools.ietf.org/html/rfc7230#section-7"
18  */
19 class QuotedCSV : Iterable!string {
20 
21     private enum State {VALUE, PARAM_NAME, PARAM_VALUE}
22 
23     protected  Array!string _values; // = new ArrayList<>();
24     protected  bool _keepQuotes;
25 
26     this(string[] values...) {
27         this(true, values);
28     }
29 
30     this(bool keepQuotes, string[] values...) {
31         _keepQuotes = keepQuotes;
32         foreach (string v ; values)
33             addValue(v);
34     }
35 
36     /**
37      * Add and parse a value string(s)
38      *
39      * @param value A value that may contain one or more Quoted CSV items.
40      */
41 
42     void addValue(string value)
43      {
44         if (value.empty)
45             return;
46 
47         StringBuilder buffer = new StringBuilder();
48 
49         int l = cast(int)value.length;
50         State state = State.VALUE;
51         bool quoted = false;
52         bool sloshed = false;
53         int nws_length = 0;
54         int last_length = 0;
55         int value_length = -1;
56         int param_name = -1;
57         int param_value = -1;
58 
59         for (int i = 0; i <= l; i++) {
60             char c = (i == l ? 0 : value[i]);
61 
62             // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6
63             if (quoted && c != 0) {
64                 if (sloshed)
65                     sloshed = false;
66                 else {
67                     switch (c) {
68                         case '\\':
69                             sloshed = true;
70                             if (!_keepQuotes)
71                                 continue;
72                             break;
73                         case '"':
74                             quoted = false;
75                             if (!_keepQuotes)
76                                 continue;
77                             break;
78                         default: break;
79                     }
80                 }
81 
82                 buffer.append(c);
83                 nws_length = buffer.length;
84                 continue;
85             }
86 
87             // Handle common cases
88             switch (c) {
89                 case ' ':
90                 case '\t':
91                     if (buffer.length > last_length) // not leading OWS
92                         buffer.append(c);
93                     continue;
94 
95                 case '"':
96                     quoted = true;
97                     if (_keepQuotes) {
98                         if (state == State.PARAM_VALUE && param_value < 0)
99                             param_value = nws_length;
100                         buffer.append(c);
101                     } else if (state == State.PARAM_VALUE && param_value < 0)
102                         param_value = nws_length;
103                     nws_length = buffer.length;
104                     continue;
105 
106                 case ';':
107                     buffer.setLength(nws_length); // trim following OWS
108                     if (state == State.VALUE) {
109                         parsedValue(buffer);
110                         value_length = buffer.length;
111                     } else
112                         parsedParam(buffer, value_length, param_name, param_value);
113                     nws_length = buffer.length;
114                     param_name = param_value = -1;
115                     buffer.append(c);
116                     last_length = ++nws_length;
117                     state = State.PARAM_NAME;
118                     continue;
119 
120                 case ',':
121                 case 0:
122                     if (nws_length > 0) {
123                         buffer.setLength(nws_length); // trim following OWS
124                         switch (state) {
125                             case State.VALUE:
126                                 parsedValue(buffer);
127                                 break;
128                             case State.PARAM_NAME:
129                             case State.PARAM_VALUE:
130                                 parsedParam(buffer, value_length, param_name, param_value);
131                                 break;
132                             default: break;
133                         }
134                         _values.insertBack(buffer.toString());
135                     }
136                     buffer.clear();
137                     last_length = 0;
138                     nws_length = 0;
139                     value_length = param_name = param_value = -1;
140                     state = State.VALUE;
141                     continue;
142 
143                 case '=':
144                     switch (state) {
145                         case State.VALUE:
146                             // It wasn't really a value, it was a param name
147                             value_length = param_name = 0;
148                             buffer.setLength(nws_length); // trim following OWS
149                             string param = buffer.toString();
150                             buffer.clear();
151                             parsedValue(buffer);
152                             value_length = buffer.length;
153                             buffer.append(param);
154                             buffer.append(c);
155                             last_length = ++nws_length;
156                             state = State.PARAM_VALUE;
157                             continue;
158 
159                         case State.PARAM_NAME:
160                             buffer.setLength(nws_length); // trim following OWS
161                             buffer.append(c);
162                             last_length = ++nws_length;
163                             state = State.PARAM_VALUE;
164                             continue;
165 
166                         case State.PARAM_VALUE:
167                             if (param_value < 0)
168                                 param_value = nws_length;
169                             buffer.append(c);
170                             nws_length = buffer.length;
171                             continue;
172 
173                         default: break;
174                     }
175                     continue;
176 
177                 default: {
178                     final switch (state) {
179                         case State.VALUE: {
180                             buffer.append(c);
181                             nws_length = buffer.length;
182                             continue;
183                         }
184 
185                         case State.PARAM_NAME: {
186                             if (param_name < 0)
187                                 param_name = nws_length;
188                             buffer.append(c);
189                             nws_length = buffer.length;
190                             continue;
191                         }
192 
193                         case State.PARAM_VALUE: {
194                             if (param_value < 0)
195                                 param_value = nws_length;
196                             buffer.append(c);
197                             nws_length = buffer.length;
198                         }
199                     }
200                 }
201             }
202         }
203     }
204 
205     /**
206      * Called when a value has been parsed
207      *
208      * @param buffer Containing the trimmed value, which may be mutated
209      */
210     protected void parsedValue(ref StringBuilder buffer) {
211     }
212 
213     /**
214      * Called when a parameter has been parsed
215      *
216      * @param buffer      Containing the trimmed value and all parameters, which may be mutated
217      * @param valueLength The length of the value
218      * @param paramName   The index of the start of the parameter just parsed
219      * @param paramValue  The index of the start of the parameter value just parsed, or -1
220      */
221     protected void parsedParam(ref StringBuilder, int valueLength, int paramName, int paramValue) {
222     }
223 
224     int size() {
225         return cast(int)_values.length;
226     }
227 
228     bool isEmpty() {
229         return _values.empty();
230     }
231 
232     // List<string> getValues() {
233     //     return _values;
234     // }
235     // ref Array!string getValues() {
236     //     return _values;
237     // }
238     string[] getValues() {
239         return _values[].array;
240     }
241 
242     int opApply(scope int delegate(ref string) dg)
243     {
244         int result = 0;
245         foreach(string v; _values)
246         {
247             result = dg(v);
248             if(result != 0) return result;
249         }
250         return result;
251     }
252 
253     static string unquote(string s) {
254         // if (!StringUtils.hasText(s)) {
255         //     return s;
256         // }
257         // handle trivial cases
258         int l = cast(int)s.length;
259         // Look for any quotes
260         int i = 0;
261         for (; i < l; i++) {
262             char c = s[i];
263             if (c == '"')
264                 break;
265         }
266         if (i == l)
267             return s;
268 
269         bool quoted = true;
270         bool sloshed = false;
271         StringBuilder buffer = new StringBuilder();
272         buffer.append(s, 0, i);
273         i++;
274         for (; i < l; i++) {
275             char c = s[i];
276             if (quoted) {
277                 if (sloshed) {
278                     buffer.append(c);
279                     sloshed = false;
280                 } else if (c == '"')
281                     quoted = false;
282                 else if (c == '\\')
283                     sloshed = true;
284                 else
285                     buffer.append(c);
286             } else if (c == '"')
287                 quoted = true;
288             else
289                 buffer.append(c);
290         }
291         return buffer.toString();
292     }
293 
294     override
295     string toString() {
296         return to!string(_values[]);
297     }
298 }