JsonParser.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. using System.Buffers;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Globalization;
  4. using System.Runtime.CompilerServices;
  5. using System.Runtime.InteropServices;
  6. using System.Text;
  7. using Esprima;
  8. using Jint.Native.Object;
  9. using Jint.Pooling;
  10. using Jint.Runtime;
  11. namespace Jint.Native.Json
  12. {
  13. public sealed class JsonParser
  14. {
  15. private readonly Engine _engine;
  16. private readonly int _maxDepth;
  17. /// <summary>
  18. /// Creates a new parser using the recursion depth specified in <see cref="JsonOptions.MaxParseDepth"/>.
  19. /// </summary>
  20. public JsonParser(Engine engine)
  21. : this(engine, engine.Options.Json.MaxParseDepth)
  22. {
  23. }
  24. public JsonParser(Engine engine, int maxDepth)
  25. {
  26. if (maxDepth < 0)
  27. {
  28. throw new ArgumentOutOfRangeException(nameof(maxDepth), $"Max depth must be greater or equal to zero");
  29. }
  30. _maxDepth = maxDepth;
  31. _engine = engine;
  32. // Two tokens are "live" during parsing,
  33. // lookahead and the current one on the stack
  34. // To add a safety boundary to not overwrite
  35. // "still in use" stuff, the buffer contains 5
  36. // instead of 2 tokens.
  37. _tokenBuffer = new Token[5];
  38. for (int i = 0; i < _tokenBuffer.Length; i++)
  39. {
  40. _tokenBuffer[i] = new Token();
  41. }
  42. _tokenBufferIndex = 0;
  43. }
  44. private int _index; // position in the stream
  45. private int _length; // length of the stream
  46. private Token _lookahead = null!;
  47. private string _source = null!;
  48. private readonly Token[] _tokenBuffer;
  49. private int _tokenBufferIndex;
  50. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  51. private static bool IsDecimalDigit(char ch)
  52. {
  53. // * For characters, which are before the '0', the equation will be negative and then wrap
  54. // around because of the unsigned short cast
  55. // * For characters, which are after the '9', the equation will be positive, but > 9
  56. // * For digits, the equation will be between int(0) and int(9)
  57. return ((uint) (ch - '0')) <= 9;
  58. }
  59. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  60. private static bool IsLowerCaseHexAlpha(char ch)
  61. {
  62. return ((uint) (ch - 'a')) <= 5;
  63. }
  64. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  65. private static bool IsUpperCaseHexAlpha(char ch)
  66. {
  67. return ((uint) (ch - 'A')) <= 5;
  68. }
  69. private static bool IsHexDigit(char ch)
  70. {
  71. return
  72. IsDecimalDigit(ch) ||
  73. IsLowerCaseHexAlpha(ch) ||
  74. IsUpperCaseHexAlpha(ch)
  75. ;
  76. }
  77. private static bool IsWhiteSpace(char ch)
  78. {
  79. return (ch == ' ') ||
  80. (ch == '\t') ||
  81. (ch == '\n') ||
  82. (ch == '\r');
  83. }
  84. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  85. private static bool IsLineTerminator(char ch)
  86. {
  87. return (ch == 10) || (ch == 13) || (ch == 0x2028) || (ch == 0x2029);
  88. }
  89. private char ScanHexEscape()
  90. {
  91. int code = char.MinValue;
  92. for (int i = 0; i < 4; ++i)
  93. {
  94. if (_index < _length + 1 && IsHexDigit(_source[_index]))
  95. {
  96. char ch = _source[_index++];
  97. code = code * 16 + "0123456789abcdef".IndexOf(ch);
  98. }
  99. else
  100. {
  101. ThrowError(_index, Messages.ExpectedHexadecimalDigit);
  102. }
  103. }
  104. return (char) code;
  105. }
  106. private char ReadToNextSignificantCharacter()
  107. {
  108. char result = _index < _length ? _source[_index] : char.MinValue;
  109. while (IsWhiteSpace(result))
  110. {
  111. if ((++_index) >= _length)
  112. {
  113. return char.MinValue;
  114. }
  115. result = _source[_index];
  116. }
  117. return result;
  118. }
  119. private Token CreateToken(Tokens type, string text, char firstCharacter, JsValue value, in TextRange range)
  120. {
  121. Token result = _tokenBuffer[_tokenBufferIndex++];
  122. if (_tokenBufferIndex >= _tokenBuffer.Length)
  123. {
  124. _tokenBufferIndex = 0;
  125. }
  126. result.Type = type;
  127. result.Text = text;
  128. result.FirstCharacter = firstCharacter;
  129. result.Value = value;
  130. result.Range = range;
  131. return result;
  132. }
  133. private Token ScanPunctuator()
  134. {
  135. int start = _index;
  136. char code = start < _source.Length ? _source[_index] : char.MinValue;
  137. string value = ScanPunctuatorValue(start, code);
  138. ++_index;
  139. return CreateToken(Tokens.Punctuator, value, code, JsValue.Undefined, new TextRange(start, _index));
  140. }
  141. private string ScanPunctuatorValue(int start, char code)
  142. {
  143. switch (code)
  144. {
  145. case '.': return ".";
  146. case ',': return ",";
  147. case '{': return "{";
  148. case '}': return "}";
  149. case '[': return "[";
  150. case ']': return "]";
  151. case ':': return ":";
  152. default:
  153. ThrowError(start, Messages.UnexpectedToken, code);
  154. return null!;
  155. }
  156. }
  157. private Token ScanNumericLiteral(ref State state)
  158. {
  159. var sb = state.TokenBuffer;
  160. var start = _index;
  161. var ch = _source.CharCodeAt(_index);
  162. var canBeInteger = true;
  163. // Number start with a -
  164. if (ch == '-')
  165. {
  166. sb.Append(ch);
  167. ch = _source.CharCodeAt(++_index);
  168. }
  169. if (ch != '.')
  170. {
  171. var firstCharacter = ch;
  172. sb.Append(ch);
  173. ch = _source.CharCodeAt(++_index);
  174. // Hex number starts with '0x'.
  175. // Octal number starts with '0'.
  176. if (sb.Length == 1 && firstCharacter == '0')
  177. {
  178. canBeInteger = false;
  179. // decimal number starts with '0' such as '09' is illegal.
  180. if (ch > 0 && IsDecimalDigit(ch))
  181. {
  182. ThrowError(_index, Messages.UnexpectedToken, ch);
  183. }
  184. }
  185. while (IsDecimalDigit((ch = _source.CharCodeAt(_index))))
  186. {
  187. sb.Append(ch);
  188. _index++;
  189. }
  190. }
  191. if (ch == '.')
  192. {
  193. canBeInteger = false;
  194. sb.Append(ch);
  195. _index++;
  196. while (IsDecimalDigit((ch = _source.CharCodeAt(_index))))
  197. {
  198. sb.Append(ch);
  199. _index++;
  200. }
  201. }
  202. if (ch is 'e' or 'E')
  203. {
  204. canBeInteger = false;
  205. sb.Append(ch);
  206. ch = _source.CharCodeAt(++_index);
  207. if (ch is '+' or '-')
  208. {
  209. sb.Append(ch);
  210. ch = _source.CharCodeAt(++_index);
  211. }
  212. if (IsDecimalDigit(ch))
  213. {
  214. while (IsDecimalDigit(ch = _source.CharCodeAt(_index)))
  215. {
  216. sb.Append(ch);
  217. _index++;
  218. }
  219. }
  220. else
  221. {
  222. ThrowError(_index, Messages.UnexpectedToken, _source.CharCodeAt(_index));
  223. }
  224. }
  225. var number = sb.ToString();
  226. sb.Clear();
  227. JsNumber value;
  228. if (canBeInteger && long.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longResult) && longResult != -0)
  229. {
  230. value = JsNumber.Create(longResult);
  231. }
  232. else
  233. {
  234. value = new JsNumber(double.Parse(number, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture));
  235. }
  236. return CreateToken(Tokens.Number, number, '\0', value, new TextRange(start, _index));
  237. }
  238. private Token ScanBooleanLiteral()
  239. {
  240. var start = _index;
  241. if (ConsumeMatch("true"))
  242. {
  243. return CreateToken(Tokens.BooleanLiteral, "true", '\t', JsBoolean.True, new TextRange(start, _index));
  244. }
  245. if (ConsumeMatch("false"))
  246. {
  247. return CreateToken(Tokens.BooleanLiteral, "false", '\f', JsBoolean.False, new TextRange(start, _index));
  248. }
  249. ThrowError(start, Messages.UnexpectedTokenIllegal);
  250. return null!;
  251. }
  252. private bool ConsumeMatch(string text)
  253. {
  254. var start = _index;
  255. var length = text.Length;
  256. if (start + length - 1 < _source.Length && _source.AsSpan(start, length).SequenceEqual(text.AsSpan()))
  257. {
  258. _index += length;
  259. return true;
  260. }
  261. return false;
  262. }
  263. private Token ScanNullLiteral()
  264. {
  265. int start = _index;
  266. if (ConsumeMatch("null"))
  267. {
  268. return CreateToken(Tokens.NullLiteral, "null", 'n', JsValue.Null, new TextRange(start, _index));
  269. }
  270. ThrowError(start, Messages.UnexpectedTokenIllegal);
  271. return null!;
  272. }
  273. private Token ScanStringLiteral(ref State state)
  274. {
  275. char quote = _source[_index];
  276. int start = _index;
  277. ++_index;
  278. var sb = state.TokenBuffer;
  279. while (_index < _length)
  280. {
  281. char ch = _source[_index++];
  282. if (ch == quote)
  283. {
  284. quote = char.MinValue;
  285. break;
  286. }
  287. if (ch <= 31)
  288. {
  289. ThrowError(_index - 1, Messages.InvalidCharacter);
  290. }
  291. if (ch == '\\')
  292. {
  293. ch = _source.CharCodeAt(_index++);
  294. switch (ch)
  295. {
  296. case '"':
  297. sb.Append('"');
  298. break;
  299. case '\\':
  300. sb.Append('\\');
  301. break;
  302. case '/':
  303. sb.Append('/');
  304. break;
  305. case 'n':
  306. sb.Append('\n');
  307. break;
  308. case 'r':
  309. sb.Append('\r');
  310. break;
  311. case 't':
  312. sb.Append('\t');
  313. break;
  314. case 'u':
  315. sb.Append(ScanHexEscape());
  316. break;
  317. case 'b':
  318. sb.Append('\b');
  319. break;
  320. case 'f':
  321. sb.Append('\f');
  322. break;
  323. default:
  324. ThrowError(_index - 1, Messages.UnexpectedToken, ch);
  325. break;
  326. }
  327. }
  328. else if (IsLineTerminator(ch))
  329. {
  330. break;
  331. }
  332. else
  333. {
  334. sb.Append(ch);
  335. }
  336. }
  337. if (quote != 0)
  338. {
  339. // unterminated string literal
  340. ThrowError(_index, Messages.UnexpectedEOS);
  341. }
  342. string value = sb.ToString();
  343. sb.Clear();
  344. return CreateToken(Tokens.String, value, '\"', new JsString(value), new TextRange(start, _index));
  345. }
  346. private Token Advance(ref State state)
  347. {
  348. char ch = ReadToNextSignificantCharacter();
  349. if (ch == char.MinValue)
  350. {
  351. return CreateToken(Tokens.EOF, string.Empty, '\0', JsValue.Undefined, new TextRange(_index, _index));
  352. }
  353. // String literal starts with double quote (#34).
  354. // Single quote (#39) are not allowed in JSON.
  355. if (ch == '"')
  356. {
  357. return ScanStringLiteral(ref state);
  358. }
  359. if (ch == '-') // Negative Number
  360. {
  361. if (IsDecimalDigit(_source.CharCodeAt(_index + 1)))
  362. {
  363. return ScanNumericLiteral(ref state);
  364. }
  365. return ScanPunctuator();
  366. }
  367. if (IsDecimalDigit(ch))
  368. {
  369. return ScanNumericLiteral(ref state);
  370. }
  371. if (ch == 't' || ch == 'f')
  372. {
  373. return ScanBooleanLiteral();
  374. }
  375. if (ch == 'n')
  376. {
  377. return ScanNullLiteral();
  378. }
  379. return ScanPunctuator();
  380. }
  381. private Token Lex(ref State state)
  382. {
  383. Token token = _lookahead;
  384. _index = token.Range.End;
  385. _lookahead = Advance(ref state);
  386. _index = token.Range.End;
  387. return token;
  388. }
  389. private void Peek(ref State state)
  390. {
  391. int pos = _index;
  392. _lookahead = Advance(ref state);
  393. _index = pos;
  394. }
  395. [DoesNotReturn]
  396. private void ThrowDepthLimitReached(Token token)
  397. {
  398. ThrowError(token.Range.Start, Messages.MaxDepthLevelReached);
  399. }
  400. [DoesNotReturn]
  401. private void ThrowError(Token token, string messageFormat, params object[] arguments)
  402. {
  403. ThrowError(token.Range.Start, messageFormat, arguments);
  404. }
  405. [DoesNotReturn]
  406. private void ThrowError(int position, string messageFormat, params object[] arguments)
  407. {
  408. var msg = string.Format(CultureInfo.InvariantCulture, messageFormat, arguments);
  409. ExceptionHelper.ThrowSyntaxError(_engine.Realm, $"{msg} at position {position}");
  410. }
  411. // Throw an exception because of the token.
  412. private void ThrowUnexpected(Token token)
  413. {
  414. if (token.Type == Tokens.EOF)
  415. {
  416. ThrowError(token, Messages.UnexpectedEOS);
  417. }
  418. if (token.Type == Tokens.Number)
  419. {
  420. ThrowError(token, Messages.UnexpectedNumber);
  421. }
  422. if (token.Type == Tokens.String)
  423. {
  424. ThrowError(token, Messages.UnexpectedString);
  425. }
  426. // BooleanLiteral, NullLiteral, or Punctuator.
  427. ThrowError(token, Messages.UnexpectedToken, token.Text);
  428. }
  429. // Expect the next token to match the specified punctuator.
  430. // If not, an exception will be thrown.
  431. private void Expect(ref State state, char value)
  432. {
  433. Token token = Lex(ref state);
  434. if (token.Type != Tokens.Punctuator || value != token.FirstCharacter)
  435. {
  436. ThrowUnexpected(token);
  437. }
  438. }
  439. // Return true if the next token matches the specified punctuator.
  440. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  441. public bool Match(char value)
  442. {
  443. return _lookahead.Type == Tokens.Punctuator && value == _lookahead.FirstCharacter;
  444. }
  445. private JsArray ParseJsonArray(ref State state)
  446. {
  447. if ((++state.CurrentDepth) > _maxDepth)
  448. {
  449. ThrowDepthLimitReached(_lookahead);
  450. }
  451. /*
  452. To speed up performance, the list allocation is deferred.
  453. First the elements are stored within an array received
  454. from the .NET array pool.
  455. If a list contains less elements that the size that array,
  456. a Jint array is constructed with the values stored in that
  457. array.
  458. When the number of elements exceed the buffer size,
  459. The elements-array gets created and filled with the content
  460. of the array. The array will then turn into an
  461. intermediate buffer which gets flushed to the list
  462. when its full.
  463. */
  464. List<JsValue>? elements = null;
  465. Expect(ref state, '[');
  466. int bufferIndex = 0;
  467. JsArray? result = null;
  468. JsValue[] buffer = ArrayPool<JsValue>.Shared.Rent(16);
  469. try
  470. {
  471. while (!Match(']'))
  472. {
  473. buffer[bufferIndex++] = ParseJsonValue(ref state);
  474. if (!Match(']'))
  475. {
  476. Expect(ref state, ',');
  477. }
  478. if (bufferIndex >= buffer.Length)
  479. {
  480. if (elements is null)
  481. {
  482. elements = new List<JsValue>(buffer);
  483. }
  484. else
  485. {
  486. elements.AddRange(buffer);
  487. }
  488. bufferIndex = 0;
  489. }
  490. }
  491. // BufferIndex = 0 has two meanings
  492. // * Empty JSON array (elements will be null)
  493. // * The buffer array has just been flushed (elements will NOT be null)
  494. if (bufferIndex > 0)
  495. {
  496. if (elements is null)
  497. {
  498. // No element list has been created, all values did fit into the array.
  499. // The Jint-Array can get constructed from that array.
  500. var data = new JsValue[bufferIndex];
  501. System.Array.Copy(buffer, data, length: bufferIndex);
  502. result = new JsArray(_engine, data);
  503. }
  504. else
  505. {
  506. // An element list has been created. Flush the
  507. // remaining added items within the array to that list.
  508. for (var i = 0; i < bufferIndex; ++i)
  509. {
  510. elements.Add(buffer[i]);
  511. }
  512. }
  513. }
  514. else if (elements is null)
  515. {
  516. // the JSON array did not have any elements
  517. // aka: []
  518. result = new JsArray(_engine);
  519. }
  520. }
  521. finally
  522. {
  523. ArrayPool<JsValue>.Shared.Return(buffer);
  524. }
  525. Expect(ref state, ']');
  526. state.CurrentDepth--;
  527. return result ?? new JsArray(_engine, elements!.ToArray());
  528. }
  529. private JsObject ParseJsonObject(ref State state)
  530. {
  531. if ((++state.CurrentDepth) > _maxDepth)
  532. {
  533. ThrowDepthLimitReached(_lookahead);
  534. }
  535. Expect(ref state, '{');
  536. var obj = new JsObject(_engine);
  537. while (!Match('}'))
  538. {
  539. Tokens type = _lookahead.Type;
  540. if (type != Tokens.String)
  541. {
  542. ThrowUnexpected(Lex(ref state));
  543. }
  544. var nameToken = Lex(ref state);
  545. var name = nameToken.Text;
  546. if (PropertyNameContainsInvalidCharacters(name))
  547. {
  548. ThrowError(nameToken, Messages.InvalidCharacter);
  549. }
  550. Expect(ref state, ':');
  551. var value = ParseJsonValue(ref state);
  552. obj.FastSetDataProperty(name, value);
  553. if (!Match('}'))
  554. {
  555. Expect(ref state, ',');
  556. }
  557. }
  558. Expect(ref state, '}');
  559. state.CurrentDepth--;
  560. return obj;
  561. }
  562. private static bool PropertyNameContainsInvalidCharacters(string propertyName)
  563. {
  564. const char max = (char) 31;
  565. foreach (var c in propertyName)
  566. {
  567. if (c != '\t' && c <= max)
  568. {
  569. return true;
  570. }
  571. }
  572. return false;
  573. }
  574. /// <summary>
  575. /// Optimization.
  576. /// By calling Lex().Value for each type, we parse the token twice.
  577. /// It was already parsed by the peek() method.
  578. /// _lookahead.Value already contain the value.
  579. /// </summary>
  580. private JsValue ParseJsonValue(ref State state)
  581. {
  582. Tokens type = _lookahead.Type;
  583. switch (type)
  584. {
  585. case Tokens.NullLiteral:
  586. case Tokens.BooleanLiteral:
  587. case Tokens.String:
  588. case Tokens.Number:
  589. return Lex(ref state).Value;
  590. case Tokens.Punctuator:
  591. if (_lookahead.FirstCharacter == '[')
  592. {
  593. return ParseJsonArray(ref state);
  594. }
  595. if (_lookahead.FirstCharacter == '{')
  596. {
  597. return ParseJsonObject(ref state);
  598. }
  599. ThrowUnexpected(Lex(ref state));
  600. break;
  601. }
  602. ThrowUnexpected(Lex(ref state));
  603. // can't be reached
  604. return JsValue.Null;
  605. }
  606. public JsValue Parse(string code)
  607. {
  608. return Parse(code, null);
  609. }
  610. public JsValue Parse(string code, ParserOptions? options)
  611. {
  612. _source = code;
  613. _index = 0;
  614. _length = _source.Length;
  615. _lookahead = null!;
  616. using var wrapper = StringBuilderPool.Rent();
  617. State state = new State(wrapper.Builder);
  618. Peek(ref state);
  619. JsValue jsv = ParseJsonValue(ref state);
  620. Peek(ref state);
  621. if (_lookahead.Type != Tokens.EOF)
  622. {
  623. ThrowError(_lookahead, Messages.UnexpectedToken, _lookahead.Text);
  624. }
  625. return jsv;
  626. }
  627. private ref struct State
  628. {
  629. public State(StringBuilder tokenBuffer)
  630. {
  631. TokenBuffer = tokenBuffer;
  632. CurrentDepth = 0;
  633. }
  634. /// <summary>
  635. /// StringBuilder instance which can be used to collect
  636. /// characters into a single string. Must only be used
  637. /// when no child-parser gets called. Must be cleared
  638. /// after usage.
  639. /// </summary>
  640. public StringBuilder TokenBuffer { get; }
  641. /// <summary>
  642. /// The current recursion depth
  643. /// </summary>
  644. public int CurrentDepth { get; set; }
  645. }
  646. private enum Tokens
  647. {
  648. NullLiteral,
  649. BooleanLiteral,
  650. String,
  651. Number,
  652. Punctuator,
  653. EOF,
  654. };
  655. private sealed class Token
  656. {
  657. public Tokens Type;
  658. public char FirstCharacter;
  659. public JsValue Value = JsValue.Undefined;
  660. public string Text = null!;
  661. public TextRange Range;
  662. }
  663. [StructLayout(LayoutKind.Auto)]
  664. private readonly struct TextRange
  665. {
  666. public TextRange(int start, int end)
  667. {
  668. Start = start;
  669. End = end;
  670. }
  671. public int Start { get; }
  672. public int End { get; }
  673. }
  674. static class Messages
  675. {
  676. public const string InvalidCharacter = "Invalid character in JSON";
  677. public const string ExpectedHexadecimalDigit = "Expected hexadecimal digit in JSON";
  678. public const string UnexpectedToken = "Unexpected token '{0}' in JSON";
  679. public const string UnexpectedTokenIllegal = "Unexpected token ILLEGAL in JSON";
  680. public const string UnexpectedNumber = "Unexpected number in JSON";
  681. public const string UnexpectedString = "Unexpected string in JSON";
  682. public const string UnexpectedEOS = "Unexpected end of JSON input";
  683. public const string MaxDepthLevelReached = "Max. depth level of JSON reached";
  684. };
  685. }
  686. internal static class StringExtensions
  687. {
  688. public static char CharCodeAt(this string source, int index)
  689. {
  690. if (index > source.Length - 1)
  691. {
  692. // char.MinValue is used as the null value
  693. return char.MinValue;
  694. }
  695. return source[index];
  696. }
  697. }
  698. }