MimeKit.cs 26 KB

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