zip.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import crc32 from 'crc/crc32';
  2. const encoder = new TextEncoder();
  3. function dosDateTime(dateTime = new Date()) {
  4. const year = (dateTime.getFullYear() - 1980) << 9;
  5. const month = (dateTime.getMonth() + 1) << 5;
  6. const day = dateTime.getDate();
  7. const date = year | month | day;
  8. const hour = dateTime.getHours() << 11;
  9. const minute = dateTime.getMinutes() << 5;
  10. const second = Math.floor(dateTime.getSeconds() / 2);
  11. const time = hour | minute | second;
  12. return { date, time };
  13. }
  14. class File {
  15. constructor(info) {
  16. this.name = encoder.encode(info.name);
  17. this.size = info.size;
  18. this.bytesRead = 0;
  19. this.crc = null;
  20. this.dateTime = dosDateTime();
  21. }
  22. get header() {
  23. const h = new ArrayBuffer(30 + this.name.byteLength);
  24. const v = new DataView(h);
  25. v.setUint32(0, 0x04034b50, true); // sig
  26. v.setUint16(4, 20, true); // version
  27. v.setUint16(6, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
  28. v.setUint16(8, 0, true); // compression
  29. v.setUint16(10, this.dateTime.time, true); // modified time
  30. v.setUint16(12, this.dateTime.date, true); // modified date
  31. v.setUint32(14, 0, true); // crc32 (in descriptor)
  32. v.setUint32(18, 0, true); // compressed size (in descriptor)
  33. v.setUint32(22, 0, true); // uncompressed size (in descriptor)
  34. v.setUint16(26, this.name.byteLength, true); // name length
  35. v.setUint16(28, 0, true); // extra field length
  36. for (let i = 0; i < this.name.byteLength; i++) {
  37. v.setUint8(30 + i, this.name[i]);
  38. }
  39. return new Uint8Array(h);
  40. }
  41. get dataDescriptor() {
  42. const dd = new ArrayBuffer(16);
  43. const v = new DataView(dd);
  44. v.setUint32(0, 0x08074b50, true); // sig
  45. v.setUint32(4, this.crc, true); // crc32
  46. v.setUint32(8, this.size, true); // compressed size
  47. v.setUint32(12, this.size, true); // uncompressed size
  48. return new Uint8Array(dd);
  49. }
  50. directoryRecord(offset) {
  51. const dr = new ArrayBuffer(46 + this.name.byteLength);
  52. const v = new DataView(dr);
  53. v.setUint32(0, 0x02014b50, true); // sig
  54. v.setUint16(4, 20, true); // version made
  55. v.setUint16(6, 20, true); // version required
  56. v.setUint16(8, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
  57. v.setUint16(10, 0, true); // compression
  58. v.setUint16(12, this.dateTime.time, true); // modified time
  59. v.setUint16(14, this.dateTime.date, true); // modified date
  60. v.setUint32(16, this.crc, true); // crc
  61. v.setUint32(20, this.size, true); // compressed size
  62. v.setUint32(24, this.size, true); // uncompressed size
  63. v.setUint16(28, this.name.byteLength, true); // name length
  64. v.setUint16(30, 0, true); // extra length
  65. v.setUint16(32, 0, true); // comment length
  66. v.setUint16(34, 0, true); // disk number
  67. v.setUint16(36, 0, true); // internal file attrs
  68. v.setUint32(38, 0, true); // external file attrs
  69. v.setUint32(42, offset, true); // file offset
  70. for (let i = 0; i < this.name.byteLength; i++) {
  71. v.setUint8(46 + i, this.name[i]);
  72. }
  73. return new Uint8Array(dr);
  74. }
  75. get byteLength() {
  76. return this.size + this.name.byteLength + 30 + 16;
  77. }
  78. append(data, controller) {
  79. this.bytesRead += data.byteLength;
  80. const endIndex = data.byteLength - Math.max(this.bytesRead - this.size, 0);
  81. const buf = data.slice(0, endIndex);
  82. this.crc = crc32(buf, this.crc);
  83. controller.enqueue(buf);
  84. if (endIndex < data.byteLength) {
  85. return data.slice(endIndex, data.byteLength);
  86. }
  87. }
  88. }
  89. function centralDirectory(files, controller) {
  90. let directoryOffset = 0;
  91. let directorySize = 0;
  92. for (let i = 0; i < files.length; i++) {
  93. const file = files[i];
  94. const record = file.directoryRecord(directoryOffset);
  95. directoryOffset += file.byteLength;
  96. controller.enqueue(record);
  97. directorySize += record.byteLength;
  98. }
  99. controller.enqueue(eod(files.length, directorySize, directoryOffset));
  100. }
  101. function eod(fileCount, directorySize, directoryOffset) {
  102. const e = new ArrayBuffer(22);
  103. const v = new DataView(e);
  104. v.setUint32(0, 0x06054b50, true); // sig
  105. v.setUint16(4, 0, true); // disk number
  106. v.setUint16(6, 0, true); // directory disk
  107. v.setUint16(8, fileCount, true); // number of records
  108. v.setUint16(10, fileCount, true); // total records
  109. v.setUint32(12, directorySize, true); // size of directory
  110. v.setUint32(16, directoryOffset, true); // offset of directory
  111. v.setUint16(20, 0, true); // comment length
  112. return new Uint8Array(e);
  113. }
  114. class ZipStreamController {
  115. constructor(files, source) {
  116. this.files = files;
  117. this.fileIndex = 0;
  118. this.file = null;
  119. this.reader = source.getReader();
  120. this.nextFile();
  121. this.extra = null;
  122. }
  123. nextFile() {
  124. this.file = this.files[this.fileIndex++];
  125. }
  126. async pull(controller) {
  127. if (!this.file) {
  128. // end of archive
  129. centralDirectory(this.files, controller);
  130. return controller.close();
  131. }
  132. if (this.file.bytesRead === 0) {
  133. // beginning of file
  134. controller.enqueue(this.file.header);
  135. if (this.extra) {
  136. this.extra = this.file.append(this.extra, controller);
  137. }
  138. }
  139. if (this.file.bytesRead >= this.file.size) {
  140. // end of file
  141. controller.enqueue(this.file.dataDescriptor);
  142. this.nextFile();
  143. return this.pull(controller);
  144. }
  145. const data = await this.reader.read();
  146. if (data.done) {
  147. this.nextFile();
  148. return this.pull(controller);
  149. }
  150. this.extra = this.file.append(data.value, controller);
  151. }
  152. }
  153. export default class Zip {
  154. constructor(manifest, source) {
  155. this.files = manifest.files.map(info => new File(info));
  156. this.source = source;
  157. }
  158. get stream() {
  159. return new ReadableStream(new ZipStreamController(this.files, this.source));
  160. }
  161. get size() {
  162. const entries = this.files.reduce(
  163. (total, file) => total + file.byteLength * 2 - file.size,
  164. 0
  165. );
  166. const eod = 22;
  167. return entries + eod;
  168. }
  169. }