styled-map-package 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/bin/smp-download.js +1 -1
  2. package/bin/smp-view.js +32 -23
  3. package/dist/download.d.cts +1 -1
  4. package/dist/download.d.ts +1 -1
  5. package/dist/index.d.cts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/reader-watch.d.cts +1 -1
  8. package/dist/reader-watch.d.ts +1 -1
  9. package/dist/reader.cjs +2 -1
  10. package/dist/reader.d.cts +1 -1
  11. package/dist/reader.d.ts +1 -1
  12. package/dist/reader.js +2 -1
  13. package/dist/server.d.cts +1 -1
  14. package/dist/server.d.ts +1 -1
  15. package/dist/style-downloader.cjs +2 -2
  16. package/dist/style-downloader.d.cts +1 -1
  17. package/dist/style-downloader.d.ts +1 -1
  18. package/dist/style-downloader.js +2 -2
  19. package/dist/tile-downloader.cjs +2 -2
  20. package/dist/tile-downloader.d.cts +1 -1
  21. package/dist/tile-downloader.d.ts +1 -1
  22. package/dist/tile-downloader.js +2 -2
  23. package/dist/{types-B4Xn1F9K.d.cts → types-yLQy3AKR.d.cts} +6 -3
  24. package/dist/{types-B4Xn1F9K.d.ts → types-yLQy3AKR.d.ts} +6 -3
  25. package/dist/utils/file-formats.d.cts +1 -1
  26. package/dist/utils/file-formats.d.ts +1 -1
  27. package/dist/utils/style.cjs +2 -2
  28. package/dist/utils/style.d.cts +1 -1
  29. package/dist/utils/style.d.ts +1 -1
  30. package/dist/utils/style.js +2 -2
  31. package/dist/utils/templates.cjs +4 -0
  32. package/dist/utils/templates.d.cts +4 -2
  33. package/dist/utils/templates.d.ts +4 -2
  34. package/dist/utils/templates.js +2 -0
  35. package/dist/writer.cjs +51 -3
  36. package/dist/writer.d.cts +1 -1
  37. package/dist/writer.d.ts +1 -1
  38. package/dist/writer.js +53 -3
  39. package/node_modules/@node-rs/crc32/LICENSE +21 -0
  40. package/node_modules/@node-rs/crc32/README.md +61 -0
  41. package/node_modules/@node-rs/crc32/browser.js +1 -0
  42. package/node_modules/@node-rs/crc32/index.d.ts +5 -0
  43. package/node_modules/@node-rs/crc32/index.js +368 -0
  44. package/node_modules/@node-rs/crc32/package.json +97 -0
  45. package/node_modules/@node-rs/crc32-linux-x64-gnu/README.md +3 -0
  46. package/node_modules/@node-rs/crc32-linux-x64-gnu/crc32.linux-x64-gnu.node +0 -0
  47. package/node_modules/@node-rs/crc32-linux-x64-gnu/package.json +43 -0
  48. package/node_modules/@node-rs/crc32-linux-x64-musl/README.md +3 -0
  49. package/node_modules/@node-rs/crc32-linux-x64-musl/crc32.linux-x64-musl.node +0 -0
  50. package/node_modules/@node-rs/crc32-linux-x64-musl/package.json +43 -0
  51. package/node_modules/define-data-property/.eslintrc +24 -0
  52. package/node_modules/define-data-property/.github/FUNDING.yml +12 -0
  53. package/node_modules/define-data-property/.nycrc +13 -0
  54. package/node_modules/define-data-property/CHANGELOG.md +70 -0
  55. package/node_modules/define-data-property/LICENSE +21 -0
  56. package/node_modules/define-data-property/README.md +67 -0
  57. package/node_modules/define-data-property/index.d.ts +12 -0
  58. package/node_modules/define-data-property/index.js +56 -0
  59. package/node_modules/define-data-property/package.json +106 -0
  60. package/node_modules/define-data-property/test/index.js +392 -0
  61. package/node_modules/define-data-property/tsconfig.json +59 -0
  62. package/node_modules/define-properties/.editorconfig +13 -0
  63. package/node_modules/define-properties/.eslintrc +19 -0
  64. package/node_modules/define-properties/.github/FUNDING.yml +12 -0
  65. package/node_modules/define-properties/.nycrc +9 -0
  66. package/node_modules/define-properties/CHANGELOG.md +91 -0
  67. package/node_modules/define-properties/LICENSE +21 -0
  68. package/node_modules/define-properties/README.md +84 -0
  69. package/node_modules/define-properties/index.js +47 -0
  70. package/node_modules/define-properties/package.json +88 -0
  71. package/node_modules/es-define-property/.eslintrc +13 -0
  72. package/node_modules/es-define-property/.github/FUNDING.yml +12 -0
  73. package/node_modules/es-define-property/.nycrc +9 -0
  74. package/node_modules/es-define-property/CHANGELOG.md +29 -0
  75. package/node_modules/es-define-property/LICENSE +21 -0
  76. package/node_modules/es-define-property/README.md +49 -0
  77. package/node_modules/es-define-property/index.d.ts +3 -0
  78. package/node_modules/es-define-property/index.js +14 -0
  79. package/node_modules/es-define-property/package.json +81 -0
  80. package/node_modules/es-define-property/test/index.js +56 -0
  81. package/node_modules/es-define-property/tsconfig.json +10 -0
  82. package/node_modules/es-errors/.eslintrc +5 -0
  83. package/node_modules/es-errors/.github/FUNDING.yml +12 -0
  84. package/node_modules/es-errors/CHANGELOG.md +40 -0
  85. package/node_modules/es-errors/LICENSE +21 -0
  86. package/node_modules/es-errors/README.md +55 -0
  87. package/node_modules/es-errors/eval.d.ts +3 -0
  88. package/node_modules/es-errors/eval.js +4 -0
  89. package/node_modules/es-errors/index.d.ts +3 -0
  90. package/node_modules/es-errors/index.js +4 -0
  91. package/node_modules/es-errors/package.json +80 -0
  92. package/node_modules/es-errors/range.d.ts +3 -0
  93. package/node_modules/es-errors/range.js +4 -0
  94. package/node_modules/es-errors/ref.d.ts +3 -0
  95. package/node_modules/es-errors/ref.js +4 -0
  96. package/node_modules/es-errors/syntax.d.ts +3 -0
  97. package/node_modules/es-errors/syntax.js +4 -0
  98. package/node_modules/es-errors/test/index.js +19 -0
  99. package/node_modules/es-errors/tsconfig.json +49 -0
  100. package/node_modules/es-errors/type.d.ts +3 -0
  101. package/node_modules/es-errors/type.js +4 -0
  102. package/node_modules/es-errors/uri.d.ts +3 -0
  103. package/node_modules/es-errors/uri.js +4 -0
  104. package/node_modules/globalthis/.eslintrc +18 -0
  105. package/node_modules/globalthis/.nycrc +10 -0
  106. package/node_modules/globalthis/CHANGELOG.md +109 -0
  107. package/node_modules/globalthis/LICENSE +21 -0
  108. package/node_modules/globalthis/README.md +70 -0
  109. package/node_modules/globalthis/auto.js +3 -0
  110. package/node_modules/globalthis/implementation.browser.js +11 -0
  111. package/node_modules/globalthis/implementation.js +3 -0
  112. package/node_modules/globalthis/index.js +19 -0
  113. package/node_modules/globalthis/package.json +99 -0
  114. package/node_modules/globalthis/polyfill.js +10 -0
  115. package/node_modules/globalthis/shim.js +29 -0
  116. package/node_modules/globalthis/test/implementation.js +11 -0
  117. package/node_modules/globalthis/test/index.js +11 -0
  118. package/node_modules/globalthis/test/native.js +26 -0
  119. package/node_modules/globalthis/test/shimmed.js +29 -0
  120. package/node_modules/globalthis/test/tests.js +36 -0
  121. package/node_modules/gopd/.eslintrc +16 -0
  122. package/node_modules/gopd/.github/FUNDING.yml +12 -0
  123. package/node_modules/gopd/CHANGELOG.md +45 -0
  124. package/node_modules/gopd/LICENSE +21 -0
  125. package/node_modules/gopd/README.md +40 -0
  126. package/node_modules/gopd/gOPD.d.ts +1 -0
  127. package/node_modules/gopd/gOPD.js +4 -0
  128. package/node_modules/gopd/index.d.ts +5 -0
  129. package/node_modules/gopd/index.js +15 -0
  130. package/node_modules/gopd/package.json +77 -0
  131. package/node_modules/gopd/test/index.js +36 -0
  132. package/node_modules/gopd/tsconfig.json +9 -0
  133. package/node_modules/has-property-descriptors/.eslintrc +13 -0
  134. package/node_modules/has-property-descriptors/.github/FUNDING.yml +12 -0
  135. package/node_modules/has-property-descriptors/.nycrc +9 -0
  136. package/node_modules/has-property-descriptors/CHANGELOG.md +35 -0
  137. package/node_modules/has-property-descriptors/LICENSE +21 -0
  138. package/node_modules/has-property-descriptors/README.md +43 -0
  139. package/node_modules/has-property-descriptors/index.js +22 -0
  140. package/node_modules/has-property-descriptors/package.json +77 -0
  141. package/node_modules/has-property-descriptors/test/index.js +57 -0
  142. package/node_modules/is-it-type/License +19 -0
  143. package/node_modules/is-it-type/README.md +102 -0
  144. package/node_modules/is-it-type/changelog.md +239 -0
  145. package/node_modules/is-it-type/dist/cjs/is-it-type.js +173 -0
  146. package/node_modules/is-it-type/dist/cjs/is-it-type.js.map +1 -0
  147. package/node_modules/is-it-type/dist/cjs/is-it-type.min.js +2 -0
  148. package/node_modules/is-it-type/dist/cjs/is-it-type.min.js.map +1 -0
  149. package/node_modules/is-it-type/dist/esm/is-it-type.js +143 -0
  150. package/node_modules/is-it-type/dist/esm/is-it-type.js.map +1 -0
  151. package/node_modules/is-it-type/dist/esm/is-it-type.min.js +2 -0
  152. package/node_modules/is-it-type/dist/esm/is-it-type.min.js.map +1 -0
  153. package/node_modules/is-it-type/dist/esm/package.json +3 -0
  154. package/node_modules/is-it-type/dist/umd/is-it-type.js +450 -0
  155. package/node_modules/is-it-type/dist/umd/is-it-type.js.map +1 -0
  156. package/node_modules/is-it-type/dist/umd/is-it-type.min.js +2 -0
  157. package/node_modules/is-it-type/dist/umd/is-it-type.min.js.map +1 -0
  158. package/node_modules/is-it-type/es/index.js +8 -0
  159. package/node_modules/is-it-type/es/package.json +3 -0
  160. package/node_modules/is-it-type/index.js +10 -0
  161. package/node_modules/is-it-type/package.json +87 -0
  162. package/node_modules/is-it-type/src/index.js +169 -0
  163. package/node_modules/object-keys/.editorconfig +13 -0
  164. package/node_modules/object-keys/.eslintrc +17 -0
  165. package/node_modules/object-keys/.travis.yml +277 -0
  166. package/node_modules/object-keys/CHANGELOG.md +232 -0
  167. package/node_modules/object-keys/LICENSE +21 -0
  168. package/node_modules/object-keys/README.md +76 -0
  169. package/node_modules/object-keys/implementation.js +122 -0
  170. package/node_modules/object-keys/index.js +32 -0
  171. package/node_modules/object-keys/isArguments.js +17 -0
  172. package/node_modules/object-keys/package.json +88 -0
  173. package/node_modules/object-keys/test/index.js +5 -0
  174. package/node_modules/simple-invariant/License +19 -0
  175. package/node_modules/simple-invariant/README.md +64 -0
  176. package/node_modules/simple-invariant/changelog.md +31 -0
  177. package/node_modules/simple-invariant/index.js +19 -0
  178. package/node_modules/simple-invariant/package.json +50 -0
  179. package/node_modules/yauzl-promise/License +19 -0
  180. package/node_modules/yauzl-promise/README.md +440 -0
  181. package/node_modules/yauzl-promise/index.js +10 -0
  182. package/node_modules/yauzl-promise/lib/entry.js +312 -0
  183. package/node_modules/yauzl-promise/lib/index.js +160 -0
  184. package/node_modules/yauzl-promise/lib/reader.js +289 -0
  185. package/node_modules/yauzl-promise/lib/shared.js +20 -0
  186. package/node_modules/yauzl-promise/lib/utils.js +105 -0
  187. package/node_modules/yauzl-promise/lib/zip.js +1224 -0
  188. package/node_modules/yauzl-promise/package.json +56 -0
  189. package/package.json +9 -11
