653 lines
22 KiB
Swift
653 lines
22 KiB
Swift
import Compression
|
|
import Foundation
|
|
import zlib
|
|
|
|
struct ZipUtils {
|
|
|
|
private static let DATA_JSON_FILENAME = "data.json"
|
|
private static let IMAGES_DIR_NAME = "images"
|
|
private static let METADATA_FILENAME = "metadata.txt"
|
|
|
|
static func createExportZip(
|
|
exportData: ClimbDataBackup,
|
|
referencedImagePaths: Set<String>
|
|
) throws -> Data {
|
|
|
|
var zipData = Data()
|
|
var centralDirectory = Data()
|
|
var fileEntries: [(name: String, data: Data, offset: UInt32)] = []
|
|
var currentOffset: UInt32 = 0
|
|
|
|
// Add metadata
|
|
let metadata = createMetadata(
|
|
exportData: exportData, referencedImagePaths: referencedImagePaths)
|
|
let metadataData = metadata.data(using: .utf8) ?? Data()
|
|
try addFileToZip(
|
|
filename: METADATA_FILENAME,
|
|
fileData: metadataData,
|
|
zipData: &zipData,
|
|
fileEntries: &fileEntries,
|
|
currentOffset: ¤tOffset
|
|
)
|
|
|
|
// Encode JSON data
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .prettyPrinted
|
|
encoder.dateEncodingStrategy = .custom { date, encoder in
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(formatter.string(from: date))
|
|
}
|
|
let jsonData = try encoder.encode(exportData)
|
|
try addFileToZip(
|
|
filename: DATA_JSON_FILENAME,
|
|
fileData: jsonData,
|
|
zipData: &zipData,
|
|
fileEntries: &fileEntries,
|
|
currentOffset: ¤tOffset
|
|
)
|
|
|
|
// Process images in batches for better performance
|
|
print("Processing \(referencedImagePaths.count) images for export")
|
|
var successfulImages = 0
|
|
let batchSize = 10
|
|
let sortedPaths = Array(referencedImagePaths).sorted()
|
|
|
|
// Pre-allocate capacity for better memory performance
|
|
zipData.reserveCapacity(zipData.count + (referencedImagePaths.count * 200_000)) // Estimate 200KB per image
|
|
|
|
for (index, imagePath) in sortedPaths.enumerated() {
|
|
if index % batchSize == 0 {
|
|
print("Processing images \(index)/\(sortedPaths.count)")
|
|
}
|
|
|
|
let imageURL = URL(fileURLWithPath: imagePath)
|
|
let imageName = imageURL.lastPathComponent
|
|
|
|
guard FileManager.default.fileExists(atPath: imagePath) else {
|
|
continue
|
|
}
|
|
|
|
do {
|
|
let imageData = try Data(contentsOf: imageURL)
|
|
if imageData.count > 0 {
|
|
let imageEntryName = "\(IMAGES_DIR_NAME)/\(imageName)"
|
|
try addFileToZip(
|
|
filename: imageEntryName,
|
|
fileData: imageData,
|
|
zipData: &zipData,
|
|
fileEntries: &fileEntries,
|
|
currentOffset: ¤tOffset
|
|
)
|
|
successfulImages += 1
|
|
}
|
|
} catch {
|
|
print("Failed to read image: \(imageName)")
|
|
}
|
|
}
|
|
|
|
print("Export: included \(successfulImages)/\(referencedImagePaths.count) images")
|
|
|
|
// Build central directory
|
|
centralDirectory.reserveCapacity(fileEntries.count * 100) // Estimate 100 bytes per entry
|
|
for entry in fileEntries {
|
|
let centralDirEntry = createCentralDirectoryEntry(
|
|
filename: entry.name,
|
|
fileData: entry.data,
|
|
localHeaderOffset: entry.offset
|
|
)
|
|
centralDirectory.append(centralDirEntry)
|
|
}
|
|
|
|
let centralDirOffset = UInt32(zipData.count)
|
|
zipData.append(centralDirectory)
|
|
|
|
let endOfCentralDir = createEndOfCentralDirectory(
|
|
numEntries: UInt16(fileEntries.count),
|
|
centralDirSize: UInt32(centralDirectory.count),
|
|
centralDirOffset: centralDirOffset
|
|
)
|
|
zipData.append(endOfCentralDir)
|
|
|
|
return zipData
|
|
}
|
|
|
|
static func extractImportZip(data: Data) throws -> ImportResult {
|
|
print("Starting ZIP extraction - data size: \(data.count) bytes")
|
|
|
|
return try extractUsingCustomParser(data: data)
|
|
}
|
|
|
|
private static func extractUsingCustomParser(data: Data) throws -> ImportResult {
|
|
var jsonContent = ""
|
|
var metadataContent = ""
|
|
var importedImagePaths: [String: String] = [:]
|
|
|
|
let zipEntries: [ZipEntry]
|
|
do {
|
|
zipEntries = try parseZipFile(data: data)
|
|
print("Successfully parsed ZIP file with \(zipEntries.count) entries")
|
|
} catch {
|
|
print("Failed to parse ZIP file: \(error)")
|
|
print(
|
|
"ZIP data header: \(data.prefix(20).map { String(format: "%02X", $0) }.joined(separator: " "))"
|
|
)
|
|
throw NSError(
|
|
domain: "ImportError", code: 1,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey:
|
|
"Failed to parse ZIP file: \(error.localizedDescription). This may be due to incompatibility with the ZIP format."
|
|
]
|
|
)
|
|
}
|
|
|
|
print("Found \(zipEntries.count) entries in ZIP file:")
|
|
for entry in zipEntries {
|
|
print(" - \(entry.filename) (size: \(entry.data.count) bytes)")
|
|
}
|
|
|
|
for entry in zipEntries {
|
|
switch entry.filename {
|
|
case METADATA_FILENAME:
|
|
metadataContent = String(data: entry.data, encoding: .utf8) ?? ""
|
|
print("Found metadata: \(metadataContent.prefix(100))...")
|
|
|
|
case DATA_JSON_FILENAME:
|
|
jsonContent = String(data: entry.data, encoding: .utf8) ?? ""
|
|
print("Found data.json with \(jsonContent.count) characters")
|
|
if jsonContent.isEmpty {
|
|
print("WARNING: data.json is empty!")
|
|
} else {
|
|
print("data.json preview: \(jsonContent.prefix(200))...")
|
|
}
|
|
|
|
default:
|
|
if entry.filename.hasPrefix("\(IMAGES_DIR_NAME)/") && !entry.filename.hasSuffix("/")
|
|
{
|
|
let originalFilename = String(
|
|
entry.filename.dropFirst("\(IMAGES_DIR_NAME)/".count))
|
|
|
|
do {
|
|
let filename = try ImageManager.shared.saveImportedImage(
|
|
entry.data, filename: originalFilename)
|
|
importedImagePaths[originalFilename] = filename
|
|
} catch {
|
|
print("Failed to import image \(originalFilename): \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
guard !jsonContent.isEmpty else {
|
|
print("ERROR: data.json not found or empty")
|
|
print("Available files in ZIP:")
|
|
for entry in zipEntries {
|
|
print(" - \(entry.filename)")
|
|
}
|
|
throw NSError(
|
|
domain: "ImportError", code: 1,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey:
|
|
"Invalid ZIP file: data.json not found or empty. Found files: \(zipEntries.map { $0.filename }.joined(separator: ", "))"
|
|
]
|
|
)
|
|
}
|
|
|
|
print("Import extraction completed: \(importedImagePaths.count) images processed")
|
|
|
|
return ImportResult(
|
|
jsonData: jsonContent.data(using: .utf8) ?? Data(), imagePathMapping: importedImagePaths
|
|
)
|
|
}
|
|
|
|
private static func createMetadata(
|
|
exportData: ClimbDataBackup,
|
|
referencedImagePaths: Set<String>
|
|
) -> String {
|
|
return """
|
|
Ascently Export Metadata
|
|
========================
|
|
Export Date: \(exportData.exportedAt)
|
|
Gyms: \(exportData.gyms.count)
|
|
Problems: \(exportData.problems.count)
|
|
Sessions: \(exportData.sessions.count)
|
|
Attempts: \(exportData.attempts.count)
|
|
Referenced Images: \(referencedImagePaths.count)
|
|
Format: ZIP with embedded JSON data and images
|
|
"""
|
|
}
|
|
|
|
private static func addFileToZip(
|
|
filename: String,
|
|
fileData: Data,
|
|
zipData: inout Data,
|
|
fileEntries: inout [(name: String, data: Data, offset: UInt32)],
|
|
currentOffset: inout UInt32
|
|
) throws {
|
|
|
|
let localHeader = createLocalFileHeader(filename: filename, fileData: fileData)
|
|
let headerOffset = currentOffset
|
|
|
|
zipData.append(localHeader)
|
|
zipData.append(fileData)
|
|
|
|
fileEntries.append((name: filename, data: fileData, offset: headerOffset))
|
|
|
|
currentOffset += UInt32(localHeader.count + fileData.count)
|
|
}
|
|
|
|
private static func createLocalFileHeader(filename: String, fileData: Data) -> Data {
|
|
var header = Data()
|
|
|
|
header.append(Data([0x50, 0x4b, 0x03, 0x04]))
|
|
|
|
header.append(Data([0x14, 0x00]))
|
|
|
|
header.append(Data([0x00, 0x00]))
|
|
|
|
header.append(Data([0x00, 0x00]))
|
|
|
|
// Last mod file time & date (use current time)
|
|
let dosTime = getDosDateTime()
|
|
header.append(dosTime)
|
|
|
|
let crc = calculateCRC32(data: fileData)
|
|
header.append(withUnsafeBytes(of: crc.littleEndian) { Data($0) })
|
|
|
|
// Compressed size (same as uncompressed since no compression)
|
|
let compressedSize = UInt32(fileData.count)
|
|
header.append(withUnsafeBytes(of: compressedSize.littleEndian) { Data($0) })
|
|
|
|
let uncompressedSize = UInt32(fileData.count)
|
|
header.append(withUnsafeBytes(of: uncompressedSize.littleEndian) { Data($0) })
|
|
|
|
let filenameData = filename.data(using: .utf8) ?? Data()
|
|
let filenameLength = UInt16(filenameData.count)
|
|
header.append(withUnsafeBytes(of: filenameLength.littleEndian) { Data($0) })
|
|
|
|
header.append(Data([0x00, 0x00]))
|
|
|
|
header.append(filenameData)
|
|
|
|
return header
|
|
}
|
|
|
|
private static func createCentralDirectoryEntry(
|
|
filename: String,
|
|
fileData: Data,
|
|
localHeaderOffset: UInt32
|
|
) -> Data {
|
|
var entry = Data()
|
|
|
|
entry.append(Data([0x50, 0x4b, 0x01, 0x02]))
|
|
|
|
entry.append(Data([0x14, 0x00]))
|
|
|
|
entry.append(Data([0x14, 0x00]))
|
|
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
// Last mod file time & date
|
|
let dosTime = getDosDateTime()
|
|
entry.append(dosTime)
|
|
|
|
let crc = calculateCRC32(data: fileData)
|
|
entry.append(withUnsafeBytes(of: crc.littleEndian) { Data($0) })
|
|
|
|
let compressedSize = UInt32(fileData.count)
|
|
entry.append(withUnsafeBytes(of: compressedSize.littleEndian) { Data($0) })
|
|
|
|
let uncompressedSize = UInt32(fileData.count)
|
|
entry.append(withUnsafeBytes(of: uncompressedSize.littleEndian) { Data($0) })
|
|
|
|
let filenameData = filename.data(using: .utf8) ?? Data()
|
|
let filenameLength = UInt16(filenameData.count)
|
|
entry.append(withUnsafeBytes(of: filenameLength.littleEndian) { Data($0) })
|
|
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
// File comment length
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
entry.append(Data([0x00, 0x00]))
|
|
|
|
entry.append(Data([0x00, 0x00, 0x00, 0x00]))
|
|
|
|
// Relative offset of local header
|
|
entry.append(withUnsafeBytes(of: localHeaderOffset.littleEndian) { Data($0) })
|
|
|
|
entry.append(filenameData)
|
|
|
|
return entry
|
|
}
|
|
|
|
private static func createEndOfCentralDirectory(
|
|
numEntries: UInt16,
|
|
centralDirSize: UInt32,
|
|
centralDirOffset: UInt32
|
|
) -> Data {
|
|
var endRecord = Data()
|
|
|
|
endRecord.append(Data([0x50, 0x4b, 0x05, 0x06]))
|
|
|
|
endRecord.append(Data([0x00, 0x00]))
|
|
|
|
// Number of the disk with the start of the central directory
|
|
endRecord.append(Data([0x00, 0x00]))
|
|
|
|
// Total number of entries in the central directory on this disk
|
|
endRecord.append(withUnsafeBytes(of: numEntries.littleEndian) { Data($0) })
|
|
|
|
// Total number of entries in the central directory
|
|
endRecord.append(withUnsafeBytes(of: numEntries.littleEndian) { Data($0) })
|
|
|
|
endRecord.append(withUnsafeBytes(of: centralDirSize.littleEndian) { Data($0) })
|
|
|
|
// Offset of start of central directory
|
|
endRecord.append(withUnsafeBytes(of: centralDirOffset.littleEndian) { Data($0) })
|
|
|
|
// ZIP file comment length
|
|
endRecord.append(Data([0x00, 0x00]))
|
|
|
|
return endRecord
|
|
}
|
|
|
|
private static func getDosDateTime() -> Data {
|
|
let date = Date()
|
|
let calendar = Calendar.current
|
|
let components = calendar.dateComponents(
|
|
[.year, .month, .day, .hour, .minute, .second], from: date)
|
|
|
|
let year = UInt16(max(1980, components.year ?? 1980) - 1980)
|
|
let month = UInt16(components.month ?? 1)
|
|
let day = UInt16(components.day ?? 1)
|
|
let hour = UInt16(components.hour ?? 0)
|
|
let minute = UInt16(components.minute ?? 0)
|
|
let second = UInt16((components.second ?? 0) / 2)
|
|
|
|
let dosDate = (year << 9) | (month << 5) | day
|
|
let dosTime = (hour << 11) | (minute << 5) | second
|
|
|
|
var data = Data()
|
|
data.append(withUnsafeBytes(of: dosTime.littleEndian) { Data($0) })
|
|
data.append(withUnsafeBytes(of: dosDate.littleEndian) { Data($0) })
|
|
return data
|
|
}
|
|
|
|
// CRC32 lookup table for faster calculation
|
|
private static let crc32Table: [UInt32] = {
|
|
let polynomial: UInt32 = 0xEDB8_8320
|
|
var table = [UInt32](repeating: 0, count: 256)
|
|
for i in 0..<256 {
|
|
var crc = UInt32(i)
|
|
for _ in 0..<8 {
|
|
if crc & 1 != 0 {
|
|
crc = (crc >> 1) ^ polynomial
|
|
} else {
|
|
crc >>= 1
|
|
}
|
|
}
|
|
table[i] = crc
|
|
}
|
|
return table
|
|
}()
|
|
|
|
private static func calculateCRC32(data: Data) -> UInt32 {
|
|
var crc: UInt32 = 0xFFFF_FFFF
|
|
|
|
data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
|
|
for byte in bytes {
|
|
let index = Int((crc ^ UInt32(byte)) & 0xFF)
|
|
crc = (crc >> 8) ^ crc32Table[index]
|
|
}
|
|
}
|
|
|
|
return ~crc
|
|
}
|
|
|
|
private static func parseZipFile(data: Data) throws -> [ZipEntry] {
|
|
|
|
var endOfCentralDirOffset = -1
|
|
let signature = Data([0x50, 0x4b, 0x05, 0x06])
|
|
|
|
for i in stride(from: data.count - 22, through: 0, by: -1) {
|
|
if data.subdata(in: i..<i + 4) == signature {
|
|
endOfCentralDirOffset = i
|
|
break
|
|
}
|
|
}
|
|
|
|
guard endOfCentralDirOffset >= 0 else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "End of central directory not found"])
|
|
}
|
|
|
|
let endRecord = data.subdata(in: endOfCentralDirOffset..<endOfCentralDirOffset + 22)
|
|
let numEntries = endRecord.subdata(in: 8..<10).withUnsafeBytes { $0.load(as: UInt16.self) }
|
|
let centralDirOffset = endRecord.subdata(in: 16..<20).withUnsafeBytes {
|
|
$0.load(as: UInt32.self)
|
|
}
|
|
|
|
var entries: [ZipEntry] = []
|
|
var offset = Int(centralDirOffset)
|
|
|
|
for _ in 0..<numEntries {
|
|
let entry = try parseCentralDirectoryEntry(data: data, offset: &offset)
|
|
entries.append(entry)
|
|
}
|
|
|
|
var zipEntries: [ZipEntry] = []
|
|
for entry in entries {
|
|
let fileData = try extractFileData(data: data, entry: entry)
|
|
let zipEntry = ZipEntry(filename: entry.filename, data: fileData)
|
|
zipEntries.append(zipEntry)
|
|
}
|
|
|
|
return zipEntries
|
|
}
|
|
|
|
private static func parseCentralDirectoryEntry(
|
|
data: Data, offset: inout Int
|
|
) throws -> ZipEntry {
|
|
guard offset + 46 <= data.count else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid central directory entry"])
|
|
}
|
|
|
|
let entryData = data.subdata(in: offset..<offset + 46)
|
|
|
|
let signature = entryData.subdata(in: 0..<4)
|
|
guard signature == Data([0x50, 0x4b, 0x01, 0x02]) else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid central directory signature"])
|
|
}
|
|
|
|
let compressionMethod = entryData.subdata(in: 10..<12).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
let compressedSize = entryData.subdata(in: 20..<24).withUnsafeBytes {
|
|
$0.load(as: UInt32.self)
|
|
}
|
|
let uncompressedSize = entryData.subdata(in: 24..<28).withUnsafeBytes {
|
|
$0.load(as: UInt32.self)
|
|
}
|
|
let filenameLength = entryData.subdata(in: 28..<30).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
let extraFieldLength = entryData.subdata(in: 30..<32).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
let commentLength = entryData.subdata(in: 32..<34).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
let localHeaderOffset = entryData.subdata(in: 42..<46).withUnsafeBytes {
|
|
$0.load(as: UInt32.self)
|
|
}
|
|
|
|
offset += 46
|
|
|
|
let filenameData = data.subdata(in: offset..<offset + Int(filenameLength))
|
|
let filename = String(data: filenameData, encoding: .utf8) ?? ""
|
|
offset += Int(filenameLength)
|
|
|
|
offset += Int(extraFieldLength) + Int(commentLength)
|
|
|
|
return ZipEntry(
|
|
filename: filename,
|
|
localHeaderOffset: localHeaderOffset,
|
|
compressedSize: compressedSize,
|
|
uncompressedSize: uncompressedSize,
|
|
compressionMethod: compressionMethod
|
|
)
|
|
}
|
|
|
|
private static func extractFileData(
|
|
data: Data, entry: ZipEntry
|
|
) throws -> Data {
|
|
let headerOffset = Int(entry.localHeaderOffset)
|
|
guard headerOffset + 30 <= data.count else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid local header offset"])
|
|
}
|
|
|
|
let headerData = data.subdata(in: headerOffset..<headerOffset + 30)
|
|
|
|
let signature = headerData.subdata(in: 0..<4)
|
|
guard signature == Data([0x50, 0x4b, 0x03, 0x04]) else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid local header signature"])
|
|
}
|
|
|
|
let localCompressionMethod = headerData.subdata(in: 8..<10).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
|
|
let filenameLength = headerData.subdata(in: 26..<28).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
let extraFieldLength = headerData.subdata(in: 28..<30).withUnsafeBytes {
|
|
$0.load(as: UInt16.self)
|
|
}
|
|
|
|
let dataOffset = headerOffset + 30 + Int(filenameLength) + Int(extraFieldLength)
|
|
let dataEndOffset = dataOffset + Int(entry.compressedSize)
|
|
|
|
guard dataEndOffset <= data.count else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "File data extends beyond ZIP file"])
|
|
}
|
|
|
|
let compressedData = data.subdata(in: dataOffset..<dataEndOffset)
|
|
|
|
switch localCompressionMethod {
|
|
case 0:
|
|
return compressedData
|
|
case 8:
|
|
return try decompressDeflate(compressedData)
|
|
default:
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey:
|
|
"Unsupported compression method: \(localCompressionMethod)"
|
|
])
|
|
}
|
|
}
|
|
|
|
private static func decompressDeflate(_ data: Data) throws -> Data {
|
|
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 1024 * 1024)
|
|
defer { buffer.deallocate() }
|
|
|
|
var decompressedData = Data()
|
|
|
|
try data.withUnsafeBytes { bytes in
|
|
var stream = z_stream()
|
|
stream.next_in = UnsafeMutablePointer<UInt8>(
|
|
mutating: bytes.bindMemory(to: UInt8.self).baseAddress)
|
|
stream.avail_in = UInt32(data.count)
|
|
|
|
let initResult = inflateInit2_(
|
|
&stream, -15, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
|
|
guard initResult == Z_OK else {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey: "Failed to initialize deflate decompression"
|
|
])
|
|
}
|
|
|
|
defer { inflateEnd(&stream) }
|
|
|
|
var result: Int32
|
|
|
|
repeat {
|
|
stream.next_out = buffer
|
|
stream.avail_out = 1024 * 1024
|
|
|
|
result = inflate(&stream, Z_NO_FLUSH)
|
|
|
|
if result != Z_OK && result != Z_STREAM_END {
|
|
throw NSError(
|
|
domain: "ZipError", code: 1,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey: "Decompression failed with code: \(result)"
|
|
])
|
|
}
|
|
|
|
let bytesDecompressed = 1024 * 1024 - Int(stream.avail_out)
|
|
if bytesDecompressed > 0 {
|
|
decompressedData.append(buffer, count: bytesDecompressed)
|
|
}
|
|
} while result != Z_STREAM_END
|
|
}
|
|
|
|
return decompressedData
|
|
}
|
|
}
|
|
|
|
struct ZipEntry {
|
|
let filename: String
|
|
let data: Data
|
|
let localHeaderOffset: UInt32
|
|
let compressedSize: UInt32
|
|
let uncompressedSize: UInt32
|
|
let compressionMethod: UInt16
|
|
|
|
init(filename: String, data: Data) {
|
|
self.filename = filename
|
|
self.data = data
|
|
self.localHeaderOffset = 0
|
|
self.compressedSize = 0
|
|
self.uncompressedSize = 0
|
|
self.compressionMethod = 0
|
|
}
|
|
|
|
init(
|
|
filename: String, localHeaderOffset: UInt32, compressedSize: UInt32,
|
|
uncompressedSize: UInt32 = 0, compressionMethod: UInt16 = 0
|
|
) {
|
|
self.filename = filename
|
|
self.data = Data()
|
|
self.localHeaderOffset = localHeaderOffset
|
|
self.compressedSize = compressedSize
|
|
self.uncompressedSize = uncompressedSize
|
|
self.compressionMethod = compressionMethod
|
|
}
|
|
}
|
|
|
|
struct ImportResult {
|
|
let jsonData: Data
|
|
let imagePathMapping: [String: String]
|
|
}
|