1 /* 2 * Hunt - A refined core library for D programming language. 3 * 4 * Copyright (C) 2018-2019 HuntLabs 5 * 6 * Website: https://www.huntlabs.net/ 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.util.MimeTypeUtils; 13 14 import hunt.util.AcceptMimeType; 15 import hunt.util.MimeType; 16 17 import hunt.collection; 18 import hunt.logging; 19 20 import hunt.text.Charset; 21 import hunt.Exceptions; 22 import hunt.text; 23 import hunt.util.Traits; 24 25 import std.algorithm; 26 import std.array; 27 import std.container.array; 28 import std.conv; 29 import std.file; 30 import std.path; 31 import std.range; 32 import std.stdio; 33 import std.string; 34 import std.uni; 35 36 37 /** 38 */ 39 class MimeTypeUtils { 40 41 // private __gshared static ByteBuffer[string] TYPES; // = new ArrayTrie<>(512); 42 private __gshared static Map!(string, string) __dftMimeMap; 43 private __gshared static Map!(string, string) __inferredEncodings; 44 private __gshared static Map!(string, string) __assumedEncodings; 45 46 47 __gshared MimeType[string] CACHE; 48 49 50 shared static this() { 51 __dftMimeMap = new HashMap!(string, string)(); 52 __inferredEncodings = new HashMap!(string, string)(); 53 __assumedEncodings = new HashMap!(string, string)(); 54 55 foreach (MimeType type ; MimeType.values) { 56 CACHE[type.toString()] = type; 57 // TYPES[type.toString()] = type.asBuffer(); 58 59 auto charset = type.toString().indexOf(";charset="); 60 if (charset > 0) { 61 string alt = type.toString().replace(";charset=", "; charset="); 62 CACHE[alt] = type; 63 // TYPES[alt] = type.asBuffer(); 64 } 65 66 if (type.isCharsetAssumed()) 67 __assumedEncodings.put(type.asString(), type.getCharsetString()); 68 } 69 70 string resourcePath = dirName(thisExePath()) ~ "/resources"; 71 72 string resourceName = buildPath(resourcePath, "mime.properties"); 73 loadMimeProperties(resourceName); 74 75 resourceName = buildPath(resourcePath, "encoding.properties"); 76 loadEncodingProperties(resourceName); 77 78 } 79 80 private static void loadMimeProperties(string fileName) { 81 if(!exists(fileName)) { 82 version(HUNT_DEBUG) warningf("File does not exist: %s", fileName); 83 return; 84 } 85 86 void doLoad() { 87 version(HUNT_DEBUG) tracef("loading MIME properties from: %s", fileName); 88 try { 89 File f = File(fileName, "r"); 90 scope(exit) f.close(); 91 string line; 92 int count = 0; 93 while((line = f.readln()) !is null) { 94 string[] parts = split(line, "="); 95 if(parts.length < 2) continue; 96 97 count++; 98 string key = parts[0].strip().toLower(); 99 string value = normalizeMimeType(parts[1].strip()); 100 // trace(key, " = ", value); 101 __dftMimeMap.put(key, value); 102 } 103 104 if (__dftMimeMap.size() == 0) { 105 warningf("Empty mime types at %s", fileName); 106 } else if (__dftMimeMap.size() < count) { 107 warningf("Duplicate or null mime-type extension in resource: %s", fileName); 108 } 109 } catch(Exception ex) { 110 warningf(ex.toString()); 111 } 112 } 113 114 doLoad(); 115 // import std.parallelism; 116 // auto t = task(&doLoad); 117 // t.executeInNewThread(); 118 } 119 120 private static void loadEncodingProperties(string fileName) { 121 if(!exists(fileName)) { 122 version(HUNT_DEBUG) warningf("File does not exist: %s", fileName); 123 return; 124 } 125 126 version(HUNT_DEBUG) tracef("loading MIME properties from: %s", fileName); 127 try { 128 File f = File(fileName, "r"); 129 scope(exit) f.close(); 130 string line; 131 int count = 0; 132 while((line = f.readln()) !is null) { 133 string[] parts = split(line, "="); 134 if(parts.length < 2) continue; 135 136 count++; 137 string t = parts[0].strip(); 138 string charset = parts[1].strip(); 139 version(HUNT_DEBUG) trace(t, " = ", charset); 140 if(charset.startsWith("-")) 141 __assumedEncodings.put(t, charset[1..$]); 142 else 143 __inferredEncodings.put(t, charset); 144 } 145 146 if (__inferredEncodings.size() == 0) { 147 warningf("Empty encodings at %s", fileName); 148 } else if (__inferredEncodings.size() + __inferredEncodings.size() < count) { 149 warningf("Null or duplicate encodings in resource: %s", fileName); 150 } 151 } catch(Exception ex) { 152 warningf(ex.toString()); 153 } 154 } 155 156 /** 157 * Constructor. 158 */ 159 this() { 160 } 161 162 Map!(string, string) getMimeMap() { 163 if(_mimeMap is null) 164 _mimeMap = new HashMap!(string, string)(); 165 return _mimeMap; 166 } 167 168 private Map!(string, string) _mimeMap; 169 170 /** 171 * @param mimeMap A Map of file extension to mime-type. 172 */ 173 void setMimeMap(Map!(string, string) mimeMap) { 174 _mimeMap.clear(); 175 if (mimeMap !is null) { 176 foreach (string k, string v ; mimeMap) { 177 _mimeMap.put(std.uni.toLower(k), normalizeMimeType(v)); 178 } 179 } 180 } 181 182 /** 183 * Get the MIME type by filename extension. 184 * Lookup only the static default mime map. 185 * 186 * @param filename A file name 187 * @return MIME type matching the longest dot extension of the 188 * file name. 189 */ 190 static string getDefaultMimeByExtension(string filename) { 191 string type = null; 192 193 if (filename != null) { 194 ptrdiff_t i = -1; 195 while (type == null) { 196 i = filename.indexOf(".", i + 1); 197 198 if (i < 0 || i >= filename.length) 199 break; 200 201 string ext = std.uni.toLower(filename[i + 1 .. $]); 202 if (type == null) 203 type = __dftMimeMap.get(ext); 204 } 205 } 206 207 if (type == null) { 208 if (type == null) 209 type = __dftMimeMap.get("*"); 210 } 211 212 return type; 213 } 214 215 /** 216 * Get the MIME type by filename extension. 217 * Lookup the content and static default mime maps. 218 * 219 * @param filename A file name 220 * @return MIME type matching the longest dot extension of the 221 * file name. 222 */ 223 string getMimeByExtension(string filename) { 224 string type = null; 225 226 if (filename != null) { 227 ptrdiff_t i = -1; 228 while (type == null) { 229 i = filename.indexOf(".", i + 1); 230 231 if (i < 0 || i >= filename.length) 232 break; 233 234 string ext = std.uni.toLower(filename[i + 1 .. $]); 235 if (_mimeMap !is null) 236 type = _mimeMap.get(ext); 237 if (type == null) 238 type = __dftMimeMap.get(ext); 239 } 240 } 241 242 if (type == null) { 243 if (_mimeMap !is null) 244 type = _mimeMap.get("*"); 245 if (type == null) 246 type = __dftMimeMap.get("*"); 247 } 248 249 return type; 250 } 251 252 /** 253 * Set a mime mapping 254 * 255 * @param extension the extension 256 * @param type the mime type 257 */ 258 void addMimeMapping(string extension, string type) { 259 _mimeMap.put(std.uni.toLower(extension), normalizeMimeType(type)); 260 } 261 262 static Set!string getKnownMimeTypes() { 263 auto hs = new HashSet!(string)(); 264 foreach(v ; __dftMimeMap.byValue()) 265 hs.add(v); 266 return hs; 267 } 268 269 private static string normalizeMimeType(string type) { 270 MimeType t = CACHE.get(type, null); 271 if (t !is null) 272 return t.asString(); 273 274 return std.uni.toLower(type); 275 } 276 277 static string getCharsetFromContentType(string value) { 278 if (value == null) 279 return null; 280 int end = cast(int)value.length; 281 int state = 0; 282 int start = 0; 283 bool quote = false; 284 int i = 0; 285 for (; i < end; i++) { 286 char b = value[i]; 287 288 if (quote && state != 10) { 289 if ('"' == b) 290 quote = false; 291 continue; 292 } 293 294 if (';' == b && state <= 8) { 295 state = 1; 296 continue; 297 } 298 299 switch (state) { 300 case 0: 301 if ('"' == b) { 302 quote = true; 303 break; 304 } 305 break; 306 307 case 1: 308 if ('c' == b) state = 2; 309 else if (' ' != b) state = 0; 310 break; 311 case 2: 312 if ('h' == b) state = 3; 313 else state = 0; 314 break; 315 case 3: 316 if ('a' == b) state = 4; 317 else state = 0; 318 break; 319 case 4: 320 if ('r' == b) state = 5; 321 else state = 0; 322 break; 323 case 5: 324 if ('s' == b) state = 6; 325 else state = 0; 326 break; 327 case 6: 328 if ('e' == b) state = 7; 329 else state = 0; 330 break; 331 case 7: 332 if ('t' == b) state = 8; 333 else state = 0; 334 break; 335 336 case 8: 337 if ('=' == b) state = 9; 338 else if (' ' != b) state = 0; 339 break; 340 341 case 9: 342 if (' ' == b) 343 break; 344 if ('"' == b) { 345 quote = true; 346 start = i + 1; 347 state = 10; 348 break; 349 } 350 start = i; 351 state = 10; 352 break; 353 354 case 10: 355 if (!quote && (';' == b || ' ' == b) || 356 (quote && '"' == b)) 357 return StringUtils.normalizeCharset(value, start, i - start); 358 break; 359 360 default: break; 361 } 362 } 363 364 if (state == 10) 365 return StringUtils.normalizeCharset(value, start, i - start); 366 367 return null; 368 } 369 370 /** 371 * Access a mutable map of mime type to the charset inferred from that content type. 372 * An inferred encoding is used by when encoding/decoding a stream and is 373 * explicitly set in any metadata (eg Content-MimeType). 374 * 375 * @return Map of mime type to charset 376 */ 377 static Map!(string, string) getInferredEncodings() { 378 return __inferredEncodings; 379 } 380 381 /** 382 * Access a mutable map of mime type to the charset assumed for that content type. 383 * An assumed encoding is used by when encoding/decoding a stream, but is not 384 * explicitly set in any metadata (eg Content-MimeType). 385 * 386 * @return Map of mime type to charset 387 */ 388 static Map!(string, string) getAssumedEncodings() { 389 return __inferredEncodings; 390 } 391 392 static string getCharsetInferredFromContentType(string contentType) { 393 return __inferredEncodings.get(contentType); 394 } 395 396 static string getCharsetAssumedFromContentType(string contentType) { 397 return __assumedEncodings.get(contentType); 398 } 399 400 static string getContentTypeWithoutCharset(string value) { 401 int end = cast(int)value.length; 402 int state = 0; 403 int start = 0; 404 bool quote = false; 405 int i = 0; 406 StringBuilder builder = null; 407 for (; i < end; i++) { 408 char b = value[i]; 409 410 if ('"' == b) { 411 quote = !quote; 412 413 switch (state) { 414 case 11: 415 builder.append(b); 416 break; 417 case 10: 418 break; 419 case 9: 420 builder = new StringBuilder(); 421 builder.append(value, 0, start + 1); 422 state = 10; 423 break; 424 default: 425 start = i; 426 state = 0; 427 } 428 continue; 429 } 430 431 if (quote) { 432 if (builder !is null && state != 10) 433 builder.append(b); 434 continue; 435 } 436 437 switch (state) { 438 case 0: 439 if (';' == b) 440 state = 1; 441 else if (' ' != b) 442 start = i; 443 break; 444 445 case 1: 446 if ('c' == b) state = 2; 447 else if (' ' != b) state = 0; 448 break; 449 case 2: 450 if ('h' == b) state = 3; 451 else state = 0; 452 break; 453 case 3: 454 if ('a' == b) state = 4; 455 else state = 0; 456 break; 457 case 4: 458 if ('r' == b) state = 5; 459 else state = 0; 460 break; 461 case 5: 462 if ('s' == b) state = 6; 463 else state = 0; 464 break; 465 case 6: 466 if ('e' == b) state = 7; 467 else state = 0; 468 break; 469 case 7: 470 if ('t' == b) state = 8; 471 else state = 0; 472 break; 473 case 8: 474 if ('=' == b) state = 9; 475 else if (' ' != b) state = 0; 476 break; 477 478 case 9: 479 if (' ' == b) 480 break; 481 builder = new StringBuilder(); 482 builder.append(value, 0, start + 1); 483 state = 10; 484 break; 485 486 case 10: 487 if (';' == b) { 488 builder.append(b); 489 state = 11; 490 } 491 break; 492 493 case 11: 494 if (' ' != b) 495 builder.append(b); 496 break; 497 498 default: break; 499 } 500 } 501 if (builder is null) 502 return value; 503 return builder.toString(); 504 505 } 506 507 static string getContentTypeMIMEType(string contentType) { 508 if (contentType.empty) 509 return null; 510 511 // parsing content-type 512 string[] strings = StringUtils.split(contentType, ";"); 513 return strings[0]; 514 } 515 516 static List!string getAcceptMIMETypes(string accept) { 517 if(accept.empty) 518 new EmptyList!string(); // Collections.emptyList(); 519 520 List!string list = new ArrayList!string(); 521 // parsing accept 522 string[] strings = StringUtils.split(accept, ","); 523 foreach (string str ; strings) { 524 string[] s = StringUtils.split(str, ";"); 525 list.add(s[0].strip()); 526 } 527 return list; 528 } 529 530 static AcceptMimeType[] parseAcceptMIMETypes(string accept) { 531 532 if(accept.empty) 533 return []; 534 535 string[] arr = StringUtils.split(accept, ","); 536 return apply(arr); 537 } 538 539 private static AcceptMimeType[] apply(string[] stream) { 540 541 Array!AcceptMimeType arr; 542 543 foreach(string s; stream) { 544 string type = strip(s); 545 if(type.empty) continue; 546 string[] mimeTypeAndQuality = StringUtils.split(type, ';'); 547 AcceptMimeType acceptMIMEType = new AcceptMimeType(); 548 549 // parse the MIME type 550 string[] mimeType = StringUtils.split(mimeTypeAndQuality[0].strip(), '/'); 551 string parentType = mimeType[0].strip(); 552 string childType = mimeType[1].strip(); 553 acceptMIMEType.setParentType(parentType); 554 acceptMIMEType.setChildType(childType); 555 if (parentType == "*") { 556 if (childType == "*") { 557 acceptMIMEType.setMatchType(AcceptMimeMatchType.ALL); 558 } else { 559 acceptMIMEType.setMatchType(AcceptMimeMatchType.CHILD); 560 } 561 } else { 562 if (childType == "*") { 563 acceptMIMEType.setMatchType(AcceptMimeMatchType.PARENT); 564 } else { 565 acceptMIMEType.setMatchType(AcceptMimeMatchType.EXACT); 566 } 567 } 568 569 // parse the quality 570 if (mimeTypeAndQuality.length > 1) { 571 string q = mimeTypeAndQuality[1]; 572 string[] qualityKV = StringUtils.split(q, '='); 573 acceptMIMEType.setQuality(to!float(qualityKV[1].strip())); 574 } 575 arr.insertBack(acceptMIMEType); 576 } 577 578 for(size_t i=0; i<arr.length-1; i++) { 579 for(size_t j=i+1; j<arr.length; j++) { 580 AcceptMimeType a = arr[i]; 581 AcceptMimeType b = arr[j]; 582 if(b.getQuality() > a.getQuality()) { // The greater quality is first. 583 arr[i] = b; arr[j] = a; 584 } 585 } 586 } 587 588 return arr.array(); 589 } 590 }