@@ -0,0 +1,1224 @@
1
+ /* --------------------
2
+ * yauzl-promise module
3
+ * `Zip` class
4
+ * ------------------*/
5
+
6
+ /* global WeakRef */
7
+
8
+ 'use strict';
9
+
10
+ // Modules
11
+ const calculateCrc32 = require('@node-rs/crc32').crc32,
12
+ assert = require('simple-invariant'),
13
+ {isPositiveIntegerOrZero} = require('is-it-type');
14
+
15
+ // Imports
16
+ const Entry = require('./entry.js'),
17
+ {INTERNAL_SYMBOL, uncertainUncompressedSizeEntriesRegistry} = require('./shared.js'),
18
+ {decodeBuffer, validateFilename, readUInt64LE} = require('./utils.js');
19
+
20
+ // Exports
21
+
22
+ // Spec of ZIP format is here: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
23
+ // Also: https://libzip.org/specifications/appnote_iz.txt
24
+
25
+ const EOCDR_WITHOUT_COMMENT_SIZE = 22,
26
+ MAX_EOCDR_COMMENT_SIZE = 0xFFFF,
27
+ MAC_CDH_EXTRA_FIELD_ID = 22613,
28
+ MAC_CDH_EXTRA_FIELD_LENGTH = 8,
29
+ MAC_CDH_EXTRA_FIELDS_LENGTH = MAC_CDH_EXTRA_FIELD_LENGTH + 4, // Field data + ID + len (2 bytes each)
30
+ MAC_LFH_EXTRA_FIELDS_LENGTH = 16,
31
+ CDH_MIN_LENGTH = 46,
32
+ CDH_MAX_LENGTH = CDH_MIN_LENGTH + 0xFFFF * 3, // 3 = Filename, extra fields, comment
33
+ CDH_MAX_LENGTH_MAC = CDH_MIN_LENGTH + 0xFFFF + MAC_CDH_EXTRA_FIELDS_LENGTH, // No comment
34
+ FOUR_GIB = 0x100000000; // Math.pow(2, 32)
35
+
36
+ class Zip {
37
+ /**
38
+ * Class representing ZIP file.
39
+ * Class is exported in public interface, for purpose of `instanceof` checks, but constructor cannot
40
+ * be called by user. This is enforced by use of private symbol `INTERNAL_SYMBOL`.
41
+ * @class
42
+ * @param {Object} testSymbol - Must be `INTERNAL_SYMBOL`
43
+ * @param {Object} reader - `Reader` to use to access the ZIP
44
+ * @param {number} size - Size of ZIP file in bytes
45
+ * @param {Object} options - Options
46
+ * @param {boolean} [options.decodeStrings=true] - Decode filenames and comments to strings
47
+ * @param {boolean} [options.validateEntrySizes=true] - Validate entry sizes
48
+ * @param {boolean} [options.validateFilenames=true] - Validate filenames
49
+ * @param {boolean} [options.strictFilenames=false] - Don't allow backslashes (`\`) in filenames
50
+ * @param {boolean} [options.supportMacArchive=true] - Support Mac OS Archive Utility faulty ZIP files
51
+ */
52
+ constructor(testSymbol, reader, size, options) {
53
+ assert(
54
+ testSymbol === INTERNAL_SYMBOL,
55
+ 'Zip class cannot be instantiated directly. Use one of the static methods.'
56
+ );
57
+
58
+ this.reader = reader;
59
+ this.size = size;
60
+ Object.assign(this, options);
61
+ this.isZip64 = null;
62
+ this.entryCount = null;
63
+ this.entryCountIsCertain = true;
64
+ this.footerOffset = null;
65
+ this.centralDirectoryOffset = null;
66
+ this.centralDirectorySize = null;
67
+ this.centralDirectorySizeIsCertain = true;
68
+ this.comment = null;
69
+ this.numEntriesRead = 0;
70
+ this.isMacArchive = false;
71
+ this.isMaybeMacArchive = false;
72
+ this.compressedSizesAreCertain = true;
73
+ this.uncompressedSizesAreCertain = true;
74
+ this._isReading = false;
75
+ this._entryCursor = null;
76
+ this._fileCursor = null;
77
+ this._uncertainUncompressedSizeEntryRefs = null;
78
+ this._firstEntryProps = null;
79
+ }
80
+
81
+ /**
82
+ * Close ZIP file. Underlying reader will be closed.
83
+ * @async
84
+ * @returns {undefined}
85
+ */
86
+ close() {
87
+ return this.reader.close();
88
+ }
89
+
90
+ /**
91
+ * Getter for whether `Zip` is open for reading.
92
+ * @returns {boolean} - `true` if open
93
+ */
94
+ get isOpen() {
95
+ return this.reader.isOpen;
96
+ }
97
+
98
+ /**
99
+ * Locate Central Directory.
100
+ * @async
101
+ * @returns {undefined}
102
+ */
103
+ async _init() {
104
+ // Parse End of Central Directory Record + ZIP64 extension
105
+ // to get location of the Central Directory
106
+ const eocdrBuffer = await this._locateEocdr();
107
+ this._parseEocdr(eocdrBuffer);
108
+ if (this.isZip64) await this._parseZip64Eocdr();
109
+ await this._locateCentralDirectory();
110
+ this._entryCursor = this.centralDirectoryOffset;
111
+ }
112
+
113
+ /**
114
+ * Locate End of Central Directory Record.
115
+ * @async
116
+ * @returns {Buffer} - Buffer containing EOCDR
117
+ */
118
+ async _locateEocdr() {
119
+ // Last field of the End of Central Directory Record is a variable-length comment.
120
+ // The comment size is encoded in a 2-byte field in the EOCDR, which we can't find without trudging
121
+ // backwards through the comment to find it.
122
+ // As a consequence of this design decision, it's possible to have ambiguous ZIP file metadata
123
+ // if a coherent EOCDR was in the comment.
124
+ // Search backwards for a EOCDR signature.
125
+ let bufferSize = EOCDR_WITHOUT_COMMENT_SIZE + MAX_EOCDR_COMMENT_SIZE;
126
+ if (this.size < bufferSize) {
127
+ assert(this.size >= EOCDR_WITHOUT_COMMENT_SIZE, 'End of Central Directory Record not found');
128
+ bufferSize = this.size;
129
+ }
130
+ const bufferOffset = this.size - bufferSize;
131
+ const buffer = await this.reader.read(bufferOffset, bufferSize);
132
+ let pos;
133
+ for (pos = bufferSize - EOCDR_WITHOUT_COMMENT_SIZE; pos >= 0; pos--) {
134
+ if (buffer[pos] !== 0x50) continue;
135
+ if (buffer.readUInt32LE(pos) !== 0x06054b50) continue;
136
+
137
+ const commentLength = buffer.readUInt16LE(pos + 20);
138
+ if (commentLength === bufferSize - pos - EOCDR_WITHOUT_COMMENT_SIZE) {
139
+ this.footerOffset = bufferOffset + pos;
140
+ return buffer.subarray(pos);
141
+ }
142
+ }
143
+ throw new Error('End of Central Directory Record not found');
144
+ }
145
+
146
+ /**
147
+ * Parse End of Central Directory Record.
148
+ * Get Central Directory location, size and entry count.
149
+ * @param {Buffer} eocdrBuffer - Buffer containing EOCDR
150
+ * @returns {undefined}
151
+ */
152
+ _parseEocdr(eocdrBuffer) {
153
+ // Bytes 0-3: End of Central Directory Record signature = 0x06054b50
154
+ // Bytes 4-5: Number of this disk
155
+ const diskNumber = eocdrBuffer.readUInt16LE(4);
156
+ assert(diskNumber === 0, 'Multi-disk ZIP files are not supported');
157
+ // Bytes 6-7: Disk where Central Directory starts
158
+ // Bytes 8-9: Number of Central Directory records on this disk
159
+ // Bytes 10-11: Total number of Central Directory records
160
+ this.entryCount = eocdrBuffer.readUInt16LE(10);
161
+ // Bytes 12-15: Size of Central Directory (bytes)
162
+ this.centralDirectorySize = eocdrBuffer.readUInt32LE(12);
163
+ // Bytes 16-19: Offset of Central Directory
164
+ this.centralDirectoryOffset = eocdrBuffer.readUInt32LE(16);
165
+ // Bytes 22-...: Comment. Encoding is always CP437.
166
+ // Copy buffer instead of slicing, so rest of buffer can be garbage collected.
167
+ this.comment = this.decodeStrings
168
+ ? decodeBuffer(eocdrBuffer, 22, false)
169
+ : Buffer.from(eocdrBuffer.subarray(22));
170
+
171
+ // Original Yauzl does not check `centralDirectorySize` here, only offset, though ZIP spec suggests
172
+ // both should be checked. I suspect this is a bug in Yauzl, and it has remained undiscovered
173
+ // because ZIP files with a Central Directory > 4 GiB are vanishingly rare
174
+ // (would require millions of files, or thousands of files with very long filenames/comments).
175
+ this.isZip64 = this.entryCount === 0xFFFF || this.centralDirectoryOffset === 0xFFFFFFFF
176
+ || this.centralDirectorySize === 0xFFFFFFFF;
177
+ }
178
+
179
+ /**
180
+ * Parse ZIP64 End of Central Directory Locator + Record.
181
+ * Get Central Directory location, size and entry count, where ZIP64 extension used.
182
+ * @async
183
+ * @returns {undefined}
184
+ */
185
+ async _parseZip64Eocdr() {
186
+ // Parse ZIP64 End of Central Directory Locator
187
+ const zip64EocdlOffset = this.footerOffset - 20;
188
+ assert(zip64EocdlOffset >= 0, 'Cannot locate ZIP64 End of Central Directory Locator');
189
+ const zip64EocdlBuffer = await this.reader.read(zip64EocdlOffset, 20);
190
+ // Bytes 0-3: ZIP64 End of Central Directory Locator signature = 0x07064b50
191
+ if (zip64EocdlBuffer.readUInt32LE(0) !== 0x07064b50) {
192
+ if (this.supportMacArchive) {
193
+ // Assume this is a faulty Mac OS archive which happens to have entry count of 65535 (possible)
194
+ // or Central Directory size/offset of 4 GiB - 1 (much less likely, but possible).
195
+ // If it's not, we'll get another error when trying to read the Central Directory.
196
+ this.isMacArchive = true;
197
+ return;
198
+ }
199
+ throw new Error('Invalid ZIP64 End of Central Directory Locator signature');
200
+ }
201
+ // Bytes 4-7 - Number of the disk with the start of the ZIP64 End of Central Directory Record
202
+ // Bytes 8-15: Position of ZIP64 End of Central Directory Record
203
+ const zip64EocdrOffset = readUInt64LE(zip64EocdlBuffer, 8);
204
+ // Bytes 16-19: Total number of disks
205
+
206
+ // Parse ZIP64 End of Central Directory Record
207
+ assert(
208
+ zip64EocdrOffset + 56 <= zip64EocdlOffset,
209
+ 'Cannot locate ZIP64 End of Central Directory Record'
210
+ );
211
+ const zip64EocdrBuffer = await this.reader.read(zip64EocdrOffset, 56);
212
+ // Bytes 0-3: ZIP64 End of Central Directory Record signature = 0x06064b50
213
+ assert(
214
+ zip64EocdrBuffer.readUInt32LE(0) === 0x06064b50,
215
+ 'Invalid ZIP64 End of Central Directory Record signature'
216
+ );
217
+ // Bytes 4-11: Size of ZIP64 End of Central Directory Record (not inc first 12 bytes)
218
+ const zip64EocdrSize = readUInt64LE(zip64EocdrBuffer, 4);
219
+ assert(
220
+ zip64EocdrOffset + zip64EocdrSize + 12 <= zip64EocdlOffset,
221
+ 'Invalid ZIP64 End of Central Directory Record'
222
+ );
223
+ // Bytes 12-13: Version made by
224
+ // Bytes 14-15: Version needed to extract
225
+ // Bytes 16-19: Number of this disk
226
+ // Bytes 20-23: Number of the disk with the start of the Central Directory
227
+ // Bytes 24-31: Total number of entries in the Central Directory on this disk
228
+ // Bytes 32-39: Total number of entries in the Central Directory
229
+ // Spec: "If an archive is in ZIP64 format and the value in this field is 0xFFFF, the size
230
+ // will be in the corresponding 8 byte zip64 end of central directory field."
231
+ // Original Yauzl expects correct entry count to always be recorded in ZIP64 EOCDR,
232
+ // but have altered that here to be more spec-compliant. Ditto Central Directory size + offset.
233
+ if (this.entryCount === 0xFFFF) this.entryCount = readUInt64LE(zip64EocdrBuffer, 32);
234
+ // Bytes 40-47: Size of the Central Directory
235
+ if (this.centralDirectorySize === 0xFFFFFFFF) {
236
+ this.centralDirectorySize = readUInt64LE(zip64EocdrBuffer, 40);
237
+ }
238
+ // Bytes 48-55: Offset of start of Central Directory with respect to the starting disk number
239
+ if (this.centralDirectoryOffset === 0xFFFFFFFF) {
240
+ this.centralDirectoryOffset = readUInt64LE(zip64EocdrBuffer, 48);
241
+ }
242
+ // Bytes 56-...: ZIP64 extensible data sector
243
+
244
+ // Record offset of start of footers.
245
+ // Either start of ZIP64 EOCDR (if it butts up to ZIP64 EOCDL), or ZIP64 EOCDL.
246
+ this.footerOffset = zip64EocdrOffset + zip64EocdrSize === zip64EocdlOffset
247
+ ? zip64EocdrOffset
248
+ : zip64EocdlOffset;
249
+ }
250
+
251
+ /**
252
+ * Locate Central Directory.
253
+ *
254
+ * In a well-formed ZIP file, the EOCDR accurately gives us the offset and size of Central
255
+ * Directory, and the entry count.
256
+ *
257
+ * However Mac OS Archive Utility, instead of using ZIP64 extension to record Central Directory
258
+ * offset or size >= 4 GiB, or entry count >= 65536, truncates size and offset to lower 32 bits,
259
+ * and entry count to lower 16 bits.
260
+ * i.e.:
261
+ * Actual offset = reported offset + n * (1 << 32)
262
+ * Actual size = reported size + m * (1 << 32)
263
+ * Actual entry count = reported entry count + o * (1 << 16)
264
+ * (where `n`, `m` and `o` are unknown)
265
+ *
266
+ * Identify if this may be a faulty Mac OS Archive Utility ZIP. If so, find the actual location of
267
+ * the Central Directory. Deduce which of above properties cannot be known with certainty.
268
+ *
269
+ * In some cases, it's not possible to immediately determine if a ZIP is definitely a Mac OS ZIP.
270
+ * If it may be, but not sure yet, record which properties are unknown at present.
271
+ * Later calls to `readEntry()` or `openReadStream()` will reveal more about the ZIP, and the
272
+ * determinaton of whether ZIP is a faulty Mac OS ZIP or not will be made then.
273
+ *
274
+ * Try to do this while ensuring a spec-compliant ZIP will never be misinterpretted.
275
+ *
276
+ * @async
277
+ * @returns {undefined}
278
+ */
279
+ async _locateCentralDirectory() {
280
+ // Skip this if Mac OS Archive Utility support disabled
281
+ if (!this.supportMacArchive) return;
282
+
283
+ // Mac OS archives don't use ZIP64 extension
284
+ if (this.isZip64) return;
285
+
286
+ // Mac Archives do not contain comment after End of Central Directory Record
287
+ if (this.size - this.footerOffset !== EOCDR_WITHOUT_COMMENT_SIZE) return;
288
+
289
+ // Mac Archives do not have gap between end of last Central Directory Header and start of EOCDR
290
+ let centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
291
+ if (centralDirectoryEnd % FOUR_GIB !== this.footerOffset % FOUR_GIB) return;
292
+
293
+ // If claims to have no entries, and there's no room for any, this must be accurate.
294
+ // Handle this here to avoid trying to read beyond end of file.
295
+ if (this.entryCount === 0 && this.centralDirectoryOffset + CDH_MIN_LENGTH > this.footerOffset) {
296
+ assert(this.centralDirectorySize === 0, 'Inconsistent Central Directory size and entry count');
297
+ return;
298
+ }
299
+
300
+ // Ensure size and entry count comply with each other and adjust if they don't
301
+ if (this.centralDirectorySize < this.entryCount * CDH_MIN_LENGTH) {
302
+ // Central Directory size is too small to contain `entryCount` entries. Must be Mac OS ZIP.
303
+ // Check is room to grow Central Directory, and grow it up to EOCDR.
304
+ assert(
305
+ centralDirectoryEnd < this.footerOffset,
306
+ 'Inconsistent Central Directory size and entry count'
307
+ );
308
+ this.isMacArchive = true;
309
+ centralDirectoryEnd = this.footerOffset;
310
+ this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
311
+ }
312
+
313
+ if (this._recalculateEntryCount(0, this.centralDirectoryOffset)) {
314
+ // Entry count was too small. Must be Mac OS ZIP.
315
+ this.isMacArchive = true;
316
+ }
317
+
318
+ // Unless we already know this is a Mac ZIP, check if Central Directory is where EOCDR says it is
319
+ // (if we know it's a Mac ZIP, better to look in last possible position first)
320
+ let entry, alreadyCheckedOffset;
321
+ if (!this.isMacArchive) {
322
+ entry = await this._readEntryAt(this.centralDirectoryOffset);
323
+
324
+ // If found a non-Mac Central Directory Header, exit - it's not a Mac archive
325
+ if (entry && !firstEntryMaybeMac(entry)) {
326
+ assert(this.entryCount > 0, 'Inconsistent Central Directory size and entry count');
327
+
328
+ // Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
329
+ this._firstEntryProps = entry;
330
+ return;
331
+ }
332
+
333
+ alreadyCheckedOffset = this.centralDirectoryOffset;
334
+ } else {
335
+ alreadyCheckedOffset = -1;
336
+ }
337
+
338
+ // If no Central Directory found where it should be, this ZIP is either:
339
+ // 1. Valid ZIP with no entries
340
+ // 2. Faulty Mac OS Archive Utility ZIP
341
+ // 3. Invalid ZIP
342
+ // If it's an invalid ZIP, all bets are off, so ignore that possibility.
343
+ // Try to locate Central Directory in possible locations it could be if this is
344
+ // a Mac OS Archive Utility ZIP (`centralDirectoryOffset + n * FOUR_GIB` where `n` is unknown).
345
+ // It's more common to have a ZIP containing large files, than a ZIP with
346
+ // so many files that the Central Directory is 4 GiB+ in size (likely requiring millions of files).
347
+ // So start with last possible position and work backwards towards start of file.
348
+ if (!entry) {
349
+ // Find last possible offset for Central Directory
350
+ let offset = this.footerOffset
351
+ - Math.max(this.centralDirectorySize, this.entryCount * CDH_MIN_LENGTH);
352
+ if (offset % FOUR_GIB < this.centralDirectoryOffset) {
353
+ assert(offset >= FOUR_GIB, 'Inconsistent Central Directory size and entry count');
354
+ offset -= FOUR_GIB;
355
+ }
356
+ offset = Math.floor(offset / FOUR_GIB) * FOUR_GIB + this.centralDirectoryOffset;
357
+
358
+ // Search for Central Directory
359
+ while (offset > alreadyCheckedOffset) {
360
+ entry = await this._readEntryAt(offset);
361
+ if (entry) {
362
+ assert(firstEntryMaybeMac(entry), 'Cannot locate Central Directory');
363
+ this.isMacArchive = true;
364
+ this.centralDirectoryOffset = offset;
365
+ break;
366
+ }
367
+
368
+ offset -= FOUR_GIB;
369
+ }
370
+ }
371
+
372
+ // If couldn't find Central Directory, it's a faulty ZIP, unless it has 0 entries
373
+ if (!entry) {
374
+ assert(
375
+ this.entryCount === 0 && this.centralDirectorySize === 0,
376
+ 'Cannot locate Central Directory'
377
+ );
378
+ return;
379
+ }
380
+
381
+ // We've found Central Directory, and it is likely to be Mac OS ZIP, but we may not know for sure.
382
+ // If reported entry count was 0, but Central Directory found, must be a Mac OS ZIP.
383
+ if (this.entryCount === 0) this.isMacArchive = true;
384
+
385
+ if (this.isMacArchive) {
386
+ // We know for sure this is a Mac OS Archive Utility ZIP,
387
+ // because some of the size/offset/entry count data has proved faulty.
388
+ // Mac OS ZIPs always have Central Directory going all the way up to the EOCDR.
389
+ centralDirectoryEnd = this.footerOffset;
390
+ this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
391
+ assert(this.centralDirectorySize > 0, 'Inconsistent Central Directory size and entry count');
392
+
393
+ // Recalculate minimum entry count
394
+ this._recalculateEntryCount(1, entry.entryEnd);
395
+
396
+ // Calculate if possible for one or more files to be 4 GiB larger than reported.
397
+ // Each entry takes at minimum 30 bytes for Local File Header + 16 bytes for Data Descriptor.
398
+ // Mac Archives repeat same filename in Local File Header as in Central Directory.
399
+ // Mac Archives contain 16 bytes Extra Fields in Local File Header if CDH contains an Extra Field.
400
+ // So minimum size occupied by first file can be included in this calculation.
401
+ const minTotalDataSize = this.entryCount * 46
402
+ + entry.compressedSize
403
+ + entry.filename.length
404
+ + entry.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
405
+ if (minTotalDataSize + FOUR_GIB <= this.centralDirectoryOffset) {
406
+ this.compressedSizesAreCertain = false;
407
+ }
408
+ } else {
409
+ // ZIP has Central Directory where it should be, and format of first entry is consistent
410
+ // with this being a Mac OS ZIP, but we don't know for sure that it is
411
+ this.isMaybeMacArchive = true;
412
+ if (centralDirectoryEnd < this.footerOffset) {
413
+ // There's room for Central Directory to be 4 GiB or more bigger than reported.
414
+ // This implies entry count is uncertain too. An extra 4 GiB could fit up to ~9 million entries.
415
+ this.centralDirectorySizeIsCertain = false;
416
+ this.entryCountIsCertain = false;
417
+ } else {
418
+ // Recalculate minimum entry count
419
+ this._recalculateEntryCount(1, entry.entryEnd);
420
+ }
421
+
422
+ // Init set of uncertain uncompressed size entries
423
+ this._uncertainUncompressedSizeEntryRefs = new Set();
424
+ }
425
+
426
+ // Check if entry count could be higher than EOCDR says it is
427
+ if (
428
+ this.entryCountIsCertain
429
+ && !entryCountIsCertain(this.entryCount - 1, centralDirectoryEnd - entry.entryEnd)
430
+ ) this.entryCountIsCertain = false;
431
+
432
+ // Even if compressed file sizes are certain, uncompressed file sizes remain uncertain
433
+ // because a file could be < 4 GiB compressed, but >= 4 GiB uncompressed
434
+ this.uncompressedSizesAreCertain = false;
435
+
436
+ // Init local file header cursor
437
+ this._fileCursor = 0;
438
+
439
+ // Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
440
+ this._firstEntryProps = entry;
441
+ }
442
+
443
+ /**
444
+ * Get next entry.
445
+ * @async
446
+ * @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
447
+ */
448
+ async readEntry() {
449
+ assert(!this._isReading, 'Cannot call `readEntry()` before previous call\'s promise has settled');
450
+ this._isReading = true;
451
+ try {
452
+ return await this._readEntry();
453
+ } finally {
454
+ this._isReading = false;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Get next entry.
460
+ * Implementation for `readEntry()`. Should not be called directly.
461
+ * @async
462
+ * @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
463
+ */
464
+ async _readEntry() {
465
+ if (this.numEntriesRead === this.entryCount && this.entryCountIsCertain) return null;
466
+
467
+ // Read Central Directory entry properties (or use the one already read)
468
+ let entryProps = this._firstEntryProps,
469
+ entryEnd;
470
+ if (entryProps) {
471
+ this._firstEntryProps = null;
472
+ entryEnd = entryProps.entryEnd;
473
+ } else {
474
+ entryProps = await this._readEntryAt(this._entryCursor);
475
+
476
+ const centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
477
+ if (!entryProps) {
478
+ // Only way to get here if the ZIP file isn't corrupt is if Central Directory size wasn't
479
+ // certain, and therefore entry count wasn't certain either, so we weren't sure if this was
480
+ // the end or not. If we've reached end of reported entries, and are at end of reported
481
+ // Central Directory, then there being no entry means the Central Directory entry size
482
+ // and entry count are accurate, and this is indeed the end.
483
+ // That implies this can't be a Mac ZIP, because Central Directory doesn't go up to EOCDR.
484
+ // NB: No need to check for `this.centralDirectorySizeIsCertain === false` because if that
485
+ // was the case, `this.entryCountIsCertain` would be `false` too, and we wouldn't be here.
486
+ assert(
487
+ !this.isMacArchive && this.numEntriesRead === this.entryCount
488
+ && this._entryCursor === centralDirectoryEnd,
489
+ 'Invalid Central Directory File Header signature'
490
+ );
491
+ // `isMaybeMacArchive` must have been `true` at start of this function, but check it here
492
+ // just in case it was already changed in a call to `openReadStream()` made by user while async
493
+ // `_readEntryAt()` call above was executing.
494
+ if (this.isMaybeMacArchive) this._setAsNotMacArchive();
495
+ return null;
496
+ }
497
+
498
+ entryEnd = entryProps.entryEnd;
499
+ if (this.isMacArchive) {
500
+ // Properties have been found to be inconsistent already, signalling a Mac OS ZIP.
501
+ // So all entries must be Mac-type, or it isn't a Mac ZIP after all, and is corrupt.
502
+ // File data is tightly packed in Mac OS ZIPs with no gaps in between.
503
+ assert(
504
+ entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
505
+ 'Inconsistent Central Directory structure'
506
+ );
507
+ entryProps.fileHeaderOffset = this._fileCursor;
508
+
509
+ if (!this.entryCountIsCertain) {
510
+ this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd);
511
+ this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
512
+ }
513
+ } else if (this.isMaybeMacArchive) {
514
+ if (this._fileCursor >= FOUR_GIB) {
515
+ // This ZIP is flagged as maybe Mac which means all data up to `_fileCursor`
516
+ // has been consumed by previous files.
517
+ // `fileHeaderOffset` is 32 bit (so < 4 GiB), and `_fileCursor` > 4 GiB, so either
518
+ // 1. file data for this entry covers data already consumed (invalid, possible ZIP bomb)
519
+ // or 2. this must be a Mac ZIP and `fileHeaderOffset` is more than stated.
520
+ assert(
521
+ entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
522
+ 'Inconsistent Central Directory structure'
523
+ );
524
+ this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
525
+ } else if (!entryMaybeMac(entryProps) || entryProps.fileHeaderOffset !== this._fileCursor) {
526
+ // Entry doesn't match signature of Mac entries, or file header is not where it would be
527
+ // in a Mac ZIP, so it can't be one
528
+ this._setAsNotMacArchive();
529
+
530
+ // If entries were meant to be exhausted, there's an error somewhere
531
+ assert(this.numEntriesRead !== this.entryCount, 'Central Directory contains too many entries');
532
+ } else if (!this.centralDirectorySizeIsCertain && (
533
+ entryEnd + (this.entryCount - this.numEntriesRead - 1) * CDH_MIN_LENGTH > centralDirectoryEnd
534
+ )) {
535
+ // Not enough space in Central Directory for number of entries remaining,
536
+ // so this must be a Mac ZIP. Grow Central Directory.
537
+ this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
538
+ } else if (!this.entryCountIsCertain) {
539
+ // Recalculate if entry count is now impossibly low
540
+ if (this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd)) {
541
+ // Entry count was impossibly low for size of Central Directory so this must be Mac ZIP
542
+ this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
543
+ } else if (this.centralDirectorySizeIsCertain) {
544
+ // Check if entry count is now high enough vs remaining Central Directory space
545
+ // that it can't be any larger
546
+ this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
547
+ }
548
+ }
549
+ }
550
+ }
551
+
552
+ // Calculate what location of file data will be if this is a Mac OS ZIP.
553
+ // Mac OS ZIPs always contain Local File Header of 30 bytes
554
+ // + same filename as in Central Directory entry
555
+ // + 16 bytes Extra Fields if Central Directory entry has extra fields.
556
+ const fileDataOffsetIfMac = entryProps.fileHeaderOffset + 30 + entryProps.filename.length
557
+ + entryProps.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
558
+
559
+ // Determine if possible for compressed data to be larger than reported,
560
+ // and, if so, the actual compressed size
561
+ if (!this.compressedSizesAreCertain) {
562
+ const isNowCertain = await this._determineCompressedSize(entryProps, fileDataOffsetIfMac);
563
+ if (isNowCertain) this.compressedSizesAreCertain = true;
564
+ }
565
+
566
+ // Determine if possible for this entry's uncompressed size to be larger than reported
567
+ if (!this.uncompressedSizesAreCertain) {
568
+ if (entryProps.compressionMethod === 0) {
569
+ // No compression - uncompressed size always equal to compressed.
570
+ // NB: We know encryption is not enabled as entry would have been flagged as non-Mac if it was.
571
+ entryProps.uncompressedSize = entryProps.compressedSize;
572
+ } else if (entryProps.compressionMethod !== 8) {
573
+ // Not Deflate compression - no idea what uncompressed size could be
574
+ entryProps.uncompressedSizeIsCertain = false;
575
+ } else {
576
+ // Deflate compression. Maximum compression ratio is 1032.
577
+ // https://stackoverflow.com/questions/16792189/gzip-compression-ratio-for-zeros/16794960#16794960
578
+ const maxUncompressedSize = entryProps.compressedSize * 1032;
579
+ if (
580
+ maxUncompressedSize > FOUR_GIB * 2
581
+ || (
582
+ maxUncompressedSize > FOUR_GIB
583
+ && maxUncompressedSize % FOUR_GIB > entryProps.uncompressedSize
584
+ )
585
+ ) entryProps.uncompressedSizeIsCertain = false;
586
+ }
587
+ }
588
+
589
+ // Create entry object + advance cursor to next entry
590
+ const entry = this._validateAndDecodeEntry(entryProps);
591
+ this._entryCursor = entryEnd;
592
+ this.numEntriesRead++;
593
+
594
+ if (this.isMacArchive || this.isMaybeMacArchive) {
595
+ // Record offset of where next Local File Header will be if this is a Mac OS ZIP.
596
+ // 16 bytes for Data Descriptor after file data, unless it's a folder, empty file, or symlink.
597
+ this._fileCursor = fileDataOffsetIfMac + entry.compressedSize
598
+ + (entryProps.compressionMethod === 8) * 16;
599
+
600
+ if (this.isMacArchive) {
601
+ // We know offset of file data for sure, so record it
602
+ entry.fileDataOffset = fileDataOffsetIfMac;
603
+ } else if (!entry.uncompressedSizeIsCertain) {
604
+ // This is a suspected Mac OS ZIP (but not for sure), and uncompressed size is uncertain.
605
+ // Record entry, so that if ZIP turns out not to be a Mac OS ZIP later,
606
+ // `uncompressedSizeIsCertain` can be changed to `true`.
607
+ // Entries are recorded as `WeakRef`s, to allow them to be garbage collected.
608
+ // The entry is also added to a `FinalizationRegistry`, which removes the ref from the set
609
+ // when entry object is garbage collected. This should prevent escalating memory usage
610
+ // if lots of entries.
611
+ const ref = new WeakRef(entry);
612
+ entry._ref = ref;
613
+ this._uncertainUncompressedSizeEntryRefs.add(ref);
614
+ uncertainUncompressedSizeEntriesRegistry.register(entry, {zip: this, ref}, ref);
615
+ }
616
+ }
617
+
618
+ // Return `Entry` object
619
+ return entry;
620
+ }
621
+
622
+ /**
623
+ * Determine actual compressed size of entry.
624
+ * Update `compressedSize` if it's not what was reported.
625
+ * Return whether *all future* entries have certain compressed size.
626
+ *
627
+ * This method should only be called if this is a Mac ZIP, or possibly a Mac ZIP.
628
+ * i.e. Compressed sizes are not certain to be as reported in the ZIP.
629
+ *
630
+ * First attempt to prove that size can be known with certainty without reading from ZIP file.
631
+ * If that's not possible, search ZIP file for the Data Descriptor which follows file data.
632
+ *
633
+ * Care has to be taken to avoid data races, because this function contains async IO calls,
634
+ * and possible for user to call `openReadStream()` on another Entry, or an event on a stream
635
+ * already in process to cause the ZIP to be identified as definitely Mac or definitely not Mac
636
+ * during this function's async calls.
637
+ *
638
+ * @param {Object} entryProps - Entry properties
639
+ * @param {number} fileDataOffsetIfMac - If ZIP is a Mac OS ZIP, offset file data will start at
640
+ * @returns {boolean} - `true` if all later entry compressed sizes must be certain
641
+ */
642
+ async _determineCompressedSize(entryProps, fileDataOffsetIfMac) {
643
+ // ZIP may only be a suspected Mac OS ZIP, rather than definitely one.
644
+ // However, we can assume it is a Mac ZIP for purposes of calculations here,
645
+ // as if actually it's not, compressed size of all entries is certain anyway.
646
+ //
647
+ // In a Mac ZIP:
648
+ // - Files (unless empty) are compressed and have Data Descriptor and Extra Fields.
649
+ // Size may be incorrect - truncated to lower 32 bits.
650
+ // - Folders and empty files are not compressed and have no Data Descriptor,
651
+ // but do have Extra Fields.
652
+ // Size = 0.
653
+ // - Symlinks are not compressed and have no Data Descriptor or Extra Fields.
654
+ // Size assumed under 4GiB as file content is just path to linked file.
655
+ //
656
+ // So we can know exact end point of this entry's data section (unless it's 4 GiB larger),
657
+ // and all other entries yet to come must use 30 bytes each at minimum.
658
+ let numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
659
+ let dataSpaceRemaining = this.centralDirectoryOffset - fileDataOffsetIfMac
660
+ - entryProps.compressedSize - (entryProps.compressionMethod === 8) * 16;
661
+
662
+ // Check if not enough data space left for this entry or any later entry
663
+ // to be 4 GiB larger than reported
664
+ if (dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB) return true;
665
+
666
+ if (this.isMacArchive && numEntriesRemaining === 0) {
667
+ // Last entry in Mac ZIP - must use all remaining space.
668
+ // We can trust `entryCount` at this point, as it would have been increased
669
+ // if there was excess space in the Central Directory.
670
+ // We cannot assume file takes up all remaining space if we don't know for sure that
671
+ // this is a Mac ZIP, because if it's not, it would be legitimate as per the ZIP spec
672
+ // to have unused space between end of file data and the Central Directory.
673
+ assert(
674
+ dataSpaceRemaining % FOUR_GIB === 0,
675
+ 'Invalid ZIP structure for Mac OS Archive Utility ZIP'
676
+ );
677
+ entryProps.compressedSize += dataSpaceRemaining;
678
+ return true;
679
+ }
680
+
681
+ if (entryProps.compressionMethod === 0) {
682
+ // If this is a Mac ZIP, entry is a folder, empty file, or symlink (see `entryMaybeMac()` below).
683
+ // Folders and empty files definitely have 0 size.
684
+ // We have to assume symlinks are under 4 GiB because they have no data descriptor after to
685
+ // search for (and what kind of maniac uses a symlink bigger than 4 GiB anyway?).
686
+ // If it's not a Mac ZIP, reported compressed size will be accurate.
687
+ // So either way, we know size is correct.
688
+ // Return `false`, because compressed size of later files may still be larger than reported.
689
+ return false;
690
+ }
691
+
692
+ // Compressed size is not certain.
693
+ // Search for Data Descriptor after file data.
694
+ // It could be where it's reported to be, or anywhere after that in 4 GiB jumps.
695
+ let fileDataEnd = fileDataOffsetIfMac + entryProps.compressedSize;
696
+ while (true) { // eslint-disable-line no-constant-condition
697
+ const buffer = await this.reader.read(fileDataEnd, 20);
698
+ if (
699
+ buffer.readUInt32LE(0) === 0x08074b50 // Data Descriptor signature
700
+ && buffer.readUInt32LE(4) === entryProps.crc32
701
+ && buffer.readUInt32LE(8) === entryProps.compressedSize
702
+ && buffer.readUInt32LE(12) === entryProps.uncompressedSize
703
+ && (
704
+ buffer.readUInt32LE(16) === 0x04034b50 // Local File Header signature
705
+ || fileDataEnd + 16 === this.centralDirectoryOffset // Last entry
706
+ )
707
+ ) break;
708
+
709
+ // During async `read()` call above, if user called `openReadStream()` on another entry,
710
+ // it could have discovered this isn't a Mac ZIP after all.
711
+ // If so, stop searching for data descriptor.
712
+ if (this.compressedSizesAreCertain) {
713
+ fileDataEnd = null;
714
+ break;
715
+ }
716
+
717
+ fileDataEnd += FOUR_GIB;
718
+ if (fileDataEnd + 16 > this.centralDirectoryOffset) {
719
+ // Data Descriptor not found
720
+ fileDataEnd = null;
721
+ break;
722
+ }
723
+ }
724
+
725
+ if (fileDataEnd === null) {
726
+ // Could not find Data Descriptor, so this can't be a Mac ZIP
727
+ assert(!this.isMacArchive, 'Cannot locate file Data Descriptor');
728
+ // Have to check `isMaybeMacArchive` again, as could have changed during async calls
729
+ // to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
730
+ if (this.isMaybeMacArchive) this._setAsNotMacArchive();
731
+ return true;
732
+ }
733
+
734
+ if (fileDataEnd === fileDataOffsetIfMac + entryProps.compressedSize) {
735
+ // Compressed size is what was stated. So size of later entries is still uncertain.
736
+ return false;
737
+ }
738
+
739
+ // Size is larger than stated, so this must be Mac ZIP
740
+ if (!this.isMacArchive) {
741
+ // Have to check `isMaybeMacArchive` again, as could have changed during async calls
742
+ // to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
743
+ assert(this.isMaybeMacArchive, 'Cannot locate file Data Descriptor');
744
+ this._setAsMacArchive(this.numEntriesRead + 1, entryProps.entryEnd);
745
+ }
746
+ entryProps.compressedSize = fileDataEnd - fileDataOffsetIfMac;
747
+
748
+ // Check if there's now not enough data space left after this entry for any later entry
749
+ // to be 4 GiB larger than reported.
750
+ // Need to recalculate `numEntriesRemaining` as `entryCount` could have changed.
751
+ // That could happen in `_setAsMacArchive()` call above. Or there's also a possible race
752
+ // if another entry is being streamed at the moment, and that stream happened to exceed
753
+ // its reported uncompressed size. That could happen during async `read()` calls above,
754
+ // and would also cause a call to `_setAsMacArchive()`.
755
+ // More obviously, `dataSpaceRemaining` has to be recalculated too,
756
+ // as initial `fileDataEnd` may have been found to be inaccurate.
757
+ numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
758
+ dataSpaceRemaining = this.centralDirectoryOffset - fileDataEnd - 16;
759
+ return dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB;
760
+ }
761
+
762
+ /**
763
+ * Attempt to read Central Directory Header at offset.
764
+ * Returns properties of entry. Does not decode strings or validate file sizes.
765
+ * @async
766
+ * @param {number} offset - Offset to parse CDH at
767
+ * @returns {Object|null} - Entry properties or `null` if no Central Directory File Header found
768
+ */
769
+ async _readEntryAt(offset) {
770
+ // Bytes 0-3: Central Directory File Header signature
771
+ assert(offset + CDH_MIN_LENGTH <= this.footerOffset, 'Invalid Central Directory File Header');
772
+ const entryBuffer = await this.reader.read(offset, CDH_MIN_LENGTH);
773
+ if (entryBuffer.readUInt32LE(0) !== 0x02014b50) return null;
774
+
775
+ // Bytes 4-5: Version made by
776
+ const versionMadeBy = entryBuffer.readUInt16LE(4);
777
+ // Bytes 6-7: Version needed to extract (minimum)
778
+ const versionNeededToExtract = entryBuffer.readUInt16LE(6);
779
+ // Bytes 8-9: General Purpose Bit Flag
780
+ const generalPurposeBitFlag = entryBuffer.readUInt16LE(8);
781
+ // Bytes 10-11: Compression method
782
+ const compressionMethod = entryBuffer.readUInt16LE(10);
783
+ // Bytes 12-13: File last modification time
784
+ const lastModTime = entryBuffer.readUInt16LE(12);
785
+ // Bytes 14-15: File last modification date
786
+ const lastModDate = entryBuffer.readUInt16LE(14);
787
+ // Bytes 16-17: CRC32
788
+ const crc32 = entryBuffer.readUInt32LE(16);
789
+ // Bytes 20-23: Compressed size
790
+ let compressedSize = entryBuffer.readUInt32LE(20);
791
+ // Bytes 24-27: Uncompressed size
792
+ let uncompressedSize = entryBuffer.readUInt32LE(24);
793
+ // Bytes 28-29: Filename length
794
+ const filenameLength = entryBuffer.readUInt16LE(28);
795
+ // Bytes 30-31: Extra field length
796
+ const extraFieldLength = entryBuffer.readUInt16LE(30);
797
+ // Bytes 32-33: File comment length
798
+ const commentLength = entryBuffer.readUInt16LE(32);
799
+ // Bytes 34-35: Disk number where file starts
800
+ // Bytes 36-37: Internal file attributes
801
+ const internalFileAttributes = entryBuffer.readUInt16LE(36);
802
+ // Bytes 38-41: External file attributes
803
+ const externalFileAttributes = entryBuffer.readUInt32LE(38);
804
+ // Bytes 42-45: Relative offset of Local File Header
805
+ let fileHeaderOffset = entryBuffer.readUInt32LE(42);
806
+
807
+ // eslint-disable-next-line no-bitwise
808
+ assert((generalPurposeBitFlag & 0x40) === 0, 'Strong encryption is not supported');
809
+
810
+ // Get filename
811
+ const extraDataOffset = offset + CDH_MIN_LENGTH,
812
+ extraDataSize = filenameLength + extraFieldLength + commentLength,
813
+ entryEnd = extraDataOffset + extraDataSize;
814
+ assert(entryEnd <= this.footerOffset, 'Invalid Central Directory File Header');
815
+ const extraBuffer = await this.reader.read(extraDataOffset, extraDataSize);
816
+
817
+ const filename = extraBuffer.subarray(0, filenameLength);
818
+
819
+ // Get extra fields
820
+ const commentStart = filenameLength + extraFieldLength;
821
+ const extraFieldBuffer = extraBuffer.subarray(filenameLength, commentStart);
822
+ let i = 0;
823
+ const extraFields = [];
824
+ let zip64EiefBuffer;
825
+ while (i < extraFieldBuffer.length - 3) {
826
+ const headerId = extraFieldBuffer.readUInt16LE(i + 0),
827
+ dataSize = extraFieldBuffer.readUInt16LE(i + 2),
828
+ dataStart = i + 4,
829
+ dataEnd = dataStart + dataSize;
830
+ assert(dataEnd <= extraFieldBuffer.length, 'Extra field length exceeds extra field buffer size');
831
+ const dataBuffer = extraFieldBuffer.subarray(dataStart, dataEnd);
832
+ extraFields.push({id: headerId, data: dataBuffer});
833
+ i = dataEnd;
834
+
835
+ if (headerId === 1) zip64EiefBuffer = dataBuffer;
836
+ }
837
+
838
+ // Get file comment
839
+ const comment = extraBuffer.subarray(commentStart, extraDataSize);
840
+
841
+ // Handle ZIP64
842
+ const isZip64 = uncompressedSize === 0xFFFFFFFF || compressedSize === 0xFFFFFFFF
843
+ || fileHeaderOffset === 0xFFFFFFFF;
844
+ if (isZip64) {
845
+ assert(zip64EiefBuffer, 'Expected ZIP64 Extended Information Extra Field');
846
+
847
+ // @overlookmotel: According to the spec, I'd expect all 3 of these fields to be present,
848
+ // but Yauzl's implementation makes them optional.
849
+ // There may be a good reason for this, so leaving it as in Yauzl's implementation.
850
+ let index = 0;
851
+
852
+ // 8 bytes: Uncompressed size
853
+ if (uncompressedSize === 0xFFFFFFFF) {
854
+ assert(
855
+ index + 8 <= zip64EiefBuffer.length,
856
+ 'ZIP64 Extended Information Extra Field does not include uncompressed size'
857
+ );
858
+ uncompressedSize = readUInt64LE(zip64EiefBuffer, index);
859
+ index += 8;
860
+ }
861
+ // 8 bytes: Compressed size
862
+ if (compressedSize === 0xFFFFFFFF) {
863
+ assert(
864
+ index + 8 <= zip64EiefBuffer.length,
865
+ 'ZIP64 Extended Information Extra Field does not include compressed size'
866
+ );
867
+ compressedSize = readUInt64LE(zip64EiefBuffer, index);
868
+ index += 8;
869
+ }
870
+ // 8 bytes: Local File Header offset
871
+ if (fileHeaderOffset === 0xFFFFFFFF) {
872
+ assert(
873
+ index + 8 <= zip64EiefBuffer.length,
874
+ 'ZIP64 Extended Information Extra Field does not include relative header offset'
875
+ );
876
+ fileHeaderOffset = readUInt64LE(zip64EiefBuffer, index);
877
+ index += 8;
878
+ }
879
+ // 4 bytes: Disk Start Number
880
+ }
881
+
882
+ // Minimum length of Local File Header = 30
883
+ assert(fileHeaderOffset + 30 <= this.footerOffset, 'Invalid location for file data');
884
+
885
+ // Return entry properties
886
+ return {
887
+ filename,
888
+ compressedSize,
889
+ uncompressedSize,
890
+ uncompressedSizeIsCertain: true, // May not be correct - may be set to `false` in `readEntry()`
891
+ compressionMethod,
892
+ fileHeaderOffset,
893
+ fileDataOffset: null,
894
+ isZip64,
895
+ crc32,
896
+ lastModTime,
897
+ lastModDate,
898
+ comment,
899
+ extraFields,
900
+ versionMadeBy,
901
+ versionNeededToExtract,
902
+ generalPurposeBitFlag,
903
+ internalFileAttributes,
904
+ externalFileAttributes,
905
+ filenameLength,
906
+ entryEnd
907
+ };
908
+ }
909
+
910
+ /**
911
+ * Update `entryCount` if it's lower than is possible for it to be.
912
+ * @param {number} numEntriesRead - Number of entries read so far
913
+ * @param {number} entryCursor - Current position in Central Directory
914
+ * @returns {boolean} - `true` if entry count was increased
915
+ */
916
+ _recalculateEntryCount(numEntriesRead, entryCursor) {
917
+ const numEntriesRemaining = this.entryCount - numEntriesRead,
918
+ centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor,
919
+ entryMaxLen = this.isMacArchive ? CDH_MAX_LENGTH_MAC : CDH_MAX_LENGTH;
920
+ if (numEntriesRemaining * entryMaxLen >= centralDirectoryRemaining) return false;
921
+
922
+ // Entry count can't be right.
923
+ // This must be a Mac Archive, so we calculate minimum entry count based on
924
+ // max length of entries in Mac OS ZIPs (which is less than for non-Mac entries).
925
+ const minEntriesRemaining = Math.ceil(centralDirectoryRemaining / CDH_MAX_LENGTH_MAC);
926
+ // eslint-disable-next-line no-bitwise
927
+ this.entryCount += (minEntriesRemaining - numEntriesRemaining + 0xFFFF) & 0x10000;
928
+
929
+ return true;
930
+ }
931
+
932
+ /**
933
+ * Update `entryCountIsCertain` if it's impossible for entry count to be 65536 larger than
934
+ * current `entryCount` without exceeding bounds of Central Directory.
935
+ * This calculation is only valid if size of Central Directory is certain,
936
+ * so must only be called if `centralDirectorySizeIsCertain` is `true`.
937
+ * @param {number} numEntriesRead - Number of entries read so far
938
+ * @param {number} entryCursor - Current position in Central Directory
939
+ * @returns {undefined}
940
+ */
941
+ _recalculateEntryCountIsCertain(numEntriesRead, entryCursor) {
942
+ const numEntriesRemaining = this.entryCount - numEntriesRead,
943
+ centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor;
944
+ if (entryCountIsCertain(numEntriesRemaining, centralDirectoryRemaining)) {
945
+ this.entryCountIsCertain = true;
946
+ }
947
+ }
948
+
949
+ /**
950
+ * Suspected Mac OS Archive Utility ZIP has turned out to definitely be one.
951
+ * Flag as Mac ZIP and calculate Central Directory size if it was ambiguous previously.
952
+ * Recalculate minimum entry count and whether it's now certain.
953
+ * @param {number} numEntriesRead - Number of entries read so far
954
+ * @param {number} entryCursor - Current position in Central Directory
955
+ * @returns {undefined}
956
+ */
957
+ _setAsMacArchive(numEntriesRead, entryCursor) {
958
+ this.isMacArchive = true;
959
+ this.isMaybeMacArchive = false;
960
+ if (!this.centralDirectorySizeIsCertain) {
961
+ this.centralDirectorySize = this.footerOffset - this.centralDirectoryOffset;
962
+ this.centralDirectorySizeIsCertain = true;
963
+ }
964
+
965
+ // Recalculate minimum entry count + whether entry count is certain
966
+ if (!this.entryCountIsCertain) {
967
+ this._recalculateEntryCount(numEntriesRead, entryCursor);
968
+ this._recalculateEntryCountIsCertain(numEntriesRead, entryCursor);
969
+ }
970
+
971
+ // Clear set of uncertain uncompressed size entries
972
+ for (const ref of this._uncertainUncompressedSizeEntryRefs) {
973
+ uncertainUncompressedSizeEntriesRegistry.unregister(ref);
974
+ const entry = ref.deref();
975
+ if (entry) entry._ref = null;
976
+ }
977
+ this._uncertainUncompressedSizeEntryRefs = null;
978
+ }
979
+
980
+ /**
981
+ * Suspected Mac OS Archive Utility ZIP has turned out not to be one.
982
+ * Reset flags.
983
+ * @returns {undefined}
984
+ */
985
+ _setAsNotMacArchive() {
986
+ this.isMaybeMacArchive = false;
987
+ this.entryCountIsCertain = true;
988
+ this.centralDirectorySizeIsCertain = true;
989
+ this.compressedSizesAreCertain = true;
990
+ this.uncompressedSizesAreCertain = true;
991
+ this._fileCursor = null;
992
+
993
+ // Flag all entries flagged as having uncertain uncompressed size as now having certain size
994
+ for (const ref of this._uncertainUncompressedSizeEntryRefs) {
995
+ uncertainUncompressedSizeEntriesRegistry.unregister(ref);
996
+ const entry = ref.deref();
997
+ if (entry) {
998
+ entry._ref = null;
999
+ entry.uncompressedSizeIsCertain = true;
1000
+ }
1001
+ }
1002
+ this._uncertainUncompressedSizeEntryRefs = null;
1003
+ }
1004
+
1005
+ /**
1006
+ * Convert entry properties returned from `_readEntryAt()` to a full `Entry` object.
1007
+ * Decode strings and validate entry size according to options.
1008
+ * @param {Object} entry - Entry properties returned by `_readEntryAt()`
1009
+ * @returns {Entry} - `Entry` object
1010
+ */
1011
+ _validateAndDecodeEntry(entry) {
1012
+ if (this.decodeStrings) {
1013
+ // Check for Info-ZIP Unicode Path Extra Field (0x7075).
1014
+ // See: https://github.com/thejoshwolfe/yauzl/issues/33
1015
+ let filename;
1016
+ for (const extraField of entry.extraFields) {
1017
+ if (extraField.id !== 0x7075) continue;
1018
+ if (extraField.data.length < 6) continue; // Too short to be meaningful
1019
+ // Check version is 1. "Changes may not be backward compatible so this extra
1020
+ // field should not be used if the version is not recognized."
1021
+ if (extraField.data[0] !== 1) continue;
1022
+ // Check CRC32 matches original filename.
1023
+ // "The NameCRC32 is the standard zip CRC32 checksum of the File Name
1024
+ // field in the header. This is used to verify that the header
1025
+ // File Name field has not changed since the Unicode Path extra field
1026
+ // was created. This can happen if a utility renames the File Name but
1027
+ // does not update the UTF-8 path extra field. If the CRC check fails,
1028
+ // this UTF-8 Path Extra Field SHOULD be ignored and the File Name field
1029
+ // in the header SHOULD be used instead."
1030
+ const oldNameCrc32 = extraField.data.readUInt32LE(1);
1031
+ if (calculateCrc32(entry.filename) !== oldNameCrc32) continue;
1032
+ filename = decodeBuffer(extraField.data, 5, true);
1033
+ break;
1034
+ }
1035
+
1036
+ // Decode filename
1037
+ const isUtf8 = (entry.generalPurposeBitFlag & 0x800) !== 0; // eslint-disable-line no-bitwise
1038
+ if (filename === undefined) filename = decodeBuffer(entry.filename, 0, isUtf8);
1039
+
1040
+ // Validate filename
1041
+ if (this.validateFilenames) {
1042
+ // Allow backslash if `strictFilenames` option disabled
1043
+ if (!this.strictFilenames) filename = filename.replace(/\\/g, '/');
1044
+ validateFilename(filename);
1045
+ }
1046
+
1047
+ entry.filename = filename;
1048
+
1049
+ // Clone Extra Fields buffers, so rest of buffer that they're sliced from
1050
+ // (which also contains strings which are now decoded) can be garbage collected
1051
+ for (const extraField of entry.extraFields) {
1052
+ extraField.data = Buffer.from(extraField.data);
1053
+ }
1054
+
1055
+ // Decode comment
1056
+ entry.comment = decodeBuffer(entry.comment, 0, isUtf8);
1057
+ }
1058
+
1059
+ // Validate file size
1060
+ if (this.validateEntrySizes && entry.compressionMethod === 0) {
1061
+ // Lowest bit of General Purpose Bit Flag is for traditional encryption.
1062
+ // Traditional encryption prefixes the file data with a header.
1063
+ // eslint-disable-next-line no-bitwise
1064
+ const expectedCompressedSize = (entry.generalPurposeBitFlag & 0x1)
1065
+ ? entry.uncompressedSize + 12
1066
+ : entry.uncompressedSize;
1067
+ assert(
1068
+ entry.compressedSize === expectedCompressedSize,
1069
+ 'Compressed/uncompressed size mismatch for stored file: '
1070
+ + `${entry.compressedSize} !== ${expectedCompressedSize}`
1071
+ );
1072
+ }
1073
+
1074
+ // Create `Entry` object
1075
+ let entryEnd;
1076
+ ({entryEnd, ...entry} = entry); // eslint-disable-line prefer-const
1077
+ return new Entry(INTERNAL_SYMBOL, {...entry, zip: this, _ref: null});
1078
+ }
1079
+
1080
+ /**
1081
+ * Read multiple entries.
1082
+ * If `numEntries` is provided, will read at maximum that number of entries.
1083
+ * Otherwise, reads all entries.
1084
+ * @async
1085
+ * @param {number} [numEntries] - Number of entries to read
1086
+ * @returns {Array<Entry>} - Array of entries
1087
+ */
1088
+ async readEntries(numEntries) {
1089
+ if (numEntries != null) {
1090
+ assert(isPositiveIntegerOrZero(numEntries), '`numEntries` must be a positive integer if provided');
1091
+ } else {
1092
+ numEntries = Infinity;
1093
+ }
1094
+
1095
+ const entries = [];
1096
+ for (let i = 0; i < numEntries; i++) {
1097
+ const entry = await this.readEntry();
1098
+ if (!entry) break;
1099
+ entries.push(entry);
1100
+ }
1101
+ return entries;
1102
+ }
1103
+
1104
+ /**
1105
+ * Get async iterator for entries.
1106
+ * Usage: `for await (const entry of zip) { ... }`
1107
+ * @returns {Object} - Async iterator
1108
+ */
1109
+ [Symbol.asyncIterator]() {
1110
+ return {
1111
+ next: async () => {
1112
+ const entry = await this.readEntry();
1113
+ return {value: entry, done: entry === null};
1114
+ }
1115
+ };
1116
+ }
1117
+
1118
+ /**
1119
+ * Get readable stream for file data.
1120
+ * @async
1121
+ * @param {Entry} entry - `Entry` object
1122
+ * @param {Object} [options] - Options
1123
+ * @param {boolean} [options.decompress] - `false` to output raw data without decompression
1124
+ * @param {boolean} [options.decrypt] - `true` to decrypt if is encrypted
1125
+ * @param {number} [options.start] - Start offset (only valid if not decompressing)
1126
+ * @param {number} [options.end] - End offset (only valid if not decompressing)
1127
+ * @returns {Object} - Readable stream
1128
+ */
1129
+ async openReadStream(entry, options) {
1130
+ assert(entry instanceof Entry, '`entry` must be an instance of `Entry`');
1131
+ assert(entry.zip === this, '`entry` must be an `Entry` from this ZIP file');
1132
+ return await entry.openReadStream(options);
1133
+ }
1134
+ }
1135
+
1136
+ module.exports = Zip;
1137
+
1138
+ /**
1139
+ * Determine if entry count is certain.
1140
+ * i.e. `centralDirectorySize` bytes could not fit 65536 more entries than stated.
1141
+ * @param {number} entryCount - Number of entries expected (may be under-estimate)
1142
+ * @param {number} centralDirectorySize - Size of Central Directory space to store entries
1143
+ * @returns {boolean} - `true` if entry count is certain
1144
+ */
1145
+ function entryCountIsCertain(entryCount, centralDirectorySize) {
1146
+ return (entryCount + 0x10000) * CDH_MIN_LENGTH > centralDirectorySize;
1147
+ }
1148
+
1149
+ /**
1150
+ * Check if first entry may be a Mac OS Archive Utility entry,
1151
+ * according to various distinguishing characteristics.
1152
+ * @param {Object} entry - Entry props from `_readEntryAt()`
1153
+ * @returns {boolean} - `true` if matches signature of a Mac OS ZIP first entry
1154
+ */
1155
+ function firstEntryMaybeMac(entry) {
1156
+ // First file always starts at byte 0
1157
+ if (entry.fileHeaderOffset !== 0) return false;
1158
+ return entryMaybeMac(entry);
1159
+ }
1160
+
1161
+ /**
1162
+ * Check if entry may be a Mac OS Archive Utility entry,
1163
+ * according to various distinguishing characteristics.
1164
+ * @param {Object} entry - Entry props from `_readEntryAt()`
1165
+ * @returns {boolean} - `true` if matches signature of a Mac OS ZIP entry
1166
+ */
1167
+ function entryMaybeMac(entry) {
1168
+ // Entries always have this `versionMadeBy` value
1169
+ if (entry.versionMadeBy !== 789) return false;
1170
+
1171
+ // Entries never have comments
1172
+ if (entry.comment.length !== 0) return false;
1173
+
1174
+ // Entries never have ZIP64 headers
1175
+ if (entry.isZip64) return false;
1176
+
1177
+ // Check various attributes for files, folders and symlinks
1178
+ if (entry.versionNeededToExtract === 20) {
1179
+ // File
1180
+ if (
1181
+ entry.generalPurposeBitFlag !== 8 || entry.compressionMethod !== 8 || endsWithSlash(entry.filename)
1182
+ ) return false;
1183
+ } else if (entry.versionNeededToExtract === 10) {
1184
+ // Folder, empty file, or symlink
1185
+ if (
1186
+ entry.generalPurposeBitFlag !== 0 || entry.compressionMethod !== 0
1187
+ || entry.uncompressedSize !== entry.compressedSize
1188
+ ) return false;
1189
+
1190
+ if (entry.extraFields.length === 0) {
1191
+ // Symlink
1192
+ if (entry.compressedSize === 0 || endsWithSlash(entry.filename)) return false;
1193
+ // Symlinks have no Extra Fields, so skip the check below.
1194
+ // It is probably a Mac Archive Utility ZIP file.
1195
+ return true;
1196
+ }
1197
+
1198
+ // Folder or empty file
1199
+ if (entry.compressedSize !== 0 || entry.crc32 !== 0) return false;
1200
+ } else {
1201
+ // Unrecognised
1202
+ return false;
1203
+ }
1204
+
1205
+ // Files + folders always have 1 Extra Field with certain id and length
1206
+ if (
1207
+ entry.extraFields.length !== 1
1208
+ || entry.extraFields[0].id !== MAC_CDH_EXTRA_FIELD_ID
1209
+ || entry.extraFields[0].data.length !== MAC_CDH_EXTRA_FIELD_LENGTH
1210
+ ) return false;
1211
+
1212
+ // It is probably a Mac Archive Utility ZIP file
1213
+ return true;
1214
+ }
1215
+
1216
+ /**
1217
+ * Determine if filename (as undecoded buffer) ends with a slash.
1218
+ * @param {Buffer} filename - Filename as buffer
1219
+ * @returns {boolean} - `true` if filename ends with slash
1220
+ */
1221
+ function endsWithSlash(filename) {
1222
+ // Code for '/' is 47 in both CP437 and UTF8
1223
+ return filename[filename.length - 1] === 47;
1224
+ }