Utils.mjs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215
  1. /**
  2. * @author n1474335 [n1474335@gmail.com]
  3. * @copyright Crown Copyright 2016
  4. * @license Apache-2.0
  5. */
  6. import utf8 from "utf8";
  7. import moment from "moment-timezone";
  8. import {fromBase64} from "./lib/Base64";
  9. import {fromHex} from "./lib/Hex";
  10. /**
  11. * Utility functions for use in operations, the core framework and the stage.
  12. */
  13. class Utils {
  14. /**
  15. * Translates an ordinal into a character.
  16. *
  17. * @param {number} o
  18. * @returns {char}
  19. *
  20. * @example
  21. * // returns 'a'
  22. * Utils.chr(97);
  23. */
  24. static chr(o) {
  25. // Detect astral symbols
  26. // Thanks to @mathiasbynens for this solution
  27. // https://mathiasbynens.be/notes/javascript-unicode
  28. if (o > 0xffff) {
  29. o -= 0x10000;
  30. const high = String.fromCharCode(o >>> 10 & 0x3ff | 0xd800);
  31. o = 0xdc00 | o & 0x3ff;
  32. return high + String.fromCharCode(o);
  33. }
  34. return String.fromCharCode(o);
  35. }
  36. /**
  37. * Translates a character into an ordinal.
  38. *
  39. * @param {char} c
  40. * @returns {number}
  41. *
  42. * @example
  43. * // returns 97
  44. * Utils.ord('a');
  45. */
  46. static ord(c) {
  47. // Detect astral symbols
  48. // Thanks to @mathiasbynens for this solution
  49. // https://mathiasbynens.be/notes/javascript-unicode
  50. if (c.length === 2) {
  51. const high = c.charCodeAt(0);
  52. const low = c.charCodeAt(1);
  53. if (high >= 0xd800 && high < 0xdc00 &&
  54. low >= 0xdc00 && low < 0xe000) {
  55. return (high - 0xd800) * 0x400 + low - 0xdc00 + 0x10000;
  56. }
  57. }
  58. return c.charCodeAt(0);
  59. }
  60. /**
  61. * Adds trailing bytes to a byteArray.
  62. *
  63. * @author tlwr [toby@toby.codes]
  64. *
  65. * @param {byteArray} arr - byteArray to add trailing bytes to.
  66. * @param {number} numBytes - Maximum width of the array.
  67. * @param {Integer} [padByte=0] - The byte to pad with.
  68. * @returns {byteArray}
  69. *
  70. * @example
  71. * // returns ["a", 0, 0, 0]
  72. * Utils.padBytesRight("a", 4);
  73. *
  74. * // returns ["a", 1, 1, 1]
  75. * Utils.padBytesRight("a", 4, 1);
  76. *
  77. * // returns ["t", "e", "s", "t", 0, 0, 0, 0]
  78. * Utils.padBytesRight("test", 8);
  79. *
  80. * // returns ["t", "e", "s", "t", 1, 1, 1, 1]
  81. * Utils.padBytesRight("test", 8, 1);
  82. */
  83. static padBytesRight(arr, numBytes, padByte=0) {
  84. const paddedBytes = new Array(numBytes);
  85. paddedBytes.fill(padByte);
  86. Array.prototype.map.call(arr, function(b, i) {
  87. paddedBytes[i] = b;
  88. });
  89. return paddedBytes;
  90. }
  91. /**
  92. * Truncates a long string to max length and adds suffix.
  93. *
  94. * @param {string} str - String to truncate
  95. * @param {number} max - Maximum length of the final string
  96. * @param {string} [suffix='...'] - The string to add to the end of the final string
  97. * @returns {string}
  98. *
  99. * @example
  100. * // returns "A long..."
  101. * Utils.truncate("A long string", 9);
  102. *
  103. * // returns "A long s-"
  104. * Utils.truncate("A long string", 9, "-");
  105. */
  106. static truncate(str, max, suffix="...") {
  107. if (str.length > max) {
  108. str = str.slice(0, max - suffix.length) + suffix;
  109. }
  110. return str;
  111. }
  112. /**
  113. * Converts a character or number to its hex representation.
  114. *
  115. * @param {char|number} c
  116. * @param {number} [length=2] - The width of the resulting hex number.
  117. * @returns {string}
  118. *
  119. * @example
  120. * // returns "6e"
  121. * Utils.hex("n");
  122. *
  123. * // returns "6e"
  124. * Utils.hex(110);
  125. */
  126. static hex(c, length=2) {
  127. c = typeof c == "string" ? Utils.ord(c) : c;
  128. return c.toString(16).padStart(length, "0");
  129. }
  130. /**
  131. * Converts a character or number to its binary representation.
  132. *
  133. * @param {char|number} c
  134. * @param {number} [length=8] - The width of the resulting binary number.
  135. * @returns {string}
  136. *
  137. * @example
  138. * // returns "01101110"
  139. * Utils.bin("n");
  140. *
  141. * // returns "01101110"
  142. * Utils.bin(110);
  143. */
  144. static bin(c, length=8) {
  145. c = typeof c == "string" ? Utils.ord(c) : c;
  146. return c.toString(2).padStart(length, "0");
  147. }
  148. /**
  149. * Returns a string with all non-printable chars as dots, optionally preserving whitespace.
  150. *
  151. * @param {string} str - The input string to display.
  152. * @param {boolean} [preserveWs=false] - Whether or not to print whitespace.
  153. * @returns {string}
  154. */
  155. static printable(str, preserveWs=false) {
  156. if (ENVIRONMENT_IS_WEB() && window.app && !window.app.options.treatAsUtf8) {
  157. str = Utils.byteArrayToChars(Utils.strToByteArray(str));
  158. }
  159. const re = /[\0-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g;
  160. const wsRe = /[\x09-\x10\x0D\u2028\u2029]/g;
  161. str = str.replace(re, ".");
  162. if (!preserveWs) str = str.replace(wsRe, ".");
  163. return str;
  164. }
  165. /**
  166. * Parse a string entered by a user and replace escaped chars with the bytes they represent.
  167. *
  168. * @param {string} str
  169. * @returns {string}
  170. *
  171. * @example
  172. * // returns "\x00"
  173. * Utils.parseEscapedChars("\\x00");
  174. *
  175. * // returns "\n"
  176. * Utils.parseEscapedChars("\\n");
  177. */
  178. static parseEscapedChars(str) {
  179. return str.replace(/(\\)?\\([bfnrtv0'"]|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\})/g, function(m, a, b) {
  180. if (a === "\\") return "\\"+b;
  181. switch (b[0]) {
  182. case "0":
  183. return "\0";
  184. case "b":
  185. return "\b";
  186. case "t":
  187. return "\t";
  188. case "n":
  189. return "\n";
  190. case "v":
  191. return "\v";
  192. case "f":
  193. return "\f";
  194. case "r":
  195. return "\r";
  196. case '"':
  197. return '"';
  198. case "'":
  199. return "'";
  200. case "x":
  201. return String.fromCharCode(parseInt(b.substr(1), 16));
  202. case "u":
  203. if (b[1] === "{")
  204. return String.fromCodePoint(parseInt(b.slice(2, -1), 16));
  205. else
  206. return String.fromCharCode(parseInt(b.substr(1), 16));
  207. }
  208. });
  209. }
  210. /**
  211. * Escape a string containing regex control characters so that it can be safely
  212. * used in a regex without causing unintended behaviours.
  213. *
  214. * @param {string} str
  215. * @returns {string}
  216. *
  217. * @example
  218. * // returns "\[example\]"
  219. * Utils.escapeRegex("[example]");
  220. */
  221. static escapeRegex(str) {
  222. return str.replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1");
  223. }
  224. /**
  225. * Expand an alphabet range string into a list of the characters in that range.
  226. *
  227. * @param {string} alphStr
  228. * @returns {char[]}
  229. *
  230. * @example
  231. * // returns ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
  232. * Utils.expandAlphRange("0-9");
  233. *
  234. * // returns ["a", "b", "c", "d", "0", "1", "2", "3", "+", "/"]
  235. * Utils.expandAlphRange("a-d0-3+/");
  236. *
  237. * // returns ["a", "b", "c", "d", "0", "-", "3"]
  238. * Utils.expandAlphRange("a-d0\\-3")
  239. */
  240. static expandAlphRange(alphStr) {
  241. const alphArr = [];
  242. for (let i = 0; i < alphStr.length; i++) {
  243. if (i < alphStr.length - 2 &&
  244. alphStr[i+1] === "-" &&
  245. alphStr[i] !== "\\") {
  246. const start = Utils.ord(alphStr[i]),
  247. end = Utils.ord(alphStr[i+2]);
  248. for (let j = start; j <= end; j++) {
  249. alphArr.push(Utils.chr(j));
  250. }
  251. i += 2;
  252. } else if (i < alphStr.length - 2 &&
  253. alphStr[i] === "\\" &&
  254. alphStr[i+1] === "-") {
  255. alphArr.push("-");
  256. i++;
  257. } else {
  258. alphArr.push(alphStr[i]);
  259. }
  260. }
  261. return alphArr;
  262. }
  263. /**
  264. * Coverts data of varying types to a byteArray.
  265. * Accepts hex, Base64, UTF8 and Latin1 strings.
  266. *
  267. * @param {string} str
  268. * @param {string} type - One of "Hex", "Base64", "UTF8" or "Latin1"
  269. * @returns {byteArray}
  270. *
  271. * @example
  272. * // returns [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130]
  273. * Utils.convertToByteArray("Привет", "utf8");
  274. *
  275. * // returns [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130]
  276. * Utils.convertToByteArray("d097d0b4d180d0b0d0b2d181d182d0b2d183d0b9d182d0b5", "hex");
  277. *
  278. * // returns [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130]
  279. * Utils.convertToByteArray("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
  280. */
  281. static convertToByteArray(str, type) {
  282. switch (type.toLowerCase()) {
  283. case "hex":
  284. return fromHex(str);
  285. case "base64":
  286. return fromBase64(str, null, "byteArray");
  287. case "utf8":
  288. return Utils.strToUtf8ByteArray(str);
  289. case "latin1":
  290. default:
  291. return Utils.strToByteArray(str);
  292. }
  293. }
  294. /**
  295. * Coverts data of varying types to a byte string.
  296. * Accepts hex, Base64, UTF8 and Latin1 strings.
  297. *
  298. * @param {string} str
  299. * @param {string} type - One of "Hex", "Base64", "UTF8" or "Latin1"
  300. * @returns {string}
  301. *
  302. * @example
  303. * // returns "Привет"
  304. * Utils.convertToByteString("Привет", "utf8");
  305. *
  306. * // returns "Здравствуйте"
  307. * Utils.convertToByteString("d097d0b4d180d0b0d0b2d181d182d0b2d183d0b9d182d0b5", "hex");
  308. *
  309. * // returns "Здравствуйте"
  310. * Utils.convertToByteString("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
  311. */
  312. static convertToByteString(str, type) {
  313. switch (type.toLowerCase()) {
  314. case "hex":
  315. return Utils.byteArrayToChars(fromHex(str));
  316. case "base64":
  317. return Utils.byteArrayToChars(fromBase64(str, null, "byteArray"));
  318. case "utf8":
  319. return utf8.encode(str);
  320. case "latin1":
  321. default:
  322. return str;
  323. }
  324. }
  325. /**
  326. * Converts a string to a byte array.
  327. * Treats the string as UTF-8 if any values are over 255.
  328. *
  329. * @param {string} str
  330. * @returns {byteArray}
  331. *
  332. * @example
  333. * // returns [72,101,108,108,111]
  334. * Utils.strToByteArray("Hello");
  335. *
  336. * // returns [228,189,160,229,165,189]
  337. * Utils.strToByteArray("你好");
  338. */
  339. static strToByteArray(str) {
  340. const byteArray = new Array(str.length);
  341. let i = str.length, b;
  342. while (i--) {
  343. b = str.charCodeAt(i);
  344. byteArray[i] = b;
  345. // If any of the bytes are over 255, read as UTF-8
  346. if (b > 255) return Utils.strToUtf8ByteArray(str);
  347. }
  348. return byteArray;
  349. }
  350. /**
  351. * Converts a string to a UTF-8 byte array.
  352. *
  353. * @param {string} str
  354. * @returns {byteArray}
  355. *
  356. * @example
  357. * // returns [72,101,108,108,111]
  358. * Utils.strToUtf8ByteArray("Hello");
  359. *
  360. * // returns [228,189,160,229,165,189]
  361. * Utils.strToUtf8ByteArray("你好");
  362. */
  363. static strToUtf8ByteArray(str) {
  364. const utf8Str = utf8.encode(str);
  365. if (str.length !== utf8Str.length) {
  366. if (ENVIRONMENT_IS_WORKER()) {
  367. self.setOption("attemptHighlight", false);
  368. } else if (ENVIRONMENT_IS_WEB()) {
  369. window.app.options.attemptHighlight = false;
  370. }
  371. }
  372. return Utils.strToByteArray(utf8Str);
  373. }
  374. /**
  375. * Converts a string to a unicode charcode array
  376. *
  377. * @param {string} str
  378. * @returns {byteArray}
  379. *
  380. * @example
  381. * // returns [72,101,108,108,111]
  382. * Utils.strToCharcode("Hello");
  383. *
  384. * // returns [20320,22909]
  385. * Utils.strToCharcode("你好");
  386. */
  387. static strToCharcode(str) {
  388. const charcode = [];
  389. for (let i = 0; i < str.length; i++) {
  390. let ord = str.charCodeAt(i);
  391. // Detect and merge astral symbols
  392. if (i < str.length - 1 && ord >= 0xd800 && ord < 0xdc00) {
  393. const low = str[i + 1].charCodeAt(0);
  394. if (low >= 0xdc00 && low < 0xe000) {
  395. ord = Utils.ord(str[i] + str[++i]);
  396. }
  397. }
  398. charcode.push(ord);
  399. }
  400. return charcode;
  401. }
  402. /**
  403. * Attempts to convert a byte array to a UTF-8 string.
  404. *
  405. * @param {byteArray} byteArray
  406. * @returns {string}
  407. *
  408. * @example
  409. * // returns "Hello"
  410. * Utils.byteArrayToUtf8([72,101,108,108,111]);
  411. *
  412. * // returns "你好"
  413. * Utils.byteArrayToUtf8([228,189,160,229,165,189]);
  414. */
  415. static byteArrayToUtf8(byteArray) {
  416. const str = Utils.byteArrayToChars(byteArray);
  417. try {
  418. const utf8Str = utf8.decode(str);
  419. if (str.length !== utf8Str.length) {
  420. if (ENVIRONMENT_IS_WORKER()) {
  421. self.setOption("attemptHighlight", false);
  422. } else if (ENVIRONMENT_IS_WEB()) {
  423. window.app.options.attemptHighlight = false;
  424. }
  425. }
  426. return utf8Str;
  427. } catch (err) {
  428. // If it fails, treat it as ANSI
  429. return str;
  430. }
  431. }
  432. /**
  433. * Converts a charcode array to a string.
  434. *
  435. * @param {byteArray|Uint8Array} byteArray
  436. * @returns {string}
  437. *
  438. * @example
  439. * // returns "Hello"
  440. * Utils.byteArrayToChars([72,101,108,108,111]);
  441. *
  442. * // returns "你好"
  443. * Utils.byteArrayToChars([20320,22909]);
  444. */
  445. static byteArrayToChars(byteArray) {
  446. if (!byteArray) return "";
  447. let str = "";
  448. for (let i = 0; i < byteArray.length;) {
  449. str += String.fromCharCode(byteArray[i++]);
  450. }
  451. return str;
  452. }
  453. /**
  454. * Converts an ArrayBuffer to a string.
  455. *
  456. * @param {ArrayBuffer} arrayBuffer
  457. * @param {boolean} [utf8=true] - Whether to attempt to decode the buffer as UTF-8
  458. * @returns {string}
  459. *
  460. * @example
  461. * // returns "hello"
  462. * Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer);
  463. */
  464. static arrayBufferToStr(arrayBuffer, utf8=true) {
  465. const byteArray = Array.prototype.slice.call(new Uint8Array(arrayBuffer));
  466. return utf8 ? Utils.byteArrayToUtf8(byteArray) : Utils.byteArrayToChars(byteArray);
  467. }
  468. /**
  469. * Parses CSV data and returns it as a two dimensional array or strings.
  470. *
  471. * @param {string} data
  472. * @param {string[]} [cellDelims=[","]]
  473. * @param {string[]} [lineDelims=["\n", "\r"]]
  474. * @returns {string[][]}
  475. *
  476. * @example
  477. * // returns [["head1", "head2"], ["data1", "data2"]]
  478. * Utils.parseCSV("head1,head2\ndata1,data2");
  479. */
  480. static parseCSV(data, cellDelims=[","], lineDelims=["\n", "\r"]) {
  481. let b,
  482. next,
  483. renderNext = false,
  484. inString = false,
  485. cell = "",
  486. line = [];
  487. const lines = [];
  488. // Remove BOM, often present in Excel CSV files
  489. if (data.length && data[0] === "\uFEFF") data = data.substr(1);
  490. for (let i = 0; i < data.length; i++) {
  491. b = data[i];
  492. next = data[i+1] || "";
  493. if (renderNext) {
  494. cell += b;
  495. renderNext = false;
  496. } else if (b === "\\") {
  497. renderNext = true;
  498. } else if (b === "\"" && !inString) {
  499. inString = true;
  500. } else if (b === "\"" && inString) {
  501. if (next === "\"") renderNext = true;
  502. else inString = false;
  503. } else if (!inString && cellDelims.indexOf(b) >= 0) {
  504. line.push(cell);
  505. cell = "";
  506. } else if (!inString && lineDelims.indexOf(b) >= 0) {
  507. line.push(cell);
  508. cell = "";
  509. lines.push(line);
  510. line = [];
  511. } else {
  512. cell += b;
  513. }
  514. }
  515. if (line.length) {
  516. line.push(cell);
  517. lines.push(line);
  518. }
  519. return lines;
  520. }
  521. /**
  522. * Removes all HTML (or XML) tags from the input string.
  523. *
  524. * @param {string} htmlStr
  525. * @param {boolean} [removeScriptAndStyle=false]
  526. * - Flag to specify whether to remove entire script or style blocks
  527. * @returns {string}
  528. *
  529. * @example
  530. * // returns "Test"
  531. * Utils.stripHtmlTags("<div>Test</div>");
  532. */
  533. static stripHtmlTags(htmlStr, removeScriptAndStyle=false) {
  534. if (removeScriptAndStyle) {
  535. htmlStr = htmlStr.replace(/<(script|style)[^>]*>.*<\/(script|style)>/gmi, "");
  536. }
  537. return htmlStr.replace(/<[^>]+>/g, "");
  538. }
  539. /**
  540. * Escapes HTML tags in a string to stop them being rendered.
  541. * https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
  542. *
  543. * @param {string} str
  544. * @returns string
  545. *
  546. * @example
  547. * // return "A &lt;script&gt; tag"
  548. * Utils.escapeHtml("A <script> tag");
  549. */
  550. static escapeHtml(str) {
  551. const HTML_CHARS = {
  552. "&": "&amp;",
  553. "<": "&lt;",
  554. ">": "&gt;",
  555. '"': "&quot;",
  556. "'": "&#x27;", // &apos; not recommended because it's not in the HTML spec
  557. "/": "&#x2F;", // forward slash is included as it helps end an HTML entity
  558. "`": "&#x60;"
  559. };
  560. return str.replace(/[&<>"'/`]/g, function (match) {
  561. return HTML_CHARS[match];
  562. });
  563. }
  564. /**
  565. * Unescapes HTML tags in a string to make them render again.
  566. *
  567. * @param {string} str
  568. * @returns string
  569. *
  570. * @example
  571. * // return "A <script> tag"
  572. * Utils.unescapeHtml("A &lt;script&gt; tag");
  573. */
  574. static unescapeHtml(str) {
  575. const HTML_CHARS = {
  576. "&amp;": "&",
  577. "&lt;": "<",
  578. "&gt;": ">",
  579. "&quot;": '"',
  580. "&#x27;": "'",
  581. "&#x2F;": "/",
  582. "&#x60;": "`"
  583. };
  584. return str.replace(/&#?x?[a-z0-9]{2,4};/ig, function (match) {
  585. return HTML_CHARS[match] || match;
  586. });
  587. }
  588. /**
  589. * Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
  590. *
  591. * RFC 3986 defines legal characters for the fragment and query parts of a URL to be as follows:
  592. *
  593. * fragment = *( pchar / "/" / "?" )
  594. * query = *( pchar / "/" / "?" )
  595. * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
  596. * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
  597. * pct-encoded = "%" HEXDIG HEXDIG
  598. * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
  599. * / "*" / "+" / "," / ";" / "="
  600. *
  601. * Meaning that the list of characters that need not be percent-encoded are alphanumeric plus:
  602. * -._~!$&'()*+,;=:@/?
  603. *
  604. * & and = are still escaped as they are used to serialise the key-value pairs in CyberChef
  605. * fragments. + is also escaped so as to prevent it being decoded to a space.
  606. *
  607. * @param {string} str
  608. * @returns {string}
  609. */
  610. static encodeURIFragment(str) {
  611. const LEGAL_CHARS = {
  612. "%2D": "-",
  613. "%2E": ".",
  614. "%5F": "_",
  615. "%7E": "~",
  616. "%21": "!",
  617. "%24": "$",
  618. //"%26": "&",
  619. "%27": "'",
  620. "%28": "(",
  621. "%29": ")",
  622. "%2A": "*",
  623. //"%2B": "+",
  624. "%2C": ",",
  625. "%3B": ";",
  626. //"%3D": "=",
  627. "%3A": ":",
  628. "%40": "@",
  629. "%2F": "/",
  630. "%3F": "?"
  631. };
  632. str = encodeURIComponent(str);
  633. return str.replace(/%[0-9A-F]{2}/g, function (match) {
  634. return LEGAL_CHARS[match] || match;
  635. });
  636. }
  637. /**
  638. * Generates a "pretty" recipe format from a recipeConfig object.
  639. *
  640. * "Pretty" CyberChef recipe formats are designed to be included in the fragment (#) or query (?)
  641. * parts of the URL. They can also be loaded into CyberChef through the 'Load' interface. In order
  642. * to make this format as readable as possible, various special characters are used unescaped. This
  643. * reduces the amount of percent-encoding included in the URL which is typically difficult to read
  644. * and substantially increases the overall length. These characteristics can be quite off-putting
  645. * for users.
  646. *
  647. * @param {Object[]} recipeConfig
  648. * @param {boolean} [newline=false] - whether to add a newline after each operation
  649. * @returns {string}
  650. */
  651. static generatePrettyRecipe(recipeConfig, newline = false) {
  652. let prettyConfig = "",
  653. name = "",
  654. args = "",
  655. disabled = "",
  656. bp = "";
  657. recipeConfig.forEach(op => {
  658. name = op.op.replace(/ /g, "_");
  659. args = JSON.stringify(op.args)
  660. .slice(1, -1) // Remove [ and ] as they are implied
  661. // We now need to switch double-quoted (") strings to single quotes (') as single quotes
  662. // do not need to be percent-encoded.
  663. .replace(/'/g, "\\'") // Escape single quotes
  664. .replace(/"((?:[^"\\]|\\.)*)"/g, "'$1'") // Replace opening and closing " with '
  665. .replace(/\\"/g, '"'); // Unescape double quotes
  666. disabled = op.disabled ? "/disabled": "";
  667. bp = op.breakpoint ? "/breakpoint" : "";
  668. prettyConfig += `${name}(${args}${disabled}${bp})`;
  669. if (newline) prettyConfig += "\n";
  670. });
  671. return prettyConfig;
  672. }
  673. /**
  674. * Converts a recipe string to the JSON representation of the recipe.
  675. * Accepts either stringified JSON or the bespoke "pretty" recipe format.
  676. *
  677. * @param {string} recipe
  678. * @returns {Object[]}
  679. */
  680. static parseRecipeConfig(recipe) {
  681. recipe = recipe.trim();
  682. if (recipe.length === 0) return [];
  683. if (recipe[0] === "[") return JSON.parse(recipe);
  684. // Parse bespoke recipe format
  685. recipe = recipe.replace(/\n/g, "");
  686. let m, args;
  687. const recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/'])*)(\/[^)]+)?\)/g,
  688. recipeConfig = [];
  689. while ((m = recipeRegex.exec(recipe))) {
  690. // Translate strings in args back to double-quotes
  691. args = m[2]
  692. .replace(/"/g, '\\"') // Escape double quotes
  693. .replace(/(^|,|{|:)'/g, '$1"') // Replace opening ' with "
  694. .replace(/([^\\])'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
  695. .replace(/\\'/g, "'"); // Unescape single quotes
  696. args = "[" + args + "]";
  697. const op = {
  698. op: m[1].replace(/_/g, " "),
  699. args: JSON.parse(args)
  700. };
  701. if (m[3] && m[3].indexOf("disabled") > 0) op.disabled = true;
  702. if (m[3] && m[3].indexOf("breakpoint") > 0) op.breakpoint = true;
  703. recipeConfig.push(op);
  704. }
  705. return recipeConfig;
  706. }
  707. /**
  708. * Expresses a number of milliseconds in a human readable format.
  709. *
  710. * Range | Sample Output
  711. * -----------------------------|-------------------------------
  712. * 0 to 45 seconds | a few seconds ago
  713. * 45 to 90 seconds | a minute ago
  714. * 90 seconds to 45 minutes | 2 minutes ago ... 45 minutes ago
  715. * 45 to 90 minutes | an hour ago
  716. * 90 minutes to 22 hours | 2 hours ago ... 22 hours ago
  717. * 22 to 36 hours | a day ago
  718. * 36 hours to 25 days | 2 days ago ... 25 days ago
  719. * 25 to 45 days | a month ago
  720. * 45 to 345 days | 2 months ago ... 11 months ago
  721. * 345 to 545 days (1.5 years) | a year ago
  722. * 546 days+ | 2 years ago ... 20 years ago
  723. *
  724. * @param {number} ms
  725. * @returns {string}
  726. *
  727. * @example
  728. * // returns "3 minutes"
  729. * Utils.fuzzyTime(152435);
  730. *
  731. * // returns "5 days"
  732. * Utils.fuzzyTime(456851321);
  733. */
  734. static fuzzyTime(ms) {
  735. return moment.duration(ms, "milliseconds").humanize();
  736. }
  737. /**
  738. * Formats a list of files or directories.
  739. *
  740. * @author tlwr [toby@toby.codes]
  741. * @author n1474335 [n1474335@gmail.com]
  742. *
  743. * @param {File[]} files
  744. * @returns {html}
  745. */
  746. static async displayFilesAsHTML(files) {
  747. const formatDirectory = function(file) {
  748. const html = `<div class='card' style='white-space: normal;'>
  749. <div class='card-header'>
  750. <h6 class="mb-0">
  751. ${Utils.escapeHtml(file.name)}
  752. </h6>
  753. </div>
  754. </div>`;
  755. return html;
  756. };
  757. const formatFile = async function(file, i) {
  758. const buff = await Utils.readFile(file);
  759. const blob = new Blob(
  760. [buff],
  761. {type: "octet/stream"}
  762. );
  763. const html = `<div class='card' style='white-space: normal;'>
  764. <div class='card-header' id='heading${i}'>
  765. <h6 class='mb-0'>
  766. <a class='collapsed'
  767. data-toggle='collapse'
  768. href='#collapse${i}'
  769. aria-expanded='false'
  770. aria-controls='collapse${i}'
  771. title="Show/hide contents of '${Utils.escapeHtml(file.name)}'">
  772. ${Utils.escapeHtml(file.name)}</a>
  773. <span class='float-right' style="margin-top: -3px">
  774. ${file.size.toLocaleString()} bytes
  775. <a title="Download ${Utils.escapeHtml(file.name)}"
  776. href='${URL.createObjectURL(blob)}'
  777. download='${Utils.escapeHtml(file.name)}'>
  778. <i class="material-icons" style="vertical-align: bottom">save</i>
  779. </a>
  780. </span>
  781. </h6>
  782. </div>
  783. <div id='collapse${i}' class='collapse' aria-labelledby='heading${i}' data-parent="#files">
  784. <div class='card-body'>
  785. <pre>${Utils.escapeHtml(Utils.arrayBufferToStr(buff.buffer))}</pre>
  786. </div>
  787. </div>
  788. </div>`;
  789. return html;
  790. };
  791. let html = `<div style='padding: 5px; white-space: normal;'>
  792. ${files.length} file(s) found
  793. </div><div id="files" style="padding: 20px">`;
  794. for (let i = 0; i < files.length; i++) {
  795. if (files[i].name.endsWith("/")) {
  796. html += formatDirectory(files[i]);
  797. } else {
  798. html += await formatFile(files[i], i);
  799. }
  800. }
  801. return html += "</div>";
  802. }
  803. /**
  804. * Parses URI parameters into a JSON object.
  805. *
  806. * @param {string} paramStr - The serialised query or hash section of a URI
  807. * @returns {object}
  808. *
  809. * @example
  810. * // returns {a: 'abc', b: '123'}
  811. * Utils.parseURIParams("?a=abc&b=123")
  812. * Utils.parseURIParams("#a=abc&b=123")
  813. */
  814. static parseURIParams(paramStr) {
  815. if (paramStr === "") return {};
  816. // Cut off ? or # and split on &
  817. if (paramStr[0] === "?" ||
  818. paramStr[0] === "#") {
  819. paramStr = paramStr.substr(1);
  820. }
  821. const params = paramStr.split("&");
  822. const result = {};
  823. for (let i = 0; i < params.length; i++) {
  824. const param = params[i].split("=");
  825. if (param.length !== 2) {
  826. result[params[i]] = true;
  827. } else {
  828. result[param[0]] = decodeURIComponent(param[1].replace(/\+/g, " "));
  829. }
  830. }
  831. return result;
  832. }
  833. /**
  834. * Reads a File and returns the data as a Uint8Array.
  835. *
  836. * @param {File} file
  837. * @returns {Uint8Array}
  838. *
  839. * @example
  840. * // returns Uint8Array(5) [104, 101, 108, 108, 111]
  841. * await Utils.readFile(new File(["hello"], "test"))
  842. */
  843. static readFile(file) {
  844. return new Promise((resolve, reject) => {
  845. const reader = new FileReader();
  846. const data = new Uint8Array(file.size);
  847. let offset = 0;
  848. const CHUNK_SIZE = 10485760; // 10MiB
  849. const seek = function() {
  850. if (offset >= file.size) {
  851. resolve(data);
  852. return;
  853. }
  854. const slice = file.slice(offset, offset + CHUNK_SIZE);
  855. reader.readAsArrayBuffer(slice);
  856. };
  857. reader.onload = function(e) {
  858. data.set(new Uint8Array(reader.result), offset);
  859. offset += CHUNK_SIZE;
  860. seek();
  861. };
  862. reader.onerror = function(e) {
  863. reject(reader.error.message);
  864. };
  865. seek();
  866. });
  867. }
  868. /**
  869. * Actual modulo function, since % is actually the remainder function in JS.
  870. *
  871. * @author Matt C [matt@artemisbot.uk]
  872. * @param {number} x
  873. * @param {number} y
  874. * @returns {number}
  875. */
  876. static mod(x, y) {
  877. return ((x % y) + y) % y;
  878. }
  879. /**
  880. * Finds the greatest common divisor of two numbers.
  881. *
  882. * @author Matt C [matt@artemisbot.uk]
  883. * @param {number} x
  884. * @param {number} y
  885. * @returns {number}
  886. */
  887. static gcd(x, y) {
  888. if (!y) {
  889. return x;
  890. }
  891. return Utils.gcd(y, x % y);
  892. }
  893. /**
  894. * Finds the modular inverse of two values.
  895. *
  896. * @author Matt C [matt@artemisbot.uk]
  897. * @param {number} x
  898. * @param {number} y
  899. * @returns {number}
  900. */
  901. static modInv(x, y) {
  902. x %= y;
  903. for (let i = 1; i < y; i++) {
  904. if ((x * i) % 26 === 1) {
  905. return i;
  906. }
  907. }
  908. }
  909. /**
  910. * A mapping of names of delimiter characters to their symbols.
  911. *
  912. * @param {string} token
  913. * @returns {string}
  914. */
  915. static charRep(token) {
  916. return {
  917. "Space": " ",
  918. "Comma": ",",
  919. "Semi-colon": ";",
  920. "Colon": ":",
  921. "Line feed": "\n",
  922. "CRLF": "\r\n",
  923. "Forward slash": "/",
  924. "Backslash": "\\",
  925. "0x": "0x",
  926. "\\x": "\\x",
  927. "Nothing (separate chars)": "",
  928. "None": "",
  929. }[token];
  930. }
  931. /**
  932. * A mapping of names of delimiter characters to regular expressions which can select them.
  933. *
  934. * @param {string} token
  935. * @returns {RegExp}
  936. */
  937. static regexRep(token) {
  938. return {
  939. "Space": /\s+/g,
  940. "Comma": /,/g,
  941. "Semi-colon": /;/g,
  942. "Colon": /:/g,
  943. "Line feed": /\n/g,
  944. "CRLF": /\r\n/g,
  945. "Forward slash": /\//g,
  946. "Backslash": /\\/g,
  947. "0x": /0x/g,
  948. "\\x": /\\x/g,
  949. "None": /\s+/g // Included here to remove whitespace when there shouldn't be any
  950. }[token];
  951. }
  952. }
  953. export default Utils;
  954. /**
  955. * Removes all duplicates from an array.
  956. *
  957. * @returns {Array}
  958. *
  959. * @example
  960. * // returns [3,6,4,8,2]
  961. * [3,6,4,8,4,2,3].unique();
  962. *
  963. * // returns ["One", "Two", "Three"]
  964. * ["One", "Two", "Three", "One"].unique();
  965. */
  966. Array.prototype.unique = function() {
  967. const u = {}, a = [];
  968. for (let i = 0, l = this.length; i < l; i++) {
  969. if (u.hasOwnProperty(this[i])) {
  970. continue;
  971. }
  972. a.push(this[i]);
  973. u[this[i]] = 1;
  974. }
  975. return a;
  976. };
  977. /**
  978. * Returns the largest value in the array.
  979. *
  980. * @returns {number}
  981. *
  982. * @example
  983. * // returns 7
  984. * [4,2,5,3,7].max();
  985. */
  986. Array.prototype.max = function() {
  987. return Math.max.apply(null, this);
  988. };
  989. /**
  990. * Returns the smallest value in the array.
  991. *
  992. * @returns {number}
  993. *
  994. * @example
  995. * // returns 2
  996. * [4,2,5,3,7].min();
  997. */
  998. Array.prototype.min = function() {
  999. return Math.min.apply(null, this);
  1000. };
  1001. /**
  1002. * Sums all the values in an array.
  1003. *
  1004. * @returns {number}
  1005. *
  1006. * @example
  1007. * // returns 21
  1008. * [4,2,5,3,7].sum();
  1009. */
  1010. Array.prototype.sum = function() {
  1011. return this.reduce(function (a, b) {
  1012. return a + b;
  1013. }, 0);
  1014. };
  1015. /**
  1016. * Determine whether two arrays are equal or not.
  1017. *
  1018. * @param {Object[]} other
  1019. * @returns {boolean}
  1020. *
  1021. * @example
  1022. * // returns true
  1023. * [1,2,3].equals([1,2,3]);
  1024. *
  1025. * // returns false
  1026. * [1,2,3].equals([3,2,1]);
  1027. */
  1028. Array.prototype.equals = function(other) {
  1029. if (!other) return false;
  1030. let i = this.length;
  1031. if (i !== other.length) return false;
  1032. while (i--) {
  1033. if (this[i] !== other[i]) return false;
  1034. }
  1035. return true;
  1036. };
  1037. /**
  1038. * Counts the number of times a char appears in a string.
  1039. *
  1040. * @param {char} chr
  1041. * @returns {number}
  1042. *
  1043. * @example
  1044. * // returns 2
  1045. * "Hello".count("l");
  1046. */
  1047. String.prototype.count = function(chr) {
  1048. return this.split(chr).length - 1;
  1049. };
  1050. /*
  1051. * Polyfills
  1052. */
  1053. // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
  1054. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
  1055. if (!String.prototype.padStart) {
  1056. String.prototype.padStart = function padStart(targetLength, padString) {
  1057. targetLength = targetLength>>0; //floor if number or convert non-number to 0;
  1058. padString = String((typeof padString !== "undefined" ? padString : " "));
  1059. if (this.length > targetLength) {
  1060. return String(this);
  1061. } else {
  1062. targetLength = targetLength-this.length;
  1063. if (targetLength > padString.length) {
  1064. padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
  1065. }
  1066. return padString.slice(0, targetLength) + String(this);
  1067. }
  1068. };
  1069. }
  1070. // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
  1071. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
  1072. if (!String.prototype.padEnd) {
  1073. String.prototype.padEnd = function padEnd(targetLength, padString) {
  1074. targetLength = targetLength>>0; //floor if number or convert non-number to 0;
  1075. padString = String((typeof padString !== "undefined" ? padString : " "));
  1076. if (this.length > targetLength) {
  1077. return String(this);
  1078. } else {
  1079. targetLength = targetLength-this.length;
  1080. if (targetLength > padString.length) {
  1081. padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
  1082. }
  1083. return String(this) + padString.slice(0, targetLength);
  1084. }
  1085. };
  1086. }