@capgo/capacitor-updater 8.51.25 → 8.52.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.
- package/CapgoCapacitorUpdater.podspec +0 -3
- package/Package.swift +3 -11
- package/README.md +4 -1
- package/android/build.gradle +0 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +182 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +60 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +80 -21
- package/android/src/main/java/ee/forgr/capacitor_updater/Version.java +306 -0
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +90 -34
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoSemanticVersion.swift +162 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +218 -71
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +3 -4
- package/ios/Sources/CapacitorUpdaterPlugin/RedirectPolicyDelegate.swift +53 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipArchiveReader.swift +247 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipCentralDirectory.swift +280 -0
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Compression
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
enum ZipError: Error, Equatable {
|
|
11
|
+
case unreadableArchive
|
|
12
|
+
case missingEndOfCentralDirectory
|
|
13
|
+
case invalidCentralDirectory
|
|
14
|
+
case invalidLocalFileHeader
|
|
15
|
+
case truncatedEntryData
|
|
16
|
+
case corruptedEntryData
|
|
17
|
+
case encryptedEntry
|
|
18
|
+
case unsupportedCompressionMethod(UInt16)
|
|
19
|
+
case invalidBufferSize
|
|
20
|
+
case entryTooLarge
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
enum ZipEntryType {
|
|
24
|
+
case file
|
|
25
|
+
case directory
|
|
26
|
+
case symlink
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
struct ZipEntry {
|
|
30
|
+
let path: String
|
|
31
|
+
let type: ZipEntryType
|
|
32
|
+
let generalPurposeFlags: UInt16
|
|
33
|
+
let compressionMethod: UInt16
|
|
34
|
+
let compressedSize: UInt64
|
|
35
|
+
let uncompressedSize: UInt64
|
|
36
|
+
let localHeaderOffset: UInt64
|
|
37
|
+
|
|
38
|
+
var isEncrypted: Bool {
|
|
39
|
+
return generalPurposeFlags & 0x0001 != 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Small read-only ZIP reader used to extract downloaded bundles.
|
|
44
|
+
///
|
|
45
|
+
/// Supports stored (0) and deflate (8) entries, ZIP64 archives, and streams entry contents in
|
|
46
|
+
/// `bufferSize` chunks so large files are never fully loaded in memory. Encrypted entries and other
|
|
47
|
+
/// compression methods are rejected. Every offset and size read from the archive is bounds-checked, so a
|
|
48
|
+
/// corrupt archive throws instead of crashing.
|
|
49
|
+
///
|
|
50
|
+
/// This reader does not validate entry paths. Callers must resolve `ZipEntry.path` against the destination
|
|
51
|
+
/// directory (see `CapgoUpdater.resolvePathInsideDirectory`) before writing anything.
|
|
52
|
+
final class ZipArchiveReader {
|
|
53
|
+
static let compressionMethodStored: UInt16 = 0
|
|
54
|
+
static let compressionMethodDeflate: UInt16 = 8
|
|
55
|
+
private static let localFileHeaderSignature: UInt32 = 0x0403_4b50
|
|
56
|
+
private static let localFileHeaderSize = 30
|
|
57
|
+
|
|
58
|
+
let entries: [ZipEntry]
|
|
59
|
+
private let fileHandle: FileHandle
|
|
60
|
+
private let fileSize: UInt64
|
|
61
|
+
|
|
62
|
+
init(url: URL) throws {
|
|
63
|
+
let handle: FileHandle
|
|
64
|
+
do {
|
|
65
|
+
handle = try FileHandle(forReadingFrom: url)
|
|
66
|
+
} catch {
|
|
67
|
+
throw ZipError.unreadableArchive
|
|
68
|
+
}
|
|
69
|
+
do {
|
|
70
|
+
let size = try handle.seekToEnd()
|
|
71
|
+
self.fileHandle = handle
|
|
72
|
+
self.fileSize = size
|
|
73
|
+
self.entries = try ZipCentralDirectory.readEntries(handle: handle, fileSize: size)
|
|
74
|
+
} catch {
|
|
75
|
+
try? handle.close()
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
deinit {
|
|
81
|
+
try? fileHandle.close()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Streams the uncompressed contents of `entry` to `consumer` in chunks of at most `bufferSize` bytes.
|
|
85
|
+
/// Directory entries produce no data.
|
|
86
|
+
func extract(_ entry: ZipEntry, bufferSize: Int, consumer: (Data) throws -> Void) throws {
|
|
87
|
+
guard bufferSize > 0 else {
|
|
88
|
+
throw ZipError.invalidBufferSize
|
|
89
|
+
}
|
|
90
|
+
if entry.type == .directory {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if entry.isEncrypted {
|
|
94
|
+
throw ZipError.encryptedEntry
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let header = try ZipBytes.readChunk(handle: fileHandle, fileSize: fileSize, at: entry.localHeaderOffset, count: Self.localFileHeaderSize)
|
|
98
|
+
guard ZipBytes.uint32(header, 0) == Self.localFileHeaderSignature else {
|
|
99
|
+
throw ZipError.invalidLocalFileHeader
|
|
100
|
+
}
|
|
101
|
+
let localFlags = ZipBytes.uint16(header, 6)
|
|
102
|
+
let localMethod = ZipBytes.uint16(header, 8)
|
|
103
|
+
let nameLength = UInt64(ZipBytes.uint16(header, 26))
|
|
104
|
+
let extraLength = UInt64(ZipBytes.uint16(header, 28))
|
|
105
|
+
if localFlags & 0x0001 != 0 {
|
|
106
|
+
throw ZipError.encryptedEntry
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let headerLength = UInt64(Self.localFileHeaderSize) + nameLength + extraLength
|
|
110
|
+
let (dataOffset, overflow) = entry.localHeaderOffset.addingReportingOverflow(headerLength)
|
|
111
|
+
guard !overflow, dataOffset <= fileSize else {
|
|
112
|
+
throw ZipError.invalidLocalFileHeader
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Like the previous ZIPFoundation-based extraction, the local header decides the compression method
|
|
116
|
+
// while sizes come from the central directory.
|
|
117
|
+
switch localMethod {
|
|
118
|
+
case Self.compressionMethodStored:
|
|
119
|
+
// A stored entry's payload is its content; a size mismatch would read past it into the next records.
|
|
120
|
+
guard entry.compressedSize == entry.uncompressedSize else {
|
|
121
|
+
throw ZipError.corruptedEntryData
|
|
122
|
+
}
|
|
123
|
+
try readStored(from: dataOffset, size: entry.uncompressedSize, bufferSize: bufferSize, consumer: consumer)
|
|
124
|
+
case Self.compressionMethodDeflate:
|
|
125
|
+
if entry.compressedSize == 0 && entry.uncompressedSize == 0 {
|
|
126
|
+
// Some writers store empty files as deflate with no payload at all.
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
try readDeflated(
|
|
130
|
+
from: dataOffset,
|
|
131
|
+
compressedSize: entry.compressedSize,
|
|
132
|
+
expectedSize: entry.uncompressedSize,
|
|
133
|
+
bufferSize: bufferSize,
|
|
134
|
+
consumer: consumer
|
|
135
|
+
)
|
|
136
|
+
default:
|
|
137
|
+
throw ZipError.unsupportedCompressionMethod(localMethod)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Reads the full contents of a small entry (e.g. a symlink target), refusing entries larger than `maxBytes`.
|
|
142
|
+
func readSmallEntry(_ entry: ZipEntry, maxBytes: Int, bufferSize: Int) throws -> Data {
|
|
143
|
+
var result = Data()
|
|
144
|
+
try extract(entry, bufferSize: bufferSize) { chunk in
|
|
145
|
+
guard result.count + chunk.count <= maxBytes else {
|
|
146
|
+
throw ZipError.entryTooLarge
|
|
147
|
+
}
|
|
148
|
+
result.append(chunk)
|
|
149
|
+
}
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func readStored(from offset: UInt64, size: UInt64, bufferSize: Int, consumer: (Data) throws -> Void) throws {
|
|
154
|
+
guard size <= fileSize, offset <= fileSize - size else {
|
|
155
|
+
throw ZipError.truncatedEntryData
|
|
156
|
+
}
|
|
157
|
+
try fileHandle.seek(toOffset: offset)
|
|
158
|
+
var remaining = size
|
|
159
|
+
while remaining > 0 {
|
|
160
|
+
let chunkSize = Int(min(UInt64(bufferSize), remaining))
|
|
161
|
+
try autoreleasepool {
|
|
162
|
+
guard let chunk = try fileHandle.read(upToCount: chunkSize), chunk.count == chunkSize else {
|
|
163
|
+
throw ZipError.truncatedEntryData
|
|
164
|
+
}
|
|
165
|
+
try consumer(chunk)
|
|
166
|
+
}
|
|
167
|
+
remaining -= UInt64(chunkSize)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Inflates an entry, rejecting output that differs from the central directory's uncompressed size.
|
|
172
|
+
/// This also bounds decompression bombs to the declared size.
|
|
173
|
+
private func readDeflated(
|
|
174
|
+
from offset: UInt64,
|
|
175
|
+
compressedSize: UInt64,
|
|
176
|
+
expectedSize: UInt64,
|
|
177
|
+
bufferSize: Int,
|
|
178
|
+
consumer: (Data) throws -> Void
|
|
179
|
+
) throws {
|
|
180
|
+
guard compressedSize <= fileSize, offset <= fileSize - compressedSize else {
|
|
181
|
+
throw ZipError.truncatedEntryData
|
|
182
|
+
}
|
|
183
|
+
try fileHandle.seek(toOffset: offset)
|
|
184
|
+
|
|
185
|
+
let stream = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
|
|
186
|
+
defer { stream.deallocate() }
|
|
187
|
+
// COMPRESSION_ZLIB decodes raw DEFLATE (RFC 1951), which is what ZIP method 8 stores.
|
|
188
|
+
guard compression_stream_init(stream, COMPRESSION_STREAM_DECODE, COMPRESSION_ZLIB) == COMPRESSION_STATUS_OK else {
|
|
189
|
+
throw ZipError.corruptedEntryData
|
|
190
|
+
}
|
|
191
|
+
defer { compression_stream_destroy(stream) }
|
|
192
|
+
|
|
193
|
+
let sourceBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
194
|
+
defer { sourceBuffer.deallocate() }
|
|
195
|
+
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
196
|
+
defer { destinationBuffer.deallocate() }
|
|
197
|
+
|
|
198
|
+
var remaining = compressedSize
|
|
199
|
+
stream.pointee.src_ptr = UnsafePointer(sourceBuffer)
|
|
200
|
+
stream.pointee.src_size = 0
|
|
201
|
+
|
|
202
|
+
var producedTotal: UInt64 = 0
|
|
203
|
+
var finished = false
|
|
204
|
+
while !finished {
|
|
205
|
+
try autoreleasepool {
|
|
206
|
+
if stream.pointee.src_size == 0 && remaining > 0 {
|
|
207
|
+
let chunkSize = Int(min(UInt64(bufferSize), remaining))
|
|
208
|
+
guard let chunk = try fileHandle.read(upToCount: chunkSize), chunk.count == chunkSize else {
|
|
209
|
+
throw ZipError.truncatedEntryData
|
|
210
|
+
}
|
|
211
|
+
chunk.copyBytes(to: sourceBuffer, count: chunkSize)
|
|
212
|
+
stream.pointee.src_ptr = UnsafePointer(sourceBuffer)
|
|
213
|
+
stream.pointee.src_size = chunkSize
|
|
214
|
+
remaining -= UInt64(chunkSize)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stream.pointee.dst_ptr = destinationBuffer
|
|
218
|
+
stream.pointee.dst_size = bufferSize
|
|
219
|
+
let flags = remaining == 0 ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
|
|
220
|
+
let status = compression_stream_process(stream, flags)
|
|
221
|
+
let produced = bufferSize - stream.pointee.dst_size
|
|
222
|
+
if produced > 0 {
|
|
223
|
+
producedTotal += UInt64(produced)
|
|
224
|
+
guard producedTotal <= expectedSize else {
|
|
225
|
+
throw ZipError.corruptedEntryData
|
|
226
|
+
}
|
|
227
|
+
try consumer(Data(bytes: destinationBuffer, count: produced))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
switch status {
|
|
231
|
+
case COMPRESSION_STATUS_END:
|
|
232
|
+
guard producedTotal == expectedSize else {
|
|
233
|
+
throw ZipError.corruptedEntryData
|
|
234
|
+
}
|
|
235
|
+
finished = true
|
|
236
|
+
case COMPRESSION_STATUS_OK:
|
|
237
|
+
// All input consumed and no output produced: the deflate stream is truncated.
|
|
238
|
+
if remaining == 0 && stream.pointee.src_size == 0 && produced == 0 {
|
|
239
|
+
throw ZipError.truncatedEntryData
|
|
240
|
+
}
|
|
241
|
+
default:
|
|
242
|
+
throw ZipError.corruptedEntryData
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
|
|
9
|
+
/// Parses the ZIP end of central directory (including ZIP64) and the central directory entries.
|
|
10
|
+
/// Every offset and length is bounds-checked; malformed input throws `ZipError`.
|
|
11
|
+
enum ZipCentralDirectory {
|
|
12
|
+
private static let eocdSignature: UInt32 = 0x0605_4b50
|
|
13
|
+
private static let zip64EocdSignature: UInt32 = 0x0606_4b50
|
|
14
|
+
private static let zip64LocatorSignature: UInt32 = 0x0706_4b50
|
|
15
|
+
private static let entrySignature: UInt32 = 0x0201_4b50
|
|
16
|
+
private static let eocdSize = 22
|
|
17
|
+
private static let zip64LocatorSize = 20
|
|
18
|
+
private static let zip64EocdMinimumSize = 56
|
|
19
|
+
private static let entryMinimumSize = 46
|
|
20
|
+
private static let maxCommentLength = 0xFFFF
|
|
21
|
+
private static let zip64ExtraFieldId: UInt16 = 0x0001
|
|
22
|
+
|
|
23
|
+
private struct EntrySizes {
|
|
24
|
+
var uncompressed: UInt64
|
|
25
|
+
var compressed: UInt64
|
|
26
|
+
var localHeaderOffset: UInt64
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private struct Location {
|
|
30
|
+
var totalEntries: UInt64
|
|
31
|
+
var size: UInt64
|
|
32
|
+
var offset: UInt64
|
|
33
|
+
/// The central directory must end before this offset (the EOCD or ZIP64 EOCD record).
|
|
34
|
+
var end: UInt64
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static func readEntries(handle: FileHandle, fileSize: UInt64) throws -> [ZipEntry] {
|
|
38
|
+
let location = try locateCentralDirectory(handle: handle, fileSize: fileSize)
|
|
39
|
+
guard location.offset <= location.end, location.size <= location.end - location.offset else {
|
|
40
|
+
throw ZipError.invalidCentralDirectory
|
|
41
|
+
}
|
|
42
|
+
// Every central directory entry takes at least 46 bytes, which bounds the entry count.
|
|
43
|
+
guard location.totalEntries <= location.size / UInt64(entryMinimumSize), location.size <= UInt64(Int.max) else {
|
|
44
|
+
throw ZipError.invalidCentralDirectory
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let data = try ZipBytes.readChunk(handle: handle, fileSize: fileSize, at: location.offset, count: Int(location.size))
|
|
48
|
+
var entries: [ZipEntry] = []
|
|
49
|
+
entries.reserveCapacity(Int(location.totalEntries))
|
|
50
|
+
var cursor = 0
|
|
51
|
+
for _ in 0..<location.totalEntries {
|
|
52
|
+
let (entry, next) = try parseEntry(data, at: cursor)
|
|
53
|
+
entries.append(entry)
|
|
54
|
+
cursor = next
|
|
55
|
+
}
|
|
56
|
+
return entries
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static func locateCentralDirectory(handle: FileHandle, fileSize: UInt64) throws -> Location {
|
|
60
|
+
guard fileSize >= UInt64(eocdSize) else {
|
|
61
|
+
throw ZipError.missingEndOfCentralDirectory
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The EOCD record is at the end of the file, followed by an optional comment of up to 65535 bytes.
|
|
65
|
+
let tailLength = min(fileSize, UInt64(eocdSize + maxCommentLength))
|
|
66
|
+
let tailOffset = fileSize - tailLength
|
|
67
|
+
let tail = try ZipBytes.readChunk(handle: handle, fileSize: fileSize, at: tailOffset, count: Int(tailLength))
|
|
68
|
+
|
|
69
|
+
var eocdIndex: Int?
|
|
70
|
+
var index = tail.count - eocdSize
|
|
71
|
+
while index >= 0 {
|
|
72
|
+
if ZipBytes.uint32(tail, index) == eocdSignature,
|
|
73
|
+
index + eocdSize + Int(ZipBytes.uint16(tail, index + 20)) <= tail.count {
|
|
74
|
+
eocdIndex = index
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
index -= 1
|
|
78
|
+
}
|
|
79
|
+
guard let eocdIndex else {
|
|
80
|
+
throw ZipError.missingEndOfCentralDirectory
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let eocdOffset = tailOffset + UInt64(eocdIndex)
|
|
84
|
+
let location = Location(
|
|
85
|
+
totalEntries: UInt64(ZipBytes.uint16(tail, eocdIndex + 10)),
|
|
86
|
+
size: UInt64(ZipBytes.uint32(tail, eocdIndex + 12)),
|
|
87
|
+
offset: UInt64(ZipBytes.uint32(tail, eocdIndex + 16)),
|
|
88
|
+
end: eocdOffset
|
|
89
|
+
)
|
|
90
|
+
let needsZip64 = location.totalEntries == 0xFFFF || location.size == 0xFFFF_FFFF || location.offset == 0xFFFF_FFFF
|
|
91
|
+
if let zip64Location = try readZip64Location(handle: handle, fileSize: fileSize, eocdOffset: eocdOffset) {
|
|
92
|
+
return zip64Location
|
|
93
|
+
}
|
|
94
|
+
if needsZip64 {
|
|
95
|
+
throw ZipError.invalidCentralDirectory
|
|
96
|
+
}
|
|
97
|
+
return location
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static func readZip64Location(handle: FileHandle, fileSize: UInt64, eocdOffset: UInt64) throws -> Location? {
|
|
101
|
+
guard eocdOffset >= UInt64(zip64LocatorSize) else {
|
|
102
|
+
return nil
|
|
103
|
+
}
|
|
104
|
+
let locatorOffset = eocdOffset - UInt64(zip64LocatorSize)
|
|
105
|
+
let locator = try ZipBytes.readChunk(handle: handle, fileSize: fileSize, at: locatorOffset, count: zip64LocatorSize)
|
|
106
|
+
guard ZipBytes.uint32(locator, 0) == zip64LocatorSignature else {
|
|
107
|
+
return nil
|
|
108
|
+
}
|
|
109
|
+
let recordOffset = ZipBytes.uint64(locator, 8)
|
|
110
|
+
guard recordOffset <= locatorOffset, locatorOffset - recordOffset >= UInt64(zip64EocdMinimumSize) else {
|
|
111
|
+
throw ZipError.invalidCentralDirectory
|
|
112
|
+
}
|
|
113
|
+
let record = try ZipBytes.readChunk(handle: handle, fileSize: fileSize, at: recordOffset, count: zip64EocdMinimumSize)
|
|
114
|
+
guard ZipBytes.uint32(record, 0) == zip64EocdSignature else {
|
|
115
|
+
throw ZipError.invalidCentralDirectory
|
|
116
|
+
}
|
|
117
|
+
return Location(
|
|
118
|
+
totalEntries: ZipBytes.uint64(record, 32),
|
|
119
|
+
size: ZipBytes.uint64(record, 40),
|
|
120
|
+
offset: ZipBytes.uint64(record, 48),
|
|
121
|
+
end: recordOffset
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private static func parseEntry(_ data: Data, at offset: Int) throws -> (ZipEntry, Int) {
|
|
126
|
+
guard offset <= data.count - entryMinimumSize, ZipBytes.uint32(data, offset) == entrySignature else {
|
|
127
|
+
throw ZipError.invalidCentralDirectory
|
|
128
|
+
}
|
|
129
|
+
let versionMadeBy = ZipBytes.uint16(data, offset + 4)
|
|
130
|
+
let flags = ZipBytes.uint16(data, offset + 8)
|
|
131
|
+
let nameLength = Int(ZipBytes.uint16(data, offset + 28))
|
|
132
|
+
let extraLength = Int(ZipBytes.uint16(data, offset + 30))
|
|
133
|
+
let commentLength = Int(ZipBytes.uint16(data, offset + 32))
|
|
134
|
+
let externalAttributes = ZipBytes.uint32(data, offset + 38)
|
|
135
|
+
|
|
136
|
+
let nameStart = offset + entryMinimumSize
|
|
137
|
+
let extraStart = nameStart + nameLength
|
|
138
|
+
let next = extraStart + extraLength + commentLength
|
|
139
|
+
guard next <= data.count else {
|
|
140
|
+
throw ZipError.invalidCentralDirectory
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var sizes = EntrySizes(
|
|
144
|
+
uncompressed: UInt64(ZipBytes.uint32(data, offset + 24)),
|
|
145
|
+
compressed: UInt64(ZipBytes.uint32(data, offset + 20)),
|
|
146
|
+
localHeaderOffset: UInt64(ZipBytes.uint32(data, offset + 42))
|
|
147
|
+
)
|
|
148
|
+
if sizes.uncompressed == 0xFFFF_FFFF || sizes.compressed == 0xFFFF_FFFF || sizes.localHeaderOffset == 0xFFFF_FFFF {
|
|
149
|
+
sizes = try applyZip64Extra(data, extraRange: extraStart..<(extraStart + extraLength), sizes: sizes)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Same path decoding as ZIPFoundation: UTF-8 when general purpose bit 11 is set, CP437 otherwise.
|
|
153
|
+
let nameData = data.subdata(in: (data.startIndex + nameStart)..<(data.startIndex + extraStart))
|
|
154
|
+
let encoding: String.Encoding = flags & (1 << 11) != 0 ? .utf8 : codePage437
|
|
155
|
+
let path = String(data: nameData, encoding: encoding) ?? ""
|
|
156
|
+
|
|
157
|
+
let entry = ZipEntry(
|
|
158
|
+
path: path,
|
|
159
|
+
type: entryType(path: path, versionMadeBy: versionMadeBy, externalAttributes: externalAttributes),
|
|
160
|
+
generalPurposeFlags: flags,
|
|
161
|
+
compressionMethod: ZipBytes.uint16(data, offset + 10),
|
|
162
|
+
compressedSize: sizes.compressed,
|
|
163
|
+
uncompressedSize: sizes.uncompressed,
|
|
164
|
+
localHeaderOffset: sizes.localHeaderOffset
|
|
165
|
+
)
|
|
166
|
+
return (entry, next)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// ZIP64 extended information: values are present only for fields saturated in the fixed record, in this order.
|
|
170
|
+
private static func applyZip64Extra(
|
|
171
|
+
_ data: Data,
|
|
172
|
+
extraRange: Range<Int>,
|
|
173
|
+
sizes: EntrySizes
|
|
174
|
+
) throws -> EntrySizes {
|
|
175
|
+
var cursor = extraRange.lowerBound
|
|
176
|
+
while cursor + 4 <= extraRange.upperBound {
|
|
177
|
+
let headerId = ZipBytes.uint16(data, cursor)
|
|
178
|
+
let fieldStart = cursor + 4
|
|
179
|
+
let fieldEnd = fieldStart + Int(ZipBytes.uint16(data, cursor + 2))
|
|
180
|
+
guard fieldEnd <= extraRange.upperBound else {
|
|
181
|
+
throw ZipError.invalidCentralDirectory
|
|
182
|
+
}
|
|
183
|
+
if headerId != zip64ExtraFieldId {
|
|
184
|
+
cursor = fieldEnd
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var result = sizes
|
|
189
|
+
var valueCursor = fieldStart
|
|
190
|
+
func nextValue() throws -> UInt64 {
|
|
191
|
+
guard valueCursor + 8 <= fieldEnd else {
|
|
192
|
+
throw ZipError.invalidCentralDirectory
|
|
193
|
+
}
|
|
194
|
+
defer { valueCursor += 8 }
|
|
195
|
+
return ZipBytes.uint64(data, valueCursor)
|
|
196
|
+
}
|
|
197
|
+
if result.uncompressed == 0xFFFF_FFFF {
|
|
198
|
+
result.uncompressed = try nextValue()
|
|
199
|
+
}
|
|
200
|
+
if result.compressed == 0xFFFF_FFFF {
|
|
201
|
+
result.compressed = try nextValue()
|
|
202
|
+
}
|
|
203
|
+
if result.localHeaderOffset == 0xFFFF_FFFF {
|
|
204
|
+
result.localHeaderOffset = try nextValue()
|
|
205
|
+
}
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
throw ZipError.invalidCentralDirectory
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Mirrors ZIPFoundation's `Entry.type` so the same entries are treated as files, directories or symlinks.
|
|
212
|
+
static func entryType(path: String, versionMadeBy: UInt16, externalAttributes: UInt32) -> ZipEntryType {
|
|
213
|
+
let hostSystem = versionMadeBy >> 8
|
|
214
|
+
let hasDirectorySuffix = path.hasSuffix("/")
|
|
215
|
+
switch hostSystem {
|
|
216
|
+
case 3, 19: // UNIX, macOS
|
|
217
|
+
let mode = mode_t(UInt16(truncatingIfNeeded: externalAttributes >> 16)) & S_IFMT
|
|
218
|
+
switch mode {
|
|
219
|
+
case S_IFREG:
|
|
220
|
+
return .file
|
|
221
|
+
case S_IFDIR:
|
|
222
|
+
return .directory
|
|
223
|
+
case S_IFLNK:
|
|
224
|
+
return .symlink
|
|
225
|
+
default:
|
|
226
|
+
return hasDirectorySuffix ? .directory : .file
|
|
227
|
+
}
|
|
228
|
+
case 0: // MS-DOS
|
|
229
|
+
return hasDirectorySuffix || (externalAttributes >> 4) == 0x01 ? .directory : .file
|
|
230
|
+
default:
|
|
231
|
+
return hasDirectorySuffix ? .directory : .file
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static let codePage437: String.Encoding = {
|
|
236
|
+
let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.dosLatinUS.rawValue))
|
|
237
|
+
return String.Encoding(rawValue: encoding)
|
|
238
|
+
}()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Little-endian readers and bounded file reads for ZIP structures.
|
|
242
|
+
enum ZipBytes {
|
|
243
|
+
static func readChunk(handle: FileHandle, fileSize: UInt64, at offset: UInt64, count: Int) throws -> Data {
|
|
244
|
+
guard count >= 0, UInt64(count) <= fileSize, offset <= fileSize - UInt64(count) else {
|
|
245
|
+
throw ZipError.invalidCentralDirectory
|
|
246
|
+
}
|
|
247
|
+
if count == 0 {
|
|
248
|
+
return Data()
|
|
249
|
+
}
|
|
250
|
+
try handle.seek(toOffset: offset)
|
|
251
|
+
guard let data = try handle.read(upToCount: count), data.count == count else {
|
|
252
|
+
throw ZipError.unreadableArchive
|
|
253
|
+
}
|
|
254
|
+
return data
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Callers bounds-check `offset` against `data.count` before reading.
|
|
258
|
+
static func uint16(_ data: Data, _ offset: Int) -> UInt16 {
|
|
259
|
+
let base = data.startIndex + offset
|
|
260
|
+
return UInt16(data[base]) | (UInt16(data[base + 1]) << 8)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static func uint32(_ data: Data, _ offset: Int) -> UInt32 {
|
|
264
|
+
let base = data.startIndex + offset
|
|
265
|
+
var value: UInt32 = 0
|
|
266
|
+
for byteIndex in 0..<4 {
|
|
267
|
+
value |= UInt32(data[base + byteIndex]) << (8 * UInt32(byteIndex))
|
|
268
|
+
}
|
|
269
|
+
return value
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static func uint64(_ data: Data, _ offset: Int) -> UInt64 {
|
|
273
|
+
let base = data.startIndex + offset
|
|
274
|
+
var value: UInt64 = 0
|
|
275
|
+
for byteIndex in 0..<8 {
|
|
276
|
+
value |= UInt64(data[base + byteIndex]) << (8 * UInt64(byteIndex))
|
|
277
|
+
}
|
|
278
|
+
return value
|
|
279
|
+
}
|
|
280
|
+
}
|