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 }