You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

892 lines
29 KiB

  1. #region License
  2. // Copyright (c) 2007 James Newton-King
  3. //
  4. // Permission is hereby granted, free of charge, to any person
  5. // obtaining a copy of this software and associated documentation
  6. // files (the "Software"), to deal in the Software without
  7. // restriction, including without limitation the rights to use,
  8. // copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following
  11. // conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be
  14. // included in all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18. // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23. // OTHER DEALINGS IN THE SOFTWARE.
  24. #endregion
  25. using System;
  26. using System.Collections.Generic;
  27. using System.Globalization;
  28. using System.Text;
  29. using Newtonsoft.Json.Utilities;
  30. namespace Newtonsoft.Json.Linq.JsonPath
  31. {
  32. internal class JPath
  33. {
  34. private static readonly char[] FloatCharacters = new[] {'.', 'E', 'e'};
  35. private readonly string _expression;
  36. public List<PathFilter> Filters { get; }
  37. private int _currentIndex;
  38. public JPath(string expression)
  39. {
  40. ValidationUtils.ArgumentNotNull(expression, nameof(expression));
  41. _expression = expression;
  42. Filters = new List<PathFilter>();
  43. ParseMain();
  44. }
  45. private void ParseMain()
  46. {
  47. int currentPartStartIndex = _currentIndex;
  48. EatWhitespace();
  49. if (_expression.Length == _currentIndex)
  50. {
  51. return;
  52. }
  53. if (_expression[_currentIndex] == '$')
  54. {
  55. if (_expression.Length == 1)
  56. {
  57. return;
  58. }
  59. // only increment position for "$." or "$["
  60. // otherwise assume property that starts with $
  61. char c = _expression[_currentIndex + 1];
  62. if (c == '.' || c == '[')
  63. {
  64. _currentIndex++;
  65. currentPartStartIndex = _currentIndex;
  66. }
  67. }
  68. if (!ParsePath(Filters, currentPartStartIndex, false))
  69. {
  70. int lastCharacterIndex = _currentIndex;
  71. EatWhitespace();
  72. if (_currentIndex < _expression.Length)
  73. {
  74. throw new JsonException("Unexpected character while parsing path: " + _expression[lastCharacterIndex]);
  75. }
  76. }
  77. }
  78. private bool ParsePath(List<PathFilter> filters, int currentPartStartIndex, bool query)
  79. {
  80. bool scan = false;
  81. bool followingIndexer = false;
  82. bool followingDot = false;
  83. bool ended = false;
  84. while (_currentIndex < _expression.Length && !ended)
  85. {
  86. char currentChar = _expression[_currentIndex];
  87. switch (currentChar)
  88. {
  89. case '[':
  90. case '(':
  91. if (_currentIndex > currentPartStartIndex)
  92. {
  93. string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
  94. if (member == "*")
  95. {
  96. member = null;
  97. }
  98. filters.Add(CreatePathFilter(member, scan));
  99. scan = false;
  100. }
  101. filters.Add(ParseIndexer(currentChar, scan));
  102. _currentIndex++;
  103. currentPartStartIndex = _currentIndex;
  104. followingIndexer = true;
  105. followingDot = false;
  106. break;
  107. case ']':
  108. case ')':
  109. ended = true;
  110. break;
  111. case ' ':
  112. if (_currentIndex < _expression.Length)
  113. {
  114. ended = true;
  115. }
  116. break;
  117. case '.':
  118. if (_currentIndex > currentPartStartIndex)
  119. {
  120. string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
  121. if (member == "*")
  122. {
  123. member = null;
  124. }
  125. filters.Add(CreatePathFilter(member, scan));
  126. scan = false;
  127. }
  128. if (_currentIndex + 1 < _expression.Length && _expression[_currentIndex + 1] == '.')
  129. {
  130. scan = true;
  131. _currentIndex++;
  132. }
  133. _currentIndex++;
  134. currentPartStartIndex = _currentIndex;
  135. followingIndexer = false;
  136. followingDot = true;
  137. break;
  138. default:
  139. if (query && (currentChar == '=' || currentChar == '<' || currentChar == '!' || currentChar == '>' || currentChar == '|' || currentChar == '&'))
  140. {
  141. ended = true;
  142. }
  143. else
  144. {
  145. if (followingIndexer)
  146. {
  147. throw new JsonException("Unexpected character following indexer: " + currentChar);
  148. }
  149. _currentIndex++;
  150. }
  151. break;
  152. }
  153. }
  154. bool atPathEnd = (_currentIndex == _expression.Length);
  155. if (_currentIndex > currentPartStartIndex)
  156. {
  157. string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex).TrimEnd();
  158. if (member == "*")
  159. {
  160. member = null;
  161. }
  162. filters.Add(CreatePathFilter(member, scan));
  163. }
  164. else
  165. {
  166. // no field name following dot in path and at end of base path/query
  167. if (followingDot && (atPathEnd || query))
  168. {
  169. throw new JsonException("Unexpected end while parsing path.");
  170. }
  171. }
  172. return atPathEnd;
  173. }
  174. private static PathFilter CreatePathFilter(string member, bool scan)
  175. {
  176. PathFilter filter = (scan) ? (PathFilter)new ScanFilter {Name = member} : new FieldFilter {Name = member};
  177. return filter;
  178. }
  179. private PathFilter ParseIndexer(char indexerOpenChar, bool scan)
  180. {
  181. _currentIndex++;
  182. char indexerCloseChar = (indexerOpenChar == '[') ? ']' : ')';
  183. EnsureLength("Path ended with open indexer.");
  184. EatWhitespace();
  185. if (_expression[_currentIndex] == '\'')
  186. {
  187. return ParseQuotedField(indexerCloseChar, scan);
  188. }
  189. else if (_expression[_currentIndex] == '?')
  190. {
  191. return ParseQuery(indexerCloseChar, scan);
  192. }
  193. else
  194. {
  195. return ParseArrayIndexer(indexerCloseChar);
  196. }
  197. }
  198. private PathFilter ParseArrayIndexer(char indexerCloseChar)
  199. {
  200. int start = _currentIndex;
  201. int? end = null;
  202. List<int> indexes = null;
  203. int colonCount = 0;
  204. int? startIndex = null;
  205. int? endIndex = null;
  206. int? step = null;
  207. while (_currentIndex < _expression.Length)
  208. {
  209. char currentCharacter = _expression[_currentIndex];
  210. if (currentCharacter == ' ')
  211. {
  212. end = _currentIndex;
  213. EatWhitespace();
  214. continue;
  215. }
  216. if (currentCharacter == indexerCloseChar)
  217. {
  218. int length = (end ?? _currentIndex) - start;
  219. if (indexes != null)
  220. {
  221. if (length == 0)
  222. {
  223. throw new JsonException("Array index expected.");
  224. }
  225. string indexer = _expression.Substring(start, length);
  226. int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture);
  227. indexes.Add(index);
  228. return new ArrayMultipleIndexFilter { Indexes = indexes };
  229. }
  230. else if (colonCount > 0)
  231. {
  232. if (length > 0)
  233. {
  234. string indexer = _expression.Substring(start, length);
  235. int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture);
  236. if (colonCount == 1)
  237. {
  238. endIndex = index;
  239. }
  240. else
  241. {
  242. step = index;
  243. }
  244. }
  245. return new ArraySliceFilter { Start = startIndex, End = endIndex, Step = step };
  246. }
  247. else
  248. {
  249. if (length == 0)
  250. {
  251. throw new JsonException("Array index expected.");
  252. }
  253. string indexer = _expression.Substring(start, length);
  254. int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture);
  255. return new ArrayIndexFilter { Index = index };
  256. }
  257. }
  258. else if (currentCharacter == ',')
  259. {
  260. int length = (end ?? _currentIndex) - start;
  261. if (length == 0)
  262. {
  263. throw new JsonException("Array index expected.");
  264. }
  265. if (indexes == null)
  266. {
  267. indexes = new List<int>();
  268. }
  269. string indexer = _expression.Substring(start, length);
  270. indexes.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture));
  271. _currentIndex++;
  272. EatWhitespace();
  273. start = _currentIndex;
  274. end = null;
  275. }
  276. else if (currentCharacter == '*')
  277. {
  278. _currentIndex++;
  279. EnsureLength("Path ended with open indexer.");
  280. EatWhitespace();
  281. if (_expression[_currentIndex] != indexerCloseChar)
  282. {
  283. throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter);
  284. }
  285. return new ArrayIndexFilter();
  286. }
  287. else if (currentCharacter == ':')
  288. {
  289. int length = (end ?? _currentIndex) - start;
  290. if (length > 0)
  291. {
  292. string indexer = _expression.Substring(start, length);
  293. int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture);
  294. if (colonCount == 0)
  295. {
  296. startIndex = index;
  297. }
  298. else if (colonCount == 1)
  299. {
  300. endIndex = index;
  301. }
  302. else
  303. {
  304. step = index;
  305. }
  306. }
  307. colonCount++;
  308. _currentIndex++;
  309. EatWhitespace();
  310. start = _currentIndex;
  311. end = null;
  312. }
  313. else if (!char.IsDigit(currentCharacter) && currentCharacter != '-')
  314. {
  315. throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter);
  316. }
  317. else
  318. {
  319. if (end != null)
  320. {
  321. throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter);
  322. }
  323. _currentIndex++;
  324. }
  325. }
  326. throw new JsonException("Path ended with open indexer.");
  327. }
  328. private void EatWhitespace()
  329. {
  330. while (_currentIndex < _expression.Length)
  331. {
  332. if (_expression[_currentIndex] != ' ')
  333. {
  334. break;
  335. }
  336. _currentIndex++;
  337. }
  338. }
  339. private PathFilter ParseQuery(char indexerCloseChar, bool scan)
  340. {
  341. _currentIndex++;
  342. EnsureLength("Path ended with open indexer.");
  343. if (_expression[_currentIndex] != '(')
  344. {
  345. throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
  346. }
  347. _currentIndex++;
  348. QueryExpression expression = ParseExpression();
  349. _currentIndex++;
  350. EnsureLength("Path ended with open indexer.");
  351. EatWhitespace();
  352. if (_expression[_currentIndex] != indexerCloseChar)
  353. {
  354. throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
  355. }
  356. if (!scan)
  357. {
  358. return new QueryFilter
  359. {
  360. Expression = expression
  361. };
  362. }
  363. else
  364. {
  365. return new QueryScanFilter
  366. {
  367. Expression = expression
  368. };
  369. }
  370. }
  371. private bool TryParseExpression(out List<PathFilter> expressionPath)
  372. {
  373. if (_expression[_currentIndex] == '$')
  374. {
  375. expressionPath = new List<PathFilter>();
  376. expressionPath.Add(RootFilter.Instance);
  377. }
  378. else if (_expression[_currentIndex] == '@')
  379. {
  380. expressionPath = new List<PathFilter>();
  381. }
  382. else
  383. {
  384. expressionPath = null;
  385. return false;
  386. }
  387. _currentIndex++;
  388. if (ParsePath(expressionPath, _currentIndex, true))
  389. {
  390. throw new JsonException("Path ended with open query.");
  391. }
  392. return true;
  393. }
  394. private JsonException CreateUnexpectedCharacterException()
  395. {
  396. return new JsonException("Unexpected character while parsing path query: " + _expression[_currentIndex]);
  397. }
  398. private object ParseSide()
  399. {
  400. EatWhitespace();
  401. if (TryParseExpression(out var expressionPath))
  402. {
  403. EatWhitespace();
  404. EnsureLength("Path ended with open query.");
  405. return expressionPath;
  406. }
  407. if (TryParseValue(out var value))
  408. {
  409. EatWhitespace();
  410. EnsureLength("Path ended with open query.");
  411. return new JValue(value);
  412. }
  413. throw CreateUnexpectedCharacterException();
  414. }
  415. private QueryExpression ParseExpression()
  416. {
  417. QueryExpression rootExpression = null;
  418. CompositeExpression parentExpression = null;
  419. while (_currentIndex < _expression.Length)
  420. {
  421. object left = ParseSide();
  422. object right = null;
  423. QueryOperator op;
  424. if (_expression[_currentIndex] == ')'
  425. || _expression[_currentIndex] == '|'
  426. || _expression[_currentIndex] == '&')
  427. {
  428. op = QueryOperator.Exists;
  429. }
  430. else
  431. {
  432. op = ParseOperator();
  433. right = ParseSide();
  434. }
  435. BooleanQueryExpression booleanExpression = new BooleanQueryExpression
  436. {
  437. Left = left,
  438. Operator = op,
  439. Right = right
  440. };
  441. if (_expression[_currentIndex] == ')')
  442. {
  443. if (parentExpression != null)
  444. {
  445. parentExpression.Expressions.Add(booleanExpression);
  446. return rootExpression;
  447. }
  448. return booleanExpression;
  449. }
  450. if (_expression[_currentIndex] == '&')
  451. {
  452. if (!Match("&&"))
  453. {
  454. throw CreateUnexpectedCharacterException();
  455. }
  456. if (parentExpression == null || parentExpression.Operator != QueryOperator.And)
  457. {
  458. CompositeExpression andExpression = new CompositeExpression { Operator = QueryOperator.And };
  459. parentExpression?.Expressions.Add(andExpression);
  460. parentExpression = andExpression;
  461. if (rootExpression == null)
  462. {
  463. rootExpression = parentExpression;
  464. }
  465. }
  466. parentExpression.Expressions.Add(booleanExpression);
  467. }
  468. if (_expression[_currentIndex] == '|')
  469. {
  470. if (!Match("||"))
  471. {
  472. throw CreateUnexpectedCharacterException();
  473. }
  474. if (parentExpression == null || parentExpression.Operator != QueryOperator.Or)
  475. {
  476. CompositeExpression orExpression = new CompositeExpression { Operator = QueryOperator.Or };
  477. parentExpression?.Expressions.Add(orExpression);
  478. parentExpression = orExpression;
  479. if (rootExpression == null)
  480. {
  481. rootExpression = parentExpression;
  482. }
  483. }
  484. parentExpression.Expressions.Add(booleanExpression);
  485. }
  486. }
  487. throw new JsonException("Path ended with open query.");
  488. }
  489. private bool TryParseValue(out object value)
  490. {
  491. char currentChar = _expression[_currentIndex];
  492. if (currentChar == '\'')
  493. {
  494. value = ReadQuotedString();
  495. return true;
  496. }
  497. else if (char.IsDigit(currentChar) || currentChar == '-')
  498. {
  499. StringBuilder sb = new StringBuilder();
  500. sb.Append(currentChar);
  501. _currentIndex++;
  502. while (_currentIndex < _expression.Length)
  503. {
  504. currentChar = _expression[_currentIndex];
  505. if (currentChar == ' ' || currentChar == ')')
  506. {
  507. string numberText = sb.ToString();
  508. if (numberText.IndexOfAny(FloatCharacters) != -1)
  509. {
  510. bool result = double.TryParse(numberText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var d);
  511. value = d;
  512. return result;
  513. }
  514. else
  515. {
  516. bool result = long.TryParse(numberText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l);
  517. value = l;
  518. return result;
  519. }
  520. }
  521. else
  522. {
  523. sb.Append(currentChar);
  524. _currentIndex++;
  525. }
  526. }
  527. }
  528. else if (currentChar == 't')
  529. {
  530. if (Match("true"))
  531. {
  532. value = true;
  533. return true;
  534. }
  535. }
  536. else if (currentChar == 'f')
  537. {
  538. if (Match("false"))
  539. {
  540. value = false;
  541. return true;
  542. }
  543. }
  544. else if (currentChar == 'n')
  545. {
  546. if (Match("null"))
  547. {
  548. value = null;
  549. return true;
  550. }
  551. }
  552. else if (currentChar == '/')
  553. {
  554. value = ReadRegexString();
  555. return true;
  556. }
  557. value = null;
  558. return false;
  559. }
  560. private string ReadQuotedString()
  561. {
  562. StringBuilder sb = new StringBuilder();
  563. _currentIndex++;
  564. while (_currentIndex < _expression.Length)
  565. {
  566. char currentChar = _expression[_currentIndex];
  567. if (currentChar == '\\' && _currentIndex + 1 < _expression.Length)
  568. {
  569. _currentIndex++;
  570. currentChar = _expression[_currentIndex];
  571. char resolvedChar;
  572. switch (currentChar)
  573. {
  574. case 'b':
  575. resolvedChar = '\b';
  576. break;
  577. case 't':
  578. resolvedChar = '\t';
  579. break;
  580. case 'n':
  581. resolvedChar = '\n';
  582. break;
  583. case 'f':
  584. resolvedChar = '\f';
  585. break;
  586. case 'r':
  587. resolvedChar = '\r';
  588. break;
  589. case '\\':
  590. case '"':
  591. case '\'':
  592. case '/':
  593. resolvedChar = currentChar;
  594. break;
  595. default:
  596. throw new JsonException(@"Unknown escape character: \" + currentChar);
  597. }
  598. sb.Append(resolvedChar);
  599. _currentIndex++;
  600. }
  601. else if (currentChar == '\'')
  602. {
  603. _currentIndex++;
  604. return sb.ToString();
  605. }
  606. else
  607. {
  608. _currentIndex++;
  609. sb.Append(currentChar);
  610. }
  611. }
  612. throw new JsonException("Path ended with an open string.");
  613. }
  614. private string ReadRegexString()
  615. {
  616. int startIndex = _currentIndex;
  617. _currentIndex++;
  618. while (_currentIndex < _expression.Length)
  619. {
  620. char currentChar = _expression[_currentIndex];
  621. // handle escaped / character
  622. if (currentChar == '\\' && _currentIndex + 1 < _expression.Length)
  623. {
  624. _currentIndex += 2;
  625. }
  626. else if (currentChar == '/')
  627. {
  628. _currentIndex++;
  629. while (_currentIndex < _expression.Length)
  630. {
  631. currentChar = _expression[_currentIndex];
  632. if (char.IsLetter(currentChar))
  633. {
  634. _currentIndex++;
  635. }
  636. else
  637. {
  638. break;
  639. }
  640. }
  641. return _expression.Substring(startIndex, _currentIndex - startIndex);
  642. }
  643. else
  644. {
  645. _currentIndex++;
  646. }
  647. }
  648. throw new JsonException("Path ended with an open regex.");
  649. }
  650. private bool Match(string s)
  651. {
  652. int currentPosition = _currentIndex;
  653. foreach (char c in s)
  654. {
  655. if (currentPosition < _expression.Length && _expression[currentPosition] == c)
  656. {
  657. currentPosition++;
  658. }
  659. else
  660. {
  661. return false;
  662. }
  663. }
  664. _currentIndex = currentPosition;
  665. return true;
  666. }
  667. private QueryOperator ParseOperator()
  668. {
  669. if (_currentIndex + 1 >= _expression.Length)
  670. {
  671. throw new JsonException("Path ended with open query.");
  672. }
  673. if (Match("=="))
  674. {
  675. return QueryOperator.Equals;
  676. }
  677. if (Match("=~"))
  678. {
  679. return QueryOperator.RegexEquals;
  680. }
  681. if (Match("!=") || Match("<>"))
  682. {
  683. return QueryOperator.NotEquals;
  684. }
  685. if (Match("<="))
  686. {
  687. return QueryOperator.LessThanOrEquals;
  688. }
  689. if (Match("<"))
  690. {
  691. return QueryOperator.LessThan;
  692. }
  693. if (Match(">="))
  694. {
  695. return QueryOperator.GreaterThanOrEquals;
  696. }
  697. if (Match(">"))
  698. {
  699. return QueryOperator.GreaterThan;
  700. }
  701. throw new JsonException("Could not read query operator.");
  702. }
  703. private PathFilter ParseQuotedField(char indexerCloseChar, bool scan)
  704. {
  705. List<string> fields = null;
  706. while (_currentIndex < _expression.Length)
  707. {
  708. string field = ReadQuotedString();
  709. EatWhitespace();
  710. EnsureLength("Path ended with open indexer.");
  711. if (_expression[_currentIndex] == indexerCloseChar)
  712. {
  713. if (fields != null)
  714. {
  715. fields.Add(field);
  716. return (scan)
  717. ? (PathFilter)new ScanMultipleFilter { Names = fields }
  718. : (PathFilter)new FieldMultipleFilter { Names = fields };
  719. }
  720. else
  721. {
  722. return CreatePathFilter(field, scan);
  723. }
  724. }
  725. else if (_expression[_currentIndex] == ',')
  726. {
  727. _currentIndex++;
  728. EatWhitespace();
  729. if (fields == null)
  730. {
  731. fields = new List<string>();
  732. }
  733. fields.Add(field);
  734. }
  735. else
  736. {
  737. throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
  738. }
  739. }
  740. throw new JsonException("Path ended with open indexer.");
  741. }
  742. private void EnsureLength(string message)
  743. {
  744. if (_currentIndex >= _expression.Length)
  745. {
  746. throw new JsonException(message);
  747. }
  748. }
  749. internal IEnumerable<JToken> Evaluate(JToken root, JToken t, bool errorWhenNoMatch)
  750. {
  751. return Evaluate(Filters, root, t, errorWhenNoMatch);
  752. }
  753. internal static IEnumerable<JToken> Evaluate(List<PathFilter> filters, JToken root, JToken t, bool errorWhenNoMatch)
  754. {
  755. IEnumerable<JToken> current = new[] { t };
  756. foreach (PathFilter filter in filters)
  757. {
  758. current = filter.ExecuteFilter(root, current, errorWhenNoMatch);
  759. }
  760. return current;
  761. }
  762. }
  763. }