1.5.0 Initial run as iOS in a monorepo
This commit is contained in:
654
ios/OpenClimb/Utils/ZipUtils.swift
Normal file
654
ios/OpenClimb/Utils/ZipUtils.swift
Normal file
@@ -0,0 +1,654 @@
|
||||
//
|
||||
// ZipUtils.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by OpenClimb on 2025-01-17.
|
||||
//
|
||||
|
||||
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: ClimbDataExport,
|
||||
referencedImagePaths: Set<String>
|
||||
) throws -> Data {
|
||||
|
||||
var zipData = Data()
|
||||
var centralDirectory = Data()
|
||||
var fileEntries: [(name: String, data: Data, offset: UInt32)] = []
|
||||
var currentOffset: UInt32 = 0
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
print("Processing \(referencedImagePaths.count) referenced image paths")
|
||||
var successfulImages = 0
|
||||
|
||||
for imagePath in referencedImagePaths {
|
||||
print("Processing image path: \(imagePath)")
|
||||
let imageURL = URL(fileURLWithPath: imagePath)
|
||||
let imageName = imageURL.lastPathComponent
|
||||
print("Image name: \(imageName)")
|
||||
|
||||
if FileManager.default.fileExists(atPath: imagePath) {
|
||||
print("Image file exists at: \(imagePath)")
|
||||
do {
|
||||
let imageData = try Data(contentsOf: imageURL)
|
||||
print("Image data size: \(imageData.count) bytes")
|
||||
if imageData.count > 0 {
|
||||
let imageEntryName = "\(IMAGES_DIR_NAME)/\(imageName)"
|
||||
try addFileToZip(
|
||||
filename: imageEntryName,
|
||||
fileData: imageData,
|
||||
zipData: &zipData,
|
||||
fileEntries: &fileEntries,
|
||||
currentOffset: ¤tOffset
|
||||
)
|
||||
successfulImages += 1
|
||||
print("Successfully added image to ZIP: \(imageEntryName)")
|
||||
} else {
|
||||
print("Image data is empty for: \(imagePath)")
|
||||
}
|
||||
} catch {
|
||||
print("Failed to read image data for \(imagePath): \(error)")
|
||||
}
|
||||
} else {
|
||||
print("Image file does not exist at: \(imagePath)")
|
||||
}
|
||||
}
|
||||
|
||||
print("Export completed: \(successfulImages)/\(referencedImagePaths.count) images included")
|
||||
|
||||
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 documentsURL = FileManager.default.urls(
|
||||
for: .documentDirectory, in: .userDomainMask
|
||||
).first!
|
||||
let imagesDir = documentsURL.appendingPathComponent("images")
|
||||
try FileManager.default.createDirectory(
|
||||
at: imagesDir, withIntermediateDirectories: true)
|
||||
|
||||
let newImageURL = imagesDir.appendingPathComponent(originalFilename)
|
||||
try entry.data.write(to: newImageURL)
|
||||
|
||||
importedImagePaths[originalFilename] = newImageURL.path
|
||||
print(
|
||||
"Successfully imported image: \(originalFilename) -> \(newImageURL.path)"
|
||||
)
|
||||
} 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: ClimbDataExport,
|
||||
referencedImagePaths: Set<String>
|
||||
) -> String {
|
||||
return """
|
||||
OpenClimb 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
|
||||
}
|
||||
|
||||
private static func calculateCRC32(data: Data) -> UInt32 {
|
||||
let polynomial: UInt32 = 0xEDB8_8320
|
||||
var crc: UInt32 = 0xFFFF_FFFF
|
||||
|
||||
for byte in data {
|
||||
crc ^= UInt32(byte)
|
||||
for _ in 0..<8 {
|
||||
if crc & 1 != 0 {
|
||||
crc = (crc >> 1) ^ polynomial
|
||||
} else {
|
||||
crc >>= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 centralDirSize = endRecord.subdata(in: 12..<16).withUnsafeBytes {
|
||||
$0.load(as: UInt32.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]
|
||||
}
|
||||
Reference in New Issue
Block a user