utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. "use strict";
  2. var support = require("./support");
  3. var base64 = require("./base64");
  4. var nodejsUtils = require("./nodejsUtils");
  5. var external = require("./external");
  6. require("setimmediate");
  7. /**
  8. * Convert a string that pass as a "binary string": it should represent a byte
  9. * array but may have > 255 char codes. Be sure to take only the first byte
  10. * and returns the byte array.
  11. * @param {String} str the string to transform.
  12. * @return {Array|Uint8Array} the string in a binary format.
  13. */
  14. function string2binary(str) {
  15. var result = null;
  16. if (support.uint8array) {
  17. result = new Uint8Array(str.length);
  18. } else {
  19. result = new Array(str.length);
  20. }
  21. return stringToArrayLike(str, result);
  22. }
  23. /**
  24. * Create a new blob with the given content and the given type.
  25. * @param {String|ArrayBuffer} part the content to put in the blob. DO NOT use
  26. * an Uint8Array because the stock browser of android 4 won't accept it (it
  27. * will be silently converted to a string, "[object Uint8Array]").
  28. *
  29. * Use only ONE part to build the blob to avoid a memory leak in IE11 / Edge:
  30. * when a large amount of Array is used to create the Blob, the amount of
  31. * memory consumed is nearly 100 times the original data amount.
  32. *
  33. * @param {String} type the mime type of the blob.
  34. * @return {Blob} the created blob.
  35. */
  36. exports.newBlob = function(part, type) {
  37. exports.checkSupport("blob");
  38. try {
  39. // Blob constructor
  40. return new Blob([part], {
  41. type: type
  42. });
  43. }
  44. catch (e) {
  45. try {
  46. // deprecated, browser only, old way
  47. var Builder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder || self.MSBlobBuilder;
  48. var builder = new Builder();
  49. builder.append(part);
  50. return builder.getBlob(type);
  51. }
  52. catch (e) {
  53. // well, fuck ?!
  54. throw new Error("Bug : can't construct the Blob.");
  55. }
  56. }
  57. };
  58. /**
  59. * The identity function.
  60. * @param {Object} input the input.
  61. * @return {Object} the same input.
  62. */
  63. function identity(input) {
  64. return input;
  65. }
  66. /**
  67. * Fill in an array with a string.
  68. * @param {String} str the string to use.
  69. * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to fill in (will be mutated).
  70. * @return {Array|ArrayBuffer|Uint8Array|Buffer} the updated array.
  71. */
  72. function stringToArrayLike(str, array) {
  73. for (var i = 0; i < str.length; ++i) {
  74. array[i] = str.charCodeAt(i) & 0xFF;
  75. }
  76. return array;
  77. }
  78. /**
  79. * An helper for the function arrayLikeToString.
  80. * This contains static information and functions that
  81. * can be optimized by the browser JIT compiler.
  82. */
  83. var arrayToStringHelper = {
  84. /**
  85. * Transform an array of int into a string, chunk by chunk.
  86. * See the performances notes on arrayLikeToString.
  87. * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform.
  88. * @param {String} type the type of the array.
  89. * @param {Integer} chunk the chunk size.
  90. * @return {String} the resulting string.
  91. * @throws Error if the chunk is too big for the stack.
  92. */
  93. stringifyByChunk: function(array, type, chunk) {
  94. var result = [], k = 0, len = array.length;
  95. // shortcut
  96. if (len <= chunk) {
  97. return String.fromCharCode.apply(null, array);
  98. }
  99. while (k < len) {
  100. if (type === "array" || type === "nodebuffer") {
  101. result.push(String.fromCharCode.apply(null, array.slice(k, Math.min(k + chunk, len))));
  102. }
  103. else {
  104. result.push(String.fromCharCode.apply(null, array.subarray(k, Math.min(k + chunk, len))));
  105. }
  106. k += chunk;
  107. }
  108. return result.join("");
  109. },
  110. /**
  111. * Call String.fromCharCode on every item in the array.
  112. * This is the naive implementation, which generate A LOT of intermediate string.
  113. * This should be used when everything else fail.
  114. * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform.
  115. * @return {String} the result.
  116. */
  117. stringifyByChar: function(array){
  118. var resultStr = "";
  119. for(var i = 0; i < array.length; i++) {
  120. resultStr += String.fromCharCode(array[i]);
  121. }
  122. return resultStr;
  123. },
  124. applyCanBeUsed : {
  125. /**
  126. * true if the browser accepts to use String.fromCharCode on Uint8Array
  127. */
  128. uint8array : (function () {
  129. try {
  130. return support.uint8array && String.fromCharCode.apply(null, new Uint8Array(1)).length === 1;
  131. } catch (e) {
  132. return false;
  133. }
  134. })(),
  135. /**
  136. * true if the browser accepts to use String.fromCharCode on nodejs Buffer.
  137. */
  138. nodebuffer : (function () {
  139. try {
  140. return support.nodebuffer && String.fromCharCode.apply(null, nodejsUtils.allocBuffer(1)).length === 1;
  141. } catch (e) {
  142. return false;
  143. }
  144. })()
  145. }
  146. };
  147. /**
  148. * Transform an array-like object to a string.
  149. * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform.
  150. * @return {String} the result.
  151. */
  152. function arrayLikeToString(array) {
  153. // Performances notes :
  154. // --------------------
  155. // String.fromCharCode.apply(null, array) is the fastest, see
  156. // see http://jsperf.com/converting-a-uint8array-to-a-string/2
  157. // but the stack is limited (and we can get huge arrays !).
  158. //
  159. // result += String.fromCharCode(array[i]); generate too many strings !
  160. //
  161. // This code is inspired by http://jsperf.com/arraybuffer-to-string-apply-performance/2
  162. // TODO : we now have workers that split the work. Do we still need that ?
  163. var chunk = 65536,
  164. type = exports.getTypeOf(array),
  165. canUseApply = true;
  166. if (type === "uint8array") {
  167. canUseApply = arrayToStringHelper.applyCanBeUsed.uint8array;
  168. } else if (type === "nodebuffer") {
  169. canUseApply = arrayToStringHelper.applyCanBeUsed.nodebuffer;
  170. }
  171. if (canUseApply) {
  172. while (chunk > 1) {
  173. try {
  174. return arrayToStringHelper.stringifyByChunk(array, type, chunk);
  175. } catch (e) {
  176. chunk = Math.floor(chunk / 2);
  177. }
  178. }
  179. }
  180. // no apply or chunk error : slow and painful algorithm
  181. // default browser on android 4.*
  182. return arrayToStringHelper.stringifyByChar(array);
  183. }
  184. exports.applyFromCharCode = arrayLikeToString;
  185. /**
  186. * Copy the data from an array-like to an other array-like.
  187. * @param {Array|ArrayBuffer|Uint8Array|Buffer} arrayFrom the origin array.
  188. * @param {Array|ArrayBuffer|Uint8Array|Buffer} arrayTo the destination array which will be mutated.
  189. * @return {Array|ArrayBuffer|Uint8Array|Buffer} the updated destination array.
  190. */
  191. function arrayLikeToArrayLike(arrayFrom, arrayTo) {
  192. for (var i = 0; i < arrayFrom.length; i++) {
  193. arrayTo[i] = arrayFrom[i];
  194. }
  195. return arrayTo;
  196. }
  197. // a matrix containing functions to transform everything into everything.
  198. var transform = {};
  199. // string to ?
  200. transform["string"] = {
  201. "string": identity,
  202. "array": function(input) {
  203. return stringToArrayLike(input, new Array(input.length));
  204. },
  205. "arraybuffer": function(input) {
  206. return transform["string"]["uint8array"](input).buffer;
  207. },
  208. "uint8array": function(input) {
  209. return stringToArrayLike(input, new Uint8Array(input.length));
  210. },
  211. "nodebuffer": function(input) {
  212. return stringToArrayLike(input, nodejsUtils.allocBuffer(input.length));
  213. }
  214. };
  215. // array to ?
  216. transform["array"] = {
  217. "string": arrayLikeToString,
  218. "array": identity,
  219. "arraybuffer": function(input) {
  220. return (new Uint8Array(input)).buffer;
  221. },
  222. "uint8array": function(input) {
  223. return new Uint8Array(input);
  224. },
  225. "nodebuffer": function(input) {
  226. return nodejsUtils.newBufferFrom(input);
  227. }
  228. };
  229. // arraybuffer to ?
  230. transform["arraybuffer"] = {
  231. "string": function(input) {
  232. return arrayLikeToString(new Uint8Array(input));
  233. },
  234. "array": function(input) {
  235. return arrayLikeToArrayLike(new Uint8Array(input), new Array(input.byteLength));
  236. },
  237. "arraybuffer": identity,
  238. "uint8array": function(input) {
  239. return new Uint8Array(input);
  240. },
  241. "nodebuffer": function(input) {
  242. return nodejsUtils.newBufferFrom(new Uint8Array(input));
  243. }
  244. };
  245. // uint8array to ?
  246. transform["uint8array"] = {
  247. "string": arrayLikeToString,
  248. "array": function(input) {
  249. return arrayLikeToArrayLike(input, new Array(input.length));
  250. },
  251. "arraybuffer": function(input) {
  252. return input.buffer;
  253. },
  254. "uint8array": identity,
  255. "nodebuffer": function(input) {
  256. return nodejsUtils.newBufferFrom(input);
  257. }
  258. };
  259. // nodebuffer to ?
  260. transform["nodebuffer"] = {
  261. "string": arrayLikeToString,
  262. "array": function(input) {
  263. return arrayLikeToArrayLike(input, new Array(input.length));
  264. },
  265. "arraybuffer": function(input) {
  266. return transform["nodebuffer"]["uint8array"](input).buffer;
  267. },
  268. "uint8array": function(input) {
  269. return arrayLikeToArrayLike(input, new Uint8Array(input.length));
  270. },
  271. "nodebuffer": identity
  272. };
  273. /**
  274. * Transform an input into any type.
  275. * The supported output type are : string, array, uint8array, arraybuffer, nodebuffer.
  276. * If no output type is specified, the unmodified input will be returned.
  277. * @param {String} outputType the output type.
  278. * @param {String|Array|ArrayBuffer|Uint8Array|Buffer} input the input to convert.
  279. * @throws {Error} an Error if the browser doesn't support the requested output type.
  280. */
  281. exports.transformTo = function(outputType, input) {
  282. if (!input) {
  283. // undefined, null, etc
  284. // an empty string won't harm.
  285. input = "";
  286. }
  287. if (!outputType) {
  288. return input;
  289. }
  290. exports.checkSupport(outputType);
  291. var inputType = exports.getTypeOf(input);
  292. var result = transform[inputType][outputType](input);
  293. return result;
  294. };
  295. /**
  296. * Resolve all relative path components, "." and "..", in a path. If these relative components
  297. * traverse above the root then the resulting path will only contain the final path component.
  298. *
  299. * All empty components, e.g. "//", are removed.
  300. * @param {string} path A path with / or \ separators
  301. * @returns {string} The path with all relative path components resolved.
  302. */
  303. exports.resolve = function(path) {
  304. var parts = path.split("/");
  305. var result = [];
  306. for (var index = 0; index < parts.length; index++) {
  307. var part = parts[index];
  308. // Allow the first and last component to be empty for trailing slashes.
  309. if (part === "." || (part === "" && index !== 0 && index !== parts.length - 1)) {
  310. continue;
  311. } else if (part === "..") {
  312. result.pop();
  313. } else {
  314. result.push(part);
  315. }
  316. }
  317. return result.join("/");
  318. };
  319. /**
  320. * Return the type of the input.
  321. * The type will be in a format valid for JSZip.utils.transformTo : string, array, uint8array, arraybuffer.
  322. * @param {Object} input the input to identify.
  323. * @return {String} the (lowercase) type of the input.
  324. */
  325. exports.getTypeOf = function(input) {
  326. if (typeof input === "string") {
  327. return "string";
  328. }
  329. if (Object.prototype.toString.call(input) === "[object Array]") {
  330. return "array";
  331. }
  332. if (support.nodebuffer && nodejsUtils.isBuffer(input)) {
  333. return "nodebuffer";
  334. }
  335. if (support.uint8array && input instanceof Uint8Array) {
  336. return "uint8array";
  337. }
  338. if (support.arraybuffer && input instanceof ArrayBuffer) {
  339. return "arraybuffer";
  340. }
  341. };
  342. /**
  343. * Throw an exception if the type is not supported.
  344. * @param {String} type the type to check.
  345. * @throws {Error} an Error if the browser doesn't support the requested type.
  346. */
  347. exports.checkSupport = function(type) {
  348. var supported = support[type.toLowerCase()];
  349. if (!supported) {
  350. throw new Error(type + " is not supported by this platform");
  351. }
  352. };
  353. exports.MAX_VALUE_16BITS = 65535;
  354. exports.MAX_VALUE_32BITS = -1; // well, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" is parsed as -1
  355. /**
  356. * Prettify a string read as binary.
  357. * @param {string} str the string to prettify.
  358. * @return {string} a pretty string.
  359. */
  360. exports.pretty = function(str) {
  361. var res = "",
  362. code, i;
  363. for (i = 0; i < (str || "").length; i++) {
  364. code = str.charCodeAt(i);
  365. res += "\\x" + (code < 16 ? "0" : "") + code.toString(16).toUpperCase();
  366. }
  367. return res;
  368. };
  369. /**
  370. * Defer the call of a function.
  371. * @param {Function} callback the function to call asynchronously.
  372. * @param {Array} args the arguments to give to the callback.
  373. */
  374. exports.delay = function(callback, args, self) {
  375. setImmediate(function () {
  376. callback.apply(self || null, args || []);
  377. });
  378. };
  379. /**
  380. * Extends a prototype with an other, without calling a constructor with
  381. * side effects. Inspired by nodejs' `utils.inherits`
  382. * @param {Function} ctor the constructor to augment
  383. * @param {Function} superCtor the parent constructor to use
  384. */
  385. exports.inherits = function (ctor, superCtor) {
  386. var Obj = function() {};
  387. Obj.prototype = superCtor.prototype;
  388. ctor.prototype = new Obj();
  389. };
  390. /**
  391. * Merge the objects passed as parameters into a new one.
  392. * @private
  393. * @param {...Object} var_args All objects to merge.
  394. * @return {Object} a new object with the data of the others.
  395. */
  396. exports.extend = function() {
  397. var result = {}, i, attr;
  398. for (i = 0; i < arguments.length; i++) { // arguments is not enumerable in some browsers
  399. for (attr in arguments[i]) {
  400. if (Object.prototype.hasOwnProperty.call(arguments[i], attr) && typeof result[attr] === "undefined") {
  401. result[attr] = arguments[i][attr];
  402. }
  403. }
  404. }
  405. return result;
  406. };
  407. /**
  408. * Transform arbitrary content into a Promise.
  409. * @param {String} name a name for the content being processed.
  410. * @param {Object} inputData the content to process.
  411. * @param {Boolean} isBinary true if the content is not an unicode string
  412. * @param {Boolean} isOptimizedBinaryString true if the string content only has one byte per character.
  413. * @param {Boolean} isBase64 true if the string content is encoded with base64.
  414. * @return {Promise} a promise in a format usable by JSZip.
  415. */
  416. exports.prepareContent = function(name, inputData, isBinary, isOptimizedBinaryString, isBase64) {
  417. // if inputData is already a promise, this flatten it.
  418. var promise = external.Promise.resolve(inputData).then(function(data) {
  419. var isBlob = support.blob && (data instanceof Blob || ["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(data)) !== -1);
  420. if (isBlob && typeof FileReader !== "undefined") {
  421. return new external.Promise(function (resolve, reject) {
  422. var reader = new FileReader();
  423. reader.onload = function(e) {
  424. resolve(e.target.result);
  425. };
  426. reader.onerror = function(e) {
  427. reject(e.target.error);
  428. };
  429. reader.readAsArrayBuffer(data);
  430. });
  431. } else {
  432. return data;
  433. }
  434. });
  435. return promise.then(function(data) {
  436. var dataType = exports.getTypeOf(data);
  437. if (!dataType) {
  438. return external.Promise.reject(
  439. new Error("Can't read the data of '" + name + "'. Is it " +
  440. "in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?")
  441. );
  442. }
  443. // special case : it's way easier to work with Uint8Array than with ArrayBuffer
  444. if (dataType === "arraybuffer") {
  445. data = exports.transformTo("uint8array", data);
  446. } else if (dataType === "string") {
  447. if (isBase64) {
  448. data = base64.decode(data);
  449. }
  450. else if (isBinary) {
  451. // optimizedBinaryString === true means that the file has already been filtered with a 0xFF mask
  452. if (isOptimizedBinaryString !== true) {
  453. // this is a string, not in a base64 format.
  454. // Be sure that this is a correct "binary string"
  455. data = string2binary(data);
  456. }
  457. }
  458. }
  459. return data;
  460. });
  461. };