httputils.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.parseByteRanges = exports.parseContentRange = exports.parseContentType = void 0;
  4. const CRLFCRLF = '\r\n\r\n';
  5. /*
  6. * Shim for 'Object.fromEntries'
  7. */
  8. function itemsToObject(items) {
  9. if (typeof Object.fromEntries !== 'undefined') {
  10. return Object.fromEntries(items);
  11. }
  12. const obj = {};
  13. for (const [key, value] of items) {
  14. obj[key.toLowerCase()] = value;
  15. }
  16. return obj;
  17. }
  18. /**
  19. * Parse HTTP headers from a given string.
  20. * @param {String} text the text to parse the headers from
  21. * @returns {Object} the parsed headers with lowercase keys
  22. */
  23. function parseHeaders(text) {
  24. const items = text
  25. .split('\r\n')
  26. .map((line) => {
  27. const kv = line.split(':').map((str) => str.trim());
  28. kv[0] = kv[0].toLowerCase();
  29. return kv;
  30. });
  31. return itemsToObject(items);
  32. }
  33. /**
  34. * Parse a 'Content-Type' header value to the content-type and parameters
  35. * @param {String} rawContentType the raw string to parse from
  36. * @returns {Object} the parsed content type with the fields: type and params
  37. */
  38. function parseContentType(rawContentType) {
  39. const [type, ...rawParams] = rawContentType.split(';').map((s) => s.trim());
  40. const paramsItems = rawParams.map((param) => param.split('='));
  41. return { type, params: itemsToObject(paramsItems) };
  42. }
  43. exports.parseContentType = parseContentType;
  44. /**
  45. * Parse a 'Content-Range' header value to its start, end, and total parts
  46. * @param {String} rawContentRange the raw string to parse from
  47. * @returns {Object} the parsed parts
  48. */
  49. function parseContentRange(rawContentRange) {
  50. let start;
  51. let end;
  52. let total;
  53. if (rawContentRange) {
  54. [, start, end, total] = rawContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
  55. start = parseInt(start, 10);
  56. end = parseInt(end, 10);
  57. total = parseInt(total, 10);
  58. }
  59. return { start, end, total };
  60. }
  61. exports.parseContentRange = parseContentRange;
  62. /**
  63. * Parses a list of byteranges from the given 'multipart/byteranges' HTTP response.
  64. * Each item in the list has the following properties:
  65. * - headers: the HTTP headers
  66. * - data: the sliced ArrayBuffer for that specific part
  67. * - offset: the offset of the byterange within its originating file
  68. * - length: the length of the byterange
  69. * @param {ArrayBuffer} responseArrayBuffer the response to be parsed and split
  70. * @param {String} boundary the boundary string used to split the sections
  71. * @returns {Object[]} the parsed byteranges
  72. */
  73. function parseByteRanges(responseArrayBuffer, boundary) {
  74. let offset = null;
  75. const decoder = new TextDecoder('ascii');
  76. const out = [];
  77. const startBoundary = `--${boundary}`;
  78. const endBoundary = `${startBoundary}--`;
  79. // search for the initial boundary, may be offset by some bytes
  80. // TODO: more efficient to check for `--` in bytes directly
  81. for (let i = 0; i < 10; ++i) {
  82. const text = decoder.decode(new Uint8Array(responseArrayBuffer, i, startBoundary.length));
  83. if (text === startBoundary) {
  84. offset = i;
  85. }
  86. }
  87. if (offset === null) {
  88. throw new Error('Could not find initial boundary');
  89. }
  90. while (offset < responseArrayBuffer.byteLength) {
  91. const text = decoder.decode(new Uint8Array(responseArrayBuffer, offset, Math.min(startBoundary.length + 1024, responseArrayBuffer.byteLength - offset)));
  92. // break if we arrived at the end
  93. if (text.length === 0 || text.startsWith(endBoundary)) {
  94. break;
  95. }
  96. // assert that we are actually dealing with a byterange and are at the correct offset
  97. if (!text.startsWith(startBoundary)) {
  98. throw new Error('Part does not start with boundary');
  99. }
  100. // get a substring from where we read the headers
  101. const innerText = text.substr(startBoundary.length + 2);
  102. if (innerText.length === 0) {
  103. break;
  104. }
  105. // find the double linebreak that denotes the end of the headers
  106. const endOfHeaders = innerText.indexOf(CRLFCRLF);
  107. // parse the headers to get the content range size
  108. const headers = parseHeaders(innerText.substr(0, endOfHeaders));
  109. const { start, end, total } = parseContentRange(headers['content-range']);
  110. // calculate the length of the slice and the next offset
  111. const startOfData = offset + startBoundary.length + endOfHeaders + CRLFCRLF.length;
  112. const length = parseInt(end, 10) + 1 - parseInt(start, 10);
  113. out.push({
  114. headers,
  115. data: responseArrayBuffer.slice(startOfData, startOfData + length),
  116. offset: start,
  117. length,
  118. fileSize: total,
  119. });
  120. offset = startOfData + length + 4;
  121. }
  122. return out;
  123. }
  124. exports.parseByteRanges = parseByteRanges;
  125. //# sourceMappingURL=httputils.js.map