MimeKit.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. #nullable disable
  2. namespace Jint.Native.Date;
  3. // This file contains code extracted from excellent MimeKit library
  4. // https://github.com/jstedfast/MimeKit , see above copyright which applies to all code
  5. // Jint version has adjusted namespaces and made members visible / removed unused ones
  6. // Author: Jeffrey Stedfast <[email protected]>
  7. //
  8. // Copyright (c) 2013-2022 .NET Foundation and Contributors
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining a copy
  11. // of this software and associated documentation files (the "Software"), to deal
  12. // in the Software without restriction, including without limitation the rights
  13. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. // copies of the Software, and to permit persons to whom the Software is
  15. // furnished to do so, subject to the following conditions:
  16. //
  17. // The above copyright notice and this permission notice shall be included in
  18. // all copies or substantial portions of the Software.
  19. //
  20. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  25. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  26. // THE SOFTWARE.
  27. //
  28. using System;
  29. using System.Text;
  30. using System.Collections.Generic;
  31. [Flags]
  32. internal enum DateTokenFlags : byte
  33. {
  34. None = 0,
  35. NonNumeric = (1 << 0),
  36. NonWeekday = (1 << 1),
  37. NonMonth = (1 << 2),
  38. NonTime = (1 << 3),
  39. NonAlphaZone = (1 << 4),
  40. NonNumericZone = (1 << 5),
  41. HasColon = (1 << 6),
  42. HasSign = (1 << 7),
  43. }
  44. internal readonly struct DateToken
  45. {
  46. public DateToken(DateTokenFlags flags, int start, int length)
  47. {
  48. Flags = flags;
  49. Start = start;
  50. Length = length;
  51. }
  52. public DateTokenFlags Flags { get; }
  53. public int Start { get; }
  54. public int Length { get; }
  55. public bool IsNumeric
  56. {
  57. get { return (Flags & DateTokenFlags.NonNumeric) == 0; }
  58. }
  59. public bool IsWeekday
  60. {
  61. get { return (Flags & DateTokenFlags.NonWeekday) == 0; }
  62. }
  63. public bool IsMonth
  64. {
  65. get { return (Flags & DateTokenFlags.NonMonth) == 0; }
  66. }
  67. public bool IsTimeOfDay
  68. {
  69. get { return (Flags & DateTokenFlags.NonTime) == 0 && (Flags & DateTokenFlags.HasColon) != 0; }
  70. }
  71. public bool IsNumericZone
  72. {
  73. get { return (Flags & DateTokenFlags.NonNumericZone) == 0 && (Flags & DateTokenFlags.HasSign) != 0; }
  74. }
  75. public bool IsAlphaZone
  76. {
  77. get { return (Flags & DateTokenFlags.NonAlphaZone) == 0; }
  78. }
  79. public bool IsTimeZone
  80. {
  81. get { return IsNumericZone || IsAlphaZone; }
  82. }
  83. }
  84. /// <summary>
  85. /// Utility methods to parse and format rfc822 date strings.
  86. /// </summary>
  87. /// <remarks>
  88. /// Utility methods to parse and format rfc822 date strings.
  89. /// </remarks>
  90. internal static class DateUtils
  91. {
  92. private const string MonthCharacters = "JanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember";
  93. private const string WeekdayCharacters = "SundayMondayTuesdayWednesdayThursdayFridaySaturday";
  94. private const string AlphaZoneCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  95. private const string NumericZoneCharacters = "+-0123456789";
  96. private const string NumericCharacters = "0123456789";
  97. private const string TimeCharacters = "0123456789:";
  98. private static readonly string[] Months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
  99. private static readonly string[] WeekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
  100. private static readonly Dictionary<string, int> timezones;
  101. private static readonly DateTokenFlags[] datetok;
  102. static DateUtils()
  103. {
  104. timezones = new Dictionary<string, int>(StringComparer.Ordinal)
  105. {
  106. { "UT", 0 },
  107. { "UTC", 0 },
  108. { "GMT", 0 },
  109. { "EDT", -400 },
  110. { "EST", -500 },
  111. { "CDT", -500 },
  112. { "CST", -600 },
  113. { "MDT", -600 },
  114. { "MST", -700 },
  115. { "PDT", -700 },
  116. { "PST", -800 },
  117. // Note: rfc822 got the signs backwards for the military
  118. // timezones so some sending clients may mistakenly use the
  119. // wrong values.
  120. { "A", 100 },
  121. { "B", 200 },
  122. { "C", 300 },
  123. { "D", 400 },
  124. { "E", 500 },
  125. { "F", 600 },
  126. { "G", 700 },
  127. { "H", 800 },
  128. { "I", 900 },
  129. { "K", 1000 },
  130. { "L", 1100 },
  131. { "M", 1200 },
  132. { "N", -100 },
  133. { "O", -200 },
  134. { "P", -300 },
  135. { "Q", -400 },
  136. { "R", -500 },
  137. { "S", -600 },
  138. { "T", -700 },
  139. { "U", -800 },
  140. { "V", -900 },
  141. { "W", -1000 },
  142. { "X", -1100 },
  143. { "Y", -1200 },
  144. { "Z", 0 },
  145. };
  146. datetok = new DateTokenFlags[256];
  147. var any = new char[2];
  148. for (int c = 0; c < 256; c++)
  149. {
  150. if (c >= 0x41 && c <= 0x5a)
  151. {
  152. any[1] = (char) (c + 0x20);
  153. any[0] = (char) c;
  154. }
  155. else if (c >= 0x61 && c <= 0x7a)
  156. {
  157. any[0] = (char) (c - 0x20);
  158. any[1] = (char) c;
  159. }
  160. if (NumericZoneCharacters.IndexOf((char) c) == -1)
  161. datetok[c] |= DateTokenFlags.NonNumericZone;
  162. if (AlphaZoneCharacters.IndexOf((char) c) == -1)
  163. datetok[c] |= DateTokenFlags.NonAlphaZone;
  164. if (WeekdayCharacters.IndexOfAny(any) == -1)
  165. datetok[c] |= DateTokenFlags.NonWeekday;
  166. if (NumericCharacters.IndexOf((char) c) == -1)
  167. datetok[c] |= DateTokenFlags.NonNumeric;
  168. if (MonthCharacters.IndexOfAny(any) == -1)
  169. datetok[c] |= DateTokenFlags.NonMonth;
  170. if (TimeCharacters.IndexOf((char) c) == -1)
  171. datetok[c] |= DateTokenFlags.NonTime;
  172. }
  173. datetok[':'] |= DateTokenFlags.HasColon;
  174. datetok['+'] |= DateTokenFlags.HasSign;
  175. datetok['-'] |= DateTokenFlags.HasSign;
  176. }
  177. private static bool TryGetWeekday(in DateToken token, byte[] text, out DayOfWeek weekday)
  178. {
  179. weekday = DayOfWeek.Sunday;
  180. if (!token.IsWeekday || token.Length < 3)
  181. return false;
  182. var name = Encoding.ASCII.GetString(text, token.Start, 3);
  183. for (int day = 0; day < WeekDays.Length; day++)
  184. {
  185. if (WeekDays[day].Equals(name, StringComparison.OrdinalIgnoreCase))
  186. {
  187. weekday = (DayOfWeek) day;
  188. return true;
  189. }
  190. }
  191. return false;
  192. }
  193. private static bool TryGetDayOfMonth(in DateToken token, byte[] text, out int day)
  194. {
  195. int endIndex = token.Start + token.Length;
  196. int index = token.Start;
  197. day = 0;
  198. if (!token.IsNumeric)
  199. return false;
  200. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out day))
  201. return false;
  202. if (day <= 0 || day > 31)
  203. return false;
  204. return true;
  205. }
  206. private static bool TryGetMonth(in DateToken token, byte[] text, out int month)
  207. {
  208. month = 0;
  209. if (!token.IsMonth || token.Length < 3)
  210. return false;
  211. var name = Encoding.ASCII.GetString(text, token.Start, 3);
  212. for (int i = 0; i < Months.Length; i++)
  213. {
  214. if (Months[i].Equals(name, StringComparison.OrdinalIgnoreCase))
  215. {
  216. month = i + 1;
  217. return true;
  218. }
  219. }
  220. return false;
  221. }
  222. private static bool TryGetYear(in DateToken token, byte[] text, out int year)
  223. {
  224. int endIndex = token.Start + token.Length;
  225. int index = token.Start;
  226. year = 0;
  227. if (!token.IsNumeric)
  228. return false;
  229. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out year))
  230. return false;
  231. if (year < 100)
  232. year += (year < 70) ? 2000 : 1900;
  233. return year >= 1969;
  234. }
  235. private static bool TryGetTimeOfDay(in DateToken token, byte[] text, out int hour, out int minute, out int second)
  236. {
  237. int endIndex = token.Start + token.Length;
  238. int index = token.Start;
  239. hour = minute = second = 0;
  240. if (!token.IsTimeOfDay)
  241. return false;
  242. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out hour) || hour > 23)
  243. return false;
  244. if (index >= endIndex || text[index++] != (byte) ':')
  245. return false;
  246. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out minute) || minute > 59)
  247. return false;
  248. // Allow just hh:mm (i.e. w/o the :ss?)
  249. if (index >= endIndex || text[index++] != (byte) ':')
  250. return true;
  251. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out second) || second > 59)
  252. return false;
  253. return index == endIndex;
  254. }
  255. private static bool TryGetTimeZone(in DateToken token, byte[] text, out int tzone)
  256. {
  257. tzone = 0;
  258. if (token.IsNumericZone)
  259. {
  260. int endIndex = token.Start + token.Length;
  261. int index = token.Start;
  262. int sign;
  263. if (text[index] == (byte) '-')
  264. sign = -1;
  265. else if (text[index] == (byte) '+')
  266. sign = 1;
  267. else
  268. return false;
  269. index++;
  270. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out tzone) || index != endIndex)
  271. return false;
  272. tzone *= sign;
  273. }
  274. else if (token.IsAlphaZone)
  275. {
  276. if (token.Length > 3)
  277. return false;
  278. var name = Encoding.ASCII.GetString(text, token.Start, token.Length);
  279. if (!timezones.TryGetValue(name, out tzone))
  280. return false;
  281. }
  282. else if (token.IsNumeric)
  283. {
  284. int endIndex = token.Start + token.Length;
  285. int index = token.Start;
  286. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out tzone) || index != endIndex)
  287. return false;
  288. }
  289. if (tzone < -1200 || tzone > 1400)
  290. return false;
  291. return true;
  292. }
  293. private static bool IsTokenDelimeter(byte c)
  294. {
  295. return c == (byte) '-' || c == (byte) '/' || c == (byte) ',' || c.IsWhitespace();
  296. }
  297. private static IEnumerable<DateToken> TokenizeDate(byte[] text, int startIndex, int length)
  298. {
  299. int endIndex = startIndex + length;
  300. int index = startIndex;
  301. DateTokenFlags mask;
  302. int start;
  303. while (index < endIndex)
  304. {
  305. if (!ParseUtils.SkipCommentsAndWhiteSpace(text, ref index, endIndex, false))
  306. break;
  307. if (index >= endIndex)
  308. break;
  309. // get the initial mask for this token
  310. if ((mask = datetok[text[index]]) != DateTokenFlags.None)
  311. {
  312. start = index++;
  313. // find the end of this token
  314. while (index < endIndex && !IsTokenDelimeter(text[index]))
  315. mask |= datetok[text[index++]];
  316. yield return new DateToken(mask, start, index - start);
  317. }
  318. // skip over the token delimeter
  319. index++;
  320. }
  321. yield break;
  322. }
  323. private static bool TryParseStandardDateFormat(List<DateToken> tokens, byte[] text, out DateTimeOffset date)
  324. {
  325. //bool haveWeekday;
  326. int n = 0;
  327. date = new DateTimeOffset();
  328. // we need at least 5 tokens, 6 if we have a weekday
  329. if (tokens.Count < 5)
  330. return false;
  331. // Note: the weekday is not required
  332. if (TryGetWeekday(tokens[n], text, out _))
  333. {
  334. if (tokens.Count < 6)
  335. return false;
  336. //haveWeekday = true;
  337. n++;
  338. }
  339. if (!TryGetDayOfMonth(tokens[n++], text, out int day))
  340. return false;
  341. if (!TryGetMonth(tokens[n++], text, out int month))
  342. return false;
  343. if (!TryGetYear(tokens[n++], text, out int year))
  344. return false;
  345. if (!TryGetTimeOfDay(tokens[n++], text, out int hour, out int minute, out int second))
  346. return false;
  347. if (!TryGetTimeZone(tokens[n], text, out int tzone))
  348. tzone = 0;
  349. int minutes = tzone % 100;
  350. int hours = tzone / 100;
  351. var offset = new TimeSpan(hours, minutes, 0);
  352. try
  353. {
  354. date = new DateTimeOffset(year, month, day, hour, minute, second, offset);
  355. }
  356. catch (ArgumentOutOfRangeException)
  357. {
  358. return false;
  359. }
  360. return true;
  361. }
  362. private static bool TryParseUnknownDateFormat(IList<DateToken> tokens, byte[] text, out DateTimeOffset date)
  363. {
  364. int? day = null, month = null, year = null, tzone = null;
  365. int hour = 0, minute = 0, second = 0;
  366. bool numericMonth = false;
  367. bool haveWeekday = false;
  368. bool haveTime = false;
  369. TimeSpan offset;
  370. for (int i = 0; i < tokens.Count; i++)
  371. {
  372. int value;
  373. if (!haveWeekday && TryGetWeekday(tokens[i], text, out _))
  374. {
  375. haveWeekday = true;
  376. continue;
  377. }
  378. if ((month == null || numericMonth) && TryGetMonth(tokens[i], text, out value))
  379. {
  380. if (numericMonth)
  381. {
  382. numericMonth = false;
  383. day = month;
  384. }
  385. month = value;
  386. continue;
  387. }
  388. if (!haveTime && TryGetTimeOfDay(tokens[i], text, out hour, out minute, out second))
  389. {
  390. haveTime = true;
  391. continue;
  392. }
  393. // Limit TryGetTimeZone to alpha and numeric timezone tokens (do not allow numeric tokens as they are handled below).
  394. if (tzone == null && tokens[i].IsTimeZone && TryGetTimeZone(tokens[i], text, out value))
  395. {
  396. tzone = value;
  397. continue;
  398. }
  399. if (tokens[i].IsNumeric)
  400. {
  401. if (tokens[i].Length == 4)
  402. {
  403. if (year == null)
  404. {
  405. if (TryGetYear(tokens[i], text, out value))
  406. year = value;
  407. }
  408. else if (tzone == null)
  409. {
  410. if (TryGetTimeZone(tokens[i], text, out value))
  411. tzone = value;
  412. }
  413. continue;
  414. }
  415. if (tokens[i].Length > 2)
  416. continue;
  417. // Note: we likely have either YYYY[-/]MM[-/]DD or MM[-/]DD[-/]YY
  418. int endIndex = tokens[i].Start + tokens[i].Length;
  419. int index = tokens[i].Start;
  420. ParseUtils.TryParseInt32(text, ref index, endIndex, out value);
  421. if (month == null && value > 0 && value <= 12)
  422. {
  423. numericMonth = true;
  424. month = value;
  425. continue;
  426. }
  427. if (day == null && value > 0 && value <= 31)
  428. {
  429. day = value;
  430. continue;
  431. }
  432. if (year == null && value >= 69)
  433. {
  434. year = 1900 + value;
  435. continue;
  436. }
  437. }
  438. // WTF is this??
  439. }
  440. if (year == null || month == null || day == null)
  441. {
  442. date = new DateTimeOffset();
  443. return false;
  444. }
  445. if (!haveTime)
  446. hour = minute = second = 0;
  447. if (tzone != null)
  448. {
  449. int minutes = tzone.Value % 100;
  450. int hours = tzone.Value / 100;
  451. offset = new TimeSpan(hours, minutes, 0);
  452. }
  453. else
  454. {
  455. offset = new TimeSpan(0);
  456. }
  457. try
  458. {
  459. date = new DateTimeOffset(year.Value, month.Value, day.Value, hour, minute, second, offset);
  460. }
  461. catch (ArgumentOutOfRangeException)
  462. {
  463. date = new DateTimeOffset();
  464. return false;
  465. }
  466. return true;
  467. }
  468. /// <summary>
  469. /// Try to parse the given input buffer into a new <see cref="System.DateTimeOffset"/> instance.
  470. /// </summary>
  471. /// <remarks>
  472. /// Parses an rfc822 date and time from the supplied buffer starting at the given index
  473. /// and spanning across the specified number of bytes.
  474. /// </remarks>
  475. /// <returns><c>true</c>, if the date was successfully parsed, <c>false</c> otherwise.</returns>
  476. /// <param name="buffer">The input buffer.</param>
  477. /// <param name="startIndex">The starting index of the input buffer.</param>
  478. /// <param name="length">The number of bytes in the input buffer to parse.</param>
  479. /// <param name="date">The parsed date.</param>
  480. /// <exception cref="System.ArgumentNullException">
  481. /// <paramref name="buffer"/> is <c>null</c>.
  482. /// </exception>
  483. /// <exception cref="System.ArgumentOutOfRangeException">
  484. /// <paramref name="startIndex"/> and <paramref name="length"/> do not specify
  485. /// a valid range in the byte array.
  486. /// </exception>
  487. public static bool TryParse(byte[] buffer, int startIndex, int length, out DateTimeOffset date)
  488. {
  489. if (buffer == null)
  490. throw new ArgumentNullException(nameof(buffer));
  491. if (startIndex < 0 || startIndex > buffer.Length)
  492. throw new ArgumentOutOfRangeException(nameof(startIndex));
  493. if (length < 0 || length > (buffer.Length - startIndex))
  494. throw new ArgumentOutOfRangeException(nameof(length));
  495. var tokens = new List<DateToken>(TokenizeDate(buffer, startIndex, length));
  496. if (TryParseStandardDateFormat(tokens, buffer, out date))
  497. return true;
  498. if (TryParseUnknownDateFormat(tokens, buffer, out date))
  499. return true;
  500. date = new DateTimeOffset();
  501. return false;
  502. }
  503. /// <summary>
  504. /// Try to parse the given input buffer into a new <see cref="System.DateTimeOffset"/> instance.
  505. /// </summary>
  506. /// <remarks>
  507. /// Parses an rfc822 date and time from the specified text.
  508. /// </remarks>
  509. /// <returns><c>true</c>, if the date was successfully parsed, <c>false</c> otherwise.</returns>
  510. /// <param name="text">The input text.</param>
  511. /// <param name="date">The parsed date.</param>
  512. /// <exception cref="System.ArgumentNullException">
  513. /// <paramref name="text"/> is <c>null</c>.
  514. /// </exception>
  515. public static bool TryParse(string text, out DateTimeOffset date)
  516. {
  517. if (text == null)
  518. throw new ArgumentNullException(nameof(text));
  519. var buffer = Encoding.UTF8.GetBytes(text);
  520. return TryParse(buffer, 0, buffer.Length, out date);
  521. }
  522. }
  523. internal static class ParseUtils
  524. {
  525. public static bool TryParseInt32(byte[] text, ref int index, int endIndex, out int value)
  526. {
  527. int startIndex = index;
  528. value = 0;
  529. while (index < endIndex && text[index] >= (byte) '0' && text[index] <= (byte) '9')
  530. {
  531. int digit = text[index] - (byte) '0';
  532. if (value > int.MaxValue / 10)
  533. {
  534. // integer overflow
  535. return false;
  536. }
  537. if (value == int.MaxValue / 10 && digit > int.MaxValue % 10)
  538. {
  539. // integer overflow
  540. return false;
  541. }
  542. value = (value * 10) + digit;
  543. index++;
  544. }
  545. return index > startIndex;
  546. }
  547. public static bool SkipWhiteSpace(byte[] text, ref int index, int endIndex)
  548. {
  549. int startIndex = index;
  550. while (index < endIndex && text[index].IsWhitespace())
  551. index++;
  552. return index > startIndex;
  553. }
  554. public static bool SkipComment(byte[] text, ref int index, int endIndex)
  555. {
  556. bool escaped = false;
  557. int depth = 1;
  558. index++;
  559. while (index < endIndex && depth > 0)
  560. {
  561. if (text[index] == (byte) '\\')
  562. {
  563. escaped = !escaped;
  564. }
  565. else if (!escaped)
  566. {
  567. if (text[index] == (byte) '(')
  568. depth++;
  569. else if (text[index] == (byte) ')')
  570. depth--;
  571. escaped = false;
  572. }
  573. else
  574. {
  575. escaped = false;
  576. }
  577. index++;
  578. }
  579. return depth == 0;
  580. }
  581. public static bool SkipCommentsAndWhiteSpace(byte[] text, ref int index, int endIndex, bool throwOnError)
  582. {
  583. SkipWhiteSpace(text, ref index, endIndex);
  584. while (index < endIndex && text[index] == (byte) '(')
  585. {
  586. int startIndex = index;
  587. if (!SkipComment(text, ref index, endIndex))
  588. {
  589. if (throwOnError)
  590. throw new Exception($"Incomplete comment token at offset {startIndex}");
  591. return false;
  592. }
  593. SkipWhiteSpace(text, ref index, endIndex);
  594. }
  595. return true;
  596. }
  597. }
  598. [Flags]
  599. internal enum CharType : ushort
  600. {
  601. None = 0,
  602. IsAscii = (1 << 0),
  603. IsAtom = (1 << 1),
  604. IsAttrChar = (1 << 2),
  605. IsBlank = (1 << 3),
  606. IsControl = (1 << 4),
  607. IsDomainSafe = (1 << 5),
  608. IsEncodedPhraseSafe = (1 << 6),
  609. IsEncodedWordSafe = (1 << 7),
  610. IsQuotedPrintableSafe = (1 << 8),
  611. IsSpace = (1 << 9),
  612. IsSpecial = (1 << 10),
  613. IsTokenSpecial = (1 << 11),
  614. IsWhitespace = (1 << 12),
  615. IsXDigit = (1 << 13),
  616. IsPhraseAtom = (1 << 14)
  617. }
  618. internal static class ByteExtensions
  619. {
  620. private const string AtomSafeCharacters = "!#$%&'*+-/=?^_`{|}~";
  621. private const string AttributeSpecials = "*'%"; // attribute specials from rfc2184/rfc2231
  622. private const string DomainSpecials = "[]\\\r \t"; // not allowed in domains
  623. private const string EncodedWordSpecials = "()<>@,;:\"/[]?.=_"; // rfc2047 5.1
  624. private const string EncodedPhraseSpecials = "!*+-/=_"; // rfc2047 5.3
  625. private const string Specials = "()<>[]:;@\\,.\""; // rfc5322 3.2.3
  626. private const string TokenSpecials = "()<>@,;:\\\"/[]?="; // rfc2045 5.1
  627. private const string Whitespace = " \t\r\n";
  628. private static readonly CharType[] table = new CharType[256];
  629. private static void RemoveFlags(string values, CharType bit)
  630. {
  631. for (int i = 0; i < values.Length; i++)
  632. table[(byte) values[i]] &= ~bit;
  633. }
  634. private static void SetFlags(string values, CharType bit, CharType bitcopy, bool remove)
  635. {
  636. int i;
  637. if (remove)
  638. {
  639. for (i = 0; i < 128; i++)
  640. table[i] |= bit;
  641. for (i = 0; i < values.Length; i++)
  642. table[values[i]] &= ~bit;
  643. // Note: not actually used...
  644. //if (bitcopy != CharType.None) {
  645. // for (i = 0; i < 256; i++) {
  646. // if ((table[i] & bitcopy) != 0)
  647. // table[i] &= ~bit;
  648. // }
  649. //}
  650. }
  651. else
  652. {
  653. for (i = 0; i < values.Length; i++)
  654. table[values[i]] |= bit;
  655. if (bitcopy != CharType.None)
  656. {
  657. for (i = 0; i < 256; i++)
  658. {
  659. if ((table[i] & bitcopy) != 0)
  660. table[i] |= bit;
  661. }
  662. }
  663. }
  664. }
  665. static ByteExtensions()
  666. {
  667. for (int i = 0; i < 256; i++)
  668. {
  669. if (i < 127)
  670. {
  671. if (i < 32)
  672. table[i] |= CharType.IsControl;
  673. if (i > 32)
  674. table[i] |= CharType.IsAttrChar;
  675. if ((i >= 33 && i <= 60) || (i >= 62 && i <= 126) || i == 32)
  676. table[i] |= (CharType.IsQuotedPrintableSafe | CharType.IsEncodedWordSafe);
  677. if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z'))
  678. table[i] |= CharType.IsEncodedPhraseSafe | CharType.IsAtom | CharType.IsPhraseAtom;
  679. if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F'))
  680. table[i] |= CharType.IsXDigit;
  681. table[i] |= CharType.IsAscii;
  682. }
  683. else
  684. {
  685. if (i == 127)
  686. table[i] |= CharType.IsAscii;
  687. else
  688. table[i] |= CharType.IsAtom | CharType.IsPhraseAtom;
  689. table[i] |= CharType.IsControl;
  690. }
  691. }
  692. table['\t'] |= CharType.IsQuotedPrintableSafe | CharType.IsBlank;
  693. table[' '] |= CharType.IsSpace | CharType.IsBlank;
  694. SetFlags(Whitespace, CharType.IsWhitespace, CharType.None, false);
  695. SetFlags(AtomSafeCharacters, CharType.IsAtom | CharType.IsPhraseAtom, CharType.None, false);
  696. SetFlags(TokenSpecials, CharType.IsTokenSpecial, CharType.IsControl, false);
  697. SetFlags(Specials, CharType.IsSpecial, CharType.None, false);
  698. SetFlags(DomainSpecials, CharType.IsDomainSafe, CharType.None, true);
  699. RemoveFlags(Specials, CharType.IsAtom | CharType.IsPhraseAtom);
  700. RemoveFlags(EncodedWordSpecials, CharType.IsEncodedWordSafe);
  701. RemoveFlags(AttributeSpecials + TokenSpecials, CharType.IsAttrChar);
  702. SetFlags(EncodedPhraseSpecials, CharType.IsEncodedPhraseSafe, CharType.None, false);
  703. // Note: Allow '[' and ']' in the display-name of a mailbox address
  704. table['['] |= CharType.IsPhraseAtom;
  705. table[']'] |= CharType.IsPhraseAtom;
  706. }
  707. public static bool IsWhitespace(this byte c)
  708. {
  709. return (table[c] & CharType.IsWhitespace) != 0;
  710. }
  711. }