httputils.js 4.3 KB

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