Source: util/boost-info-parser.js

  1. /**
  2. * Copyright (C) 2014-2016 Regents of the University of California.
  3. * @author: Jeff Thompson <jefft0@remap.ucla.edu>
  4. * From PyNDN boost_info_parser by Adeola Bannis.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Lesser General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. * A copy of the GNU Lesser General Public License is in the file COPYING.
  19. */
  20. /** @ignore */
  21. var fs = require('fs');
  22. /**
  23. * BoostInfoTree is provided for compatibility with the Boost INFO property list
  24. * format used in ndn-cxx.
  25. *
  26. * Each node in the tree may have a name and a value as well as associated
  27. * sub-trees. The sub-tree names are not unique, and so sub-trees are stored as
  28. * dictionaries where the key is a sub-tree name and the values are the
  29. * sub-trees sharing the same name.
  30. *
  31. * Nodes can be accessed with a path syntax, as long as nodes in the path do not
  32. * contain the path separator '/' in their names.
  33. * @constructor
  34. */
  35. var BoostInfoTree = function BoostInfoTree(value, parent)
  36. {
  37. // subtrees is an array of {key: treeName, value: subtreeList} where
  38. // treeName is a string and subtreeList is an array of BoostInfoTree.
  39. // We can't use a dictionary because we want the keys to be in order.
  40. this.subtrees = [];
  41. this.value = value;
  42. this.parent = parent;
  43. this.lastChild = null;
  44. };
  45. /**
  46. * Insert a BoostInfoTree as a sub-tree with the given name.
  47. * @param {string} treeName The name of the new sub-tree.
  48. * @param {BoostInfoTree} newTree The sub-tree to add.
  49. */
  50. BoostInfoTree.prototype.addSubtree = function(treeName, newTree)
  51. {
  52. var subtreeList = this.find(treeName);
  53. if (subtreeList !== null)
  54. subtreeList.push(newTree);
  55. else
  56. this.subtrees.push({key: treeName, value: [newTree]});
  57. newTree.parent = this;
  58. this.lastChild = newTree;
  59. };
  60. /**
  61. * Create a new BoostInfo and insert it as a sub-tree with the given name.
  62. * @param {string} treeName The name of the new sub-tree.
  63. * @param {string} value The value associated with the new sub-tree.
  64. * @returns {BoostInfoTree} The created sub-tree.
  65. */
  66. BoostInfoTree.prototype.createSubtree = function(treeName, value)
  67. {
  68. var newTree = new BoostInfoTree(value, this);
  69. this.addSubtree(treeName, newTree);
  70. return newTree;
  71. };
  72. /**
  73. * Look up using the key and return a list of the subtrees.
  74. * @param {string} key The key which may be a path separated with '/'.
  75. * @returns {Array<BoostInfoTree>} A new array with pointers to the subtrees.
  76. */
  77. BoostInfoTree.prototype.get = function(key)
  78. {
  79. // Strip beginning '/'.
  80. key = key.replace(/^\/+/, "");
  81. if (key.length === 0)
  82. return [this];
  83. var path = key.split('/');
  84. var subtrees = this.find(path[0]);
  85. if (subtrees === null)
  86. return [];
  87. if (path.length == 1)
  88. return subtrees.slice(0);
  89. var newPath = path.slice(1).join('/');
  90. var foundVals = [];
  91. for (var i = 0; i < subtrees.length; ++i) {
  92. var t = subtrees[i];
  93. var partial = t.get(newPath);
  94. foundVals = foundVals.concat(partial);
  95. }
  96. return foundVals;
  97. };
  98. /**
  99. * Look up using the key and return string value of the first subtree.
  100. * @param {string} key The key which may be a path separated with '/'.
  101. * @returns {string} The string value or null if not found.
  102. */
  103. BoostInfoTree.prototype.getFirstValue = function(key)
  104. {
  105. var list = this.get(key);
  106. if (list.length >= 1)
  107. return list[0].value;
  108. else
  109. return null;
  110. };
  111. BoostInfoTree.prototype.getValue = function() { return this.value; };
  112. BoostInfoTree.prototype.getParent = function() { return this.parent; };
  113. BoostInfoTree.prototype.getLastChild = function() { return this.lastChild; };
  114. BoostInfoTree.prototype.prettyPrint = function(indentLevel)
  115. {
  116. indentLevel = indentLevel || 1;
  117. var prefix = Array(indentLevel + 1).join(' ');
  118. var s = "";
  119. if (this.parent != null) {
  120. if (this.value && this.value.length > 0)
  121. s += "\"" + this.value + "\"";
  122. s += "\n";
  123. }
  124. if (this.subtrees.length > 0) {
  125. if (this.parent)
  126. s += prefix + "{\n";
  127. var nextLevel = Array(indentLevel + 2 + 1).join(' ');
  128. for (var i = 0; i < this.subtrees.length; ++i) {
  129. for (var iSubTree = 0; iSubTree < this.subtrees[i].value.length; ++iSubTree)
  130. s += nextLevel + this.subtrees[i].key + " " +
  131. this.subtrees[i].value[iSubTree].prettyPrint(indentLevel + 2);
  132. }
  133. if (this.parent)
  134. s += prefix + "}\n";
  135. }
  136. return s;
  137. };
  138. BoostInfoTree.prototype.toString = function()
  139. {
  140. return this.prettyPrint();
  141. };
  142. /**
  143. * Use treeName to find the array of BoostInfoTree in this.subtrees.
  144. * @param {string} treeName The key in this.subtrees to search for. This does a
  145. * flat search in subtrees_. It does not split by '/' into a path.
  146. * @returns {Array<BoostInfoTree>} A array of BoostInfoTree, or null if not found.
  147. */
  148. BoostInfoTree.prototype.find = function(treeName)
  149. {
  150. for (var i = 0; i < this.subtrees.length; ++i) {
  151. if (this.subtrees[i].key == treeName)
  152. return this.subtrees[i].value;
  153. }
  154. return null;
  155. };
  156. /**
  157. * A BoostInfoParser reads files in Boost's INFO format and constructs a
  158. * BoostInfoTree.
  159. * @constructor
  160. */
  161. var BoostInfoParser = function BoostInfoParser()
  162. {
  163. this.root = new BoostInfoTree();
  164. };
  165. exports.BoostInfoParser = BoostInfoParser;
  166. exports.BoostInfoTree = BoostInfoTree; // debug
  167. /**
  168. * Add the contents of the file or input string to the root BoostInfoTree. There
  169. * are two forms:
  170. * read(fileName) reads fileName from the file system.
  171. * read(input, inputName) reads from the input, in which case inputName is used
  172. * only for log messages, etc.
  173. * @param {string} fileName The path to the INFO file.
  174. * @param {string} input The contents of the INFO file, with lines separated by
  175. * "\n" or "\r\n".
  176. * @param {string} inputName Use with input for log messages, etc.
  177. */
  178. BoostInfoParser.prototype.read = function(fileNameOrInput, inputName)
  179. {
  180. var input;
  181. if (typeof inputName == 'string')
  182. input = fileNameOrInput;
  183. else {
  184. // No inputName, so assume the first arg is the file name.
  185. var fileName = fileNameOrInput;
  186. inputName = fileName;
  187. input = fs.readFileSync(fileName).toString();
  188. }
  189. var ctx = this.root;
  190. var thisParser = this;
  191. input.split(/\r?\n/).forEach(function(line) {
  192. ctx = thisParser.parseLine(line.trim(), ctx);
  193. });
  194. };
  195. /**
  196. * Write the root tree of this BoostInfoParser as file in Boost's INFO format.
  197. * @param {string} fileName The output path.
  198. */
  199. BoostInfoParser.prototype.write = function(fileName)
  200. {
  201. fs.writeFileSync(fileName, "" + this.root);
  202. };
  203. /**
  204. * Get the root tree of this parser.
  205. * @return {BoostInfoTree} The root BoostInfoTree.
  206. */
  207. BoostInfoParser.prototype.getRoot = function() { return this.root; };
  208. /**
  209. * Similar to Python's shlex.split, split s into an array of strings which are
  210. * separated by whitespace, treating a string within quotes as a single entity
  211. * regardless of whitespace between the quotes. Also allow a backslash to escape
  212. * the next character.
  213. * @param {string} s The input string to split.
  214. * @returns {Array<string>} An array of strings.
  215. */
  216. BoostInfoParser.shlex_split = function(s)
  217. {
  218. var result = [];
  219. if (s == "")
  220. return result;
  221. var whiteSpace = " \t\n\r";
  222. var iStart = 0;
  223. while (true) {
  224. // Move iStart past whitespace.
  225. while (whiteSpace.indexOf(s[iStart]) >= 0) {
  226. iStart += 1;
  227. if (iStart >= s.length)
  228. // Done.
  229. return result;
  230. }
  231. // Move iEnd to the end of the token.
  232. var iEnd = iStart;
  233. var inQuotation = false;
  234. var token = "";
  235. while (true) {
  236. if (s[iEnd] == '\\') {
  237. // Append characters up to the backslash, skip the backslash and
  238. // move iEnd past the escaped character.
  239. token += s.substring(iStart, iEnd);
  240. iStart = iEnd + 1;
  241. iEnd = iStart;
  242. if (iEnd >= s.length)
  243. // An unusual case: A backslash at the end of the string.
  244. break;
  245. }
  246. else {
  247. if (inQuotation) {
  248. if (s[iEnd] == '\"') {
  249. // Append characters up to the end quote and skip.
  250. token += s.substring(iStart, iEnd);
  251. iStart = iEnd + 1;
  252. inQuotation = false;
  253. }
  254. }
  255. else {
  256. if (s[iEnd] == '\"') {
  257. // Append characters up to the start quote and skip.
  258. token += s.substring(iStart, iEnd);
  259. iStart = iEnd + 1;
  260. inQuotation = true;
  261. }
  262. else
  263. if (whiteSpace.indexOf(s[iEnd]) >= 0)
  264. break;
  265. }
  266. }
  267. iEnd += 1;
  268. if (iEnd >= s.length)
  269. break;
  270. }
  271. token += s.substring(iStart, iEnd);
  272. result.push(token);
  273. if (iEnd >= s.length)
  274. // Done.
  275. return result;
  276. iStart = iEnd;
  277. }
  278. };
  279. BoostInfoParser.prototype.parseLine = function(line, context)
  280. {
  281. // Skip blank lines and comments.
  282. var commentStart = line.indexOf(';');
  283. if (commentStart >= 0)
  284. line = line.substring(0, commentStart).trim();
  285. if (line.length == 0)
  286. return context;
  287. // Usually we are expecting key and optional value.
  288. var strings = BoostInfoParser.shlex_split(line);
  289. var isSectionStart = false;
  290. var isSectionEnd = false;
  291. for (var i = 0; i < strings.length; ++i) {
  292. isSectionStart = (isSectionStart || strings[i] == "{");
  293. isSectionEnd = (isSectionEnd || strings[i] == "}");
  294. }
  295. if (!isSectionStart && !isSectionEnd) {
  296. var key = strings[0];
  297. var val;
  298. if (strings.length > 1)
  299. val = strings[1];
  300. context.createSubtree(key, val);
  301. return context;
  302. }
  303. // OK, who is the joker who put a { on the same line as the key name?!
  304. var sectionStart = line.indexOf('{');
  305. if (sectionStart > 0) {
  306. var firstPart = line.substring(0, sectionStart);
  307. var secondPart = line.substring(sectionStart);
  308. var ctx = this.parseLine(firstPart, context);
  309. return this.parseLine(secondPart, ctx);
  310. }
  311. // If we encounter a {, we are beginning a new context.
  312. // TODO: Error if there was already a subcontext here.
  313. if (line[0] == '{') {
  314. context = context.getLastChild();
  315. return context;
  316. }
  317. // If we encounter a }, we are ending a list context.
  318. if (line[0] == '}') {
  319. context = context.getParent();
  320. return context;
  321. }
  322. throw runtime_error("BoostInfoParser: input line is malformed");
  323. };