diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 93987d528e..a4236b3ebc 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -460,6 +460,7 @@ C16B983E26B4893300256B05 /* DoseEnactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983D26B4893300256B05 /* DoseEnactor.swift */; }; C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983F26B4898800256B05 /* DoseEnactorTests.swift */; }; C16DA84222E8E112008624C2 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; + C1735B1E2A0809830082BB8A /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C1735B1D2A0809830082BB8A /* ZIPFoundation */; }; C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */; }; C174233C259BEB0F00399C9D /* ManualEntryDoseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */; }; C1777A6625A125F100595963 /* ManualEntryDoseViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1777A6525A125F100595963 /* ManualEntryDoseViewModelTests.swift */; }; @@ -476,8 +477,6 @@ C191D2A125B3ACAA00C26C0B /* DosingStrategySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */; }; C19C8BBA28651DFB0056D5E4 /* TrueTime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C19C8BB928651DFB0056D5E4 /* TrueTime.framework */; }; C19C8BBB28651DFB0056D5E4 /* TrueTime.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C19C8BB928651DFB0056D5E4 /* TrueTime.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C19C8BBC28651E1C0056D5E4 /* Minizip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1750AEB255B013300B8011C /* Minizip.framework */; }; - C19C8BBD28651E1C0056D5E4 /* Minizip.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C1750AEB255B013300B8011C /* Minizip.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C19C8BBE28651E3D0056D5E4 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4B1C914197002152D1 /* LoopKit.framework */; }; C19C8BBF28651E3D0056D5E4 /* LoopKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4B1C914197002152D1 /* LoopKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C19C8BC328651EAE0056D5E4 /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C19C8BC228651EAE0056D5E4 /* LoopTestingKit.framework */; }; @@ -726,7 +725,6 @@ 4F2C159A1E0C9E5600E160D4 /* LoopUI.framework in Embed Frameworks */, C19C8BCF28651F520056D5E4 /* LoopKitUI.framework in Embed Frameworks */, C11B9D63286779C000500CF8 /* MockKit.framework in Embed Frameworks */, - C19C8BBD28651E1C0056D5E4 /* Minizip.framework in Embed Frameworks */, C19C8BBB28651DFB0056D5E4 /* TrueTime.framework in Embed Frameworks */, C11B9D65286779C000500CF8 /* MockKitUI.framework in Embed Frameworks */, C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */, @@ -1821,7 +1819,6 @@ C1D6EEA02A06C7270047DE5C /* MKRingProgressView in Frameworks */, 43F5C2C91B929C09003EB13D /* HealthKit.framework in Frameworks */, 43D9FFD621EAE05D00AF44BF /* LoopCore.framework in Frameworks */, - C19C8BBC28651E1C0056D5E4 /* Minizip.framework in Frameworks */, C11B9D64286779C000500CF8 /* MockKitUI.framework in Frameworks */, 4F7528941DFE1E9500C322D6 /* LoopUI.framework in Frameworks */, C11B9D62286779C000500CF8 /* MockKit.framework in Frameworks */, @@ -1829,6 +1826,7 @@ C19C8BBA28651DFB0056D5E4 /* TrueTime.framework in Frameworks */, C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */, C19C8BC328651EAE0056D5E4 /* LoopTestingKit.framework in Frameworks */, + C1735B1E2A0809830082BB8A /* ZIPFoundation in Frameworks */, C19C8BCE28651F520056D5E4 /* LoopKitUI.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3140,6 +3138,7 @@ packageProductDependencies = ( C1F00C5F285A802A006302C5 /* SwiftCharts */, C1D6EE9F2A06C7270047DE5C /* MKRingProgressView */, + C1735B1D2A0809830082BB8A /* ZIPFoundation */, ); productName = Loop; productReference = 43776F8C1B8022E90074EA36 /* Loop.app */; @@ -3464,6 +3463,7 @@ packageReferences = ( C1CCF10B2858F4F70035389C /* XCRemoteSwiftPackageReference "SwiftCharts" */, C1D6EE9E2A06C7270047DE5C /* XCRemoteSwiftPackageReference "MKRingProgressView" */, + C1735B1C2A0809830082BB8A /* XCRemoteSwiftPackageReference "ZIPFoundation" */, ); productRefGroup = 43776F8D1B8022E90074EA36 /* Products */; projectDirPath = ""; @@ -5883,6 +5883,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + C1735B1C2A0809830082BB8A /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LoopKit/ZIPFoundation.git"; + requirement = { + branch = "stream-entry"; + kind = branch; + }; + }; C1CCF10B2858F4F70035389C /* XCRemoteSwiftPackageReference "SwiftCharts" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ivanschuetz/SwiftCharts"; @@ -5907,6 +5915,11 @@ package = C1CCF10B2858F4F70035389C /* XCRemoteSwiftPackageReference "SwiftCharts" */; productName = SwiftCharts; }; + C1735B1D2A0809830082BB8A /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = C1735B1C2A0809830082BB8A /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; C1CCF1162858FBAD0035389C /* SwiftCharts */ = { isa = XCSwiftPackageProductDependency; package = C1CCF10B2858F4F70035389C /* XCRemoteSwiftPackageReference "SwiftCharts" */; diff --git a/Loop/Managers/Alerts/AlertStore.swift b/Loop/Managers/Alerts/AlertStore.swift index 4bf90385b0..d8d6db7e5c 100644 --- a/Loop/Managers/Alerts/AlertStore.swift +++ b/Loop/Managers/Alerts/AlertStore.swift @@ -539,7 +539,7 @@ extension AlertStore: CriticalEventLog { return result! } - public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? { + public func export(startDate: Date, endDate: Date, to stream: DataOutputStream, progress: Progress) -> Error? { let encoder = JSONStreamEncoder(stream: stream) var modificationCounter: Int64 = 0 var fetching = true diff --git a/Loop/Managers/CriticalEventLogExportManager.swift b/Loop/Managers/CriticalEventLogExportManager.swift index 40f15f40b7..6b8f699e5c 100644 --- a/Loop/Managers/CriticalEventLogExportManager.swift +++ b/Loop/Managers/CriticalEventLogExportManager.swift @@ -250,7 +250,7 @@ public class CriticalEventLogBaseExporter { } } - fileprivate func export(startDate: Date, endDate: Date, to url: URL, compression: ZipArchive.Compression, progress: Progress) -> Error? { + fileprivate func export(startDate: Date, endDate: Date, to url: URL, progress: Progress) -> Error? { guard !progress.isCancelled else { return CriticalEventLogError.cancelled } @@ -261,7 +261,7 @@ public class CriticalEventLogBaseExporter { defer { archive.close() } for log in manager.logs { - if let error = export(startDate: startDate, endDate: endDate, from: log, to: archive, compression: compression, progress: progress) { + if let error = export(startDate: startDate, endDate: endDate, from: log, to: archive, progress: progress) { return error } } @@ -269,16 +269,24 @@ public class CriticalEventLogBaseExporter { return archive.close() } - private func export(startDate: Date, endDate: Date, from log: CriticalEventLog, to archive: ZipArchive, compression: ZipArchive.Compression, progress: Progress) -> Error? { + private func export(startDate: Date, endDate: Date, from log: CriticalEventLog, to archive: ZipArchive, progress: Progress) -> Error? { guard !progress.isCancelled else { return CriticalEventLogError.cancelled } - let stream = archive.createArchiveFile(withPath: log.exportName, compression: compression) - stream.open() - defer { stream.close() } + let stream = archive.createArchiveFile(withPath: log.exportName) - return log.export(startDate: startDate, endDate: endDate, to: stream, progress: progress) + if let error = log.export(startDate: startDate, endDate: endDate, to: stream, progress: progress) { + return error + } + + do { + try stream.finish(sync: true) + } catch { + return error + } + + return nil } fileprivate func historicalDate(from now: Date) -> Date { manager.exportDate(for: manager.date(byAddingDays: -Int(manager.historicalDuration.days), to: now)) } @@ -334,7 +342,7 @@ public class CriticalEventLogHistoricalExporter: CriticalEventLogBaseExporter, C let temporaryFileURL = manager.fileManager.temporaryFileURL defer { try? manager.fileManager.removeItem(at: temporaryFileURL) } - if let error = export(startDate: startDate, endDate: endDate, to: temporaryFileURL, compression: .bestCompression, progress: progress) { + if let error = export(startDate: startDate, endDate: endDate, to: temporaryFileURL, progress: progress) { return error } @@ -431,7 +439,7 @@ public class CriticalEventLogFullExporter: CriticalEventLogBaseExporter, Critica log.default("Exporting %{public}@...", recentFileURL.lastPathComponent) - if let error = export(startDate: manager.recentDate(from: now), endDate: now, to: recentTemporaryFileURL, compression: .bestSpeed, progress: progress) { + if let error = export(startDate: manager.recentDate(from: now), endDate: now, to: recentTemporaryFileURL, progress: progress) { return error } @@ -460,7 +468,8 @@ public class CriticalEventLogFullExporter: CriticalEventLogBaseExporter, Critica date = manager.date(byAddingDays: 1, to: date) let exportFileURL = exportsFileURL(for: date) - if let error = archive.createArchiveFile(withPath: exportFileURL.lastPathComponent, contentsOf: exportFileURL) { + log.default("Bundling %{public}@", exportFileURL.lastPathComponent) + if let error = archive.createArchiveFile(withPath: exportFileURL.lastPathComponent, contentsOf: exportFileURL, compressionMethod: .none) { return error } @@ -471,7 +480,8 @@ public class CriticalEventLogFullExporter: CriticalEventLogBaseExporter, Critica return CriticalEventLogError.cancelled } - if let error = archive.createArchiveFile(withPath: recentFileURL.lastPathComponent, contentsOf: recentTemporaryFileURL) { + log.default("Bundling %{public}@", recentFileURL.lastPathComponent) + if let error = archive.createArchiveFile(withPath: recentFileURL.lastPathComponent, contentsOf: recentTemporaryFileURL, compressionMethod: .none) { return error } diff --git a/Loop/Models/ZipArchive.swift b/Loop/Models/ZipArchive.swift index 458139ccdd..26f2e57d14 100644 --- a/Loop/Models/ZipArchive.swift +++ b/Loop/Models/ZipArchive.swift @@ -8,181 +8,127 @@ import Foundation import LoopKit -import Minizip +import ZIPFoundation public enum ZipArchiveError: Error, Equatable { - case unexpectedStatus(Stream.Status) - case internalFailure(Int32) + case streamFinished } public class ZipArchive { - public enum Compression: Int { + + public enum CompressionMethod { case none - case bestSpeed - case bestCompression - case `default` + case deflate - fileprivate var zCompression: Int32 { + var zfMethod: ZIPFoundation.CompressionMethod { switch self { + case .deflate: + return .deflate case .none: - return Z_NO_COMPRESSION - case .bestSpeed: - return Z_BEST_SPEED - case .bestCompression: - return Z_BEST_COMPRESSION - case .default: - return Z_DEFAULT_COMPRESSION + return .none } } } - public class Stream: OutputStream, StreamDelegate { - private let archive: ZipArchive - private let path: String - private let compression: Compression - private var status: Status { - didSet { - if let event = status.event { - synchronizedDelegate.notify { $0?.stream?(self, handle: event) } - } - } - } + public class Stream: NSObject, DataOutputStream { - private var synchronizedDelegate = WeakSynchronizedDelegate() + private let archive: Archive + private let compressionMethod: CompressionMethod - fileprivate init(archive: ZipArchive, path: String, compression: Compression) { - self.archive = archive - self.path = path - self.compression = compression - self.status = archive.closed ? .closed : archive.error != nil ? .error : .notOpen - super.init(toMemory: ()) - } + private let semaphore = DispatchSemaphore(value: 0) + private let processingQueue: DispatchQueue - // MARK: - Stream - public override func open() { - lock.withLock { - guard transitionStatus(from: .notOpen, to: .opening) else { - return - } + private let chunks = Locked<[Data]>([]) + private let error = Locked(nil) + private let finished = Locked(false) - var zipFileInfo = zip_fileinfo(tmz_date: Date().zip, dosDate: 0, internal_fa: 0, external_fa: 0) - setErrorIfZipFailure(zipOpenNewFileInZip(archive.file, path, &zipFileInfo, nil, 0, nil, 0, nil, Z_DEFLATED, compression.zCompression)) - guard error == nil else { - return + fileprivate init(archive: Archive, path: String, compressionMethod: CompressionMethod) { + self.archive = archive + self.compressionMethod = compressionMethod + processingQueue = DispatchQueue(label: "org.loopkit.Loop.zipArchive." + path) + super.init() + startProcessing(path) + } + + public var streamError: Error? { + return error.value + } + + private func startProcessing(_ path: String) { + processingQueue.async { + do { + try self.archive.addEntry(with: path, type: .file, compressionMethod: self.compressionMethod.zfMethod) { position, size in + self.semaphore.wait() + var chunk: Data! + self.chunks.mutate { (value) in + if value.count > 0 { + chunk = value.removeFirst() + } else if self.finished.value { + chunk = Data() + } + } + return chunk + } + } catch { + self.error.mutate { value in + value = error + } } - - _ = transitionStatus(from: .opening, to: .open) } } - public override func close() { - lock.withLock { - unlockedClose() + // MARK: - DataOutputStream + public func write(_ data: Data) throws { + if let error = error.value { + throw error } - } - - public override var delegate: StreamDelegate? { - get { - return synchronizedDelegate.delegate + if finished.value { + throw ZipArchiveError.streamFinished } - set { - synchronizedDelegate.delegate = newValue ?? self + chunks.mutate { value in + value.append(data) } - } - - public override func property(forKey key: Stream.PropertyKey) -> Any? { nil } - - public override func setProperty(_ property: Any?, forKey key: Stream.PropertyKey) -> Bool { false } - - public override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { fatalError("not implemented") } - - public override func remove(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { fatalError("not implemented") } - - public override var streamStatus: Status { lock.withLock { status } } - - public override var streamError: Error? { lock.withLock { error } } - - // MARK: - OutputStream - - public override func write(_ buffer: UnsafePointer, maxLength len: Int) -> Int { - return lock.withLock { - guard transitionStatus(from: .open, to: .writing) else { - return -1 - } - - setErrorIfZipFailure(zipWriteInFileInZip(archive.file, buffer, UInt32(len))) - guard error == nil else { - return -1 - } - - guard transitionStatus(from: .writing, to: .open) else { - return -1 + semaphore.signal() + } + + public func finish(sync: Bool) throws { + // An empty Data() is the sigil for the ZipFoundation read callback + // to detect end of stream. + finished.value = true + semaphore.signal() + if sync { + // Block until processingQueue is finished, and then check error state + processingQueue.sync(flags: .barrier) { } + if let error = error.value { + throw error } - - return len - } - } - - public override var hasSpaceAvailable: Bool { true } - - // MARK: - Internal - - fileprivate func unlockedClose() { - _ = transitionStatus(from: .open, to: .closed) - setErrorIfZipFailure(zipCloseFileInZip(archive.file)) - archive.stream = nil - } - - private func transitionStatus(from: Status, to: Status) -> Bool { - guard status == from else { - setError(ZipArchiveError.unexpectedStatus(status)) - return false - } - status = to - return true - } - - private func setErrorIfZipFailure(_ err: Int32) { - guard err != ZIP_OK else { - return } - setError(ZipArchiveError.internalFailure(err)) } - - private func setError(_ err: Error) { - status = .error - archive.setError(err) - } - - private var error: Error? { archive.error } - - private var lock: UnfairLock { archive.lock } } - private var closed: Bool - private var file: zipFile + private var closed: Bool = false + private let archive: Archive private var stream: Stream? private var error: Error? private let lock = UnfairLock() public init?(url: URL) { - guard let file = zipOpen(url.path, APPEND_STATUS_CREATE) else { + guard let archive = Archive(url: url, accessMode: .create) else { return nil } - self.closed = false - self.file = file + self.archive = archive } - public func createArchiveFile(withPath path: String, compression: Compression = .default) -> OutputStream { + public func createArchiveFile(withPath path: String, compressionMethod: CompressionMethod = .deflate) -> DataOutputStream { return lock.withLock { - stream?.unlockedClose() - stream = Stream(archive: self, path: path, compression: compression) + try? stream?.finish(sync: true) + stream = Stream(archive: archive, path: path, compressionMethod: compressionMethod) return stream! } } - public func createArchiveFile(withPath path: String, contentsOf url: URL, compression: Compression = .default) -> Error? { + public func createArchiveFile(withPath path: String, contentsOf url: URL, compressionMethod: CompressionMethod = .deflate) -> Error? { let data: Data do { @@ -191,10 +137,8 @@ public class ZipArchive { return error } - let stream = createArchiveFile(withPath: path, compression: compression) - stream.open() + let stream = createArchiveFile(withPath: path, compressionMethod: compressionMethod) try? stream.write(data) - stream.close() return lock.withLock { error } } @@ -206,19 +150,15 @@ public class ZipArchive { return nil } defer { closed = true } - stream?.unlockedClose() - setErrorIfZipFailure(zipClose(file, nil)) + do { + try stream?.finish(sync: true) + } catch { + setError(error) + } return error } } - private func setErrorIfZipFailure(_ err: Int32) { - guard err != ZIP_OK else { - return - } - setError(ZipArchiveError.internalFailure(err)) - } - private func setError(_ err: Error) { guard error == nil else { return @@ -226,54 +166,3 @@ public class ZipArchive { error = err } } - -extension Stream.Status: CustomDebugStringConvertible { - public var debugDescription: String { - switch self { - case .notOpen: - return "notOpen" - case .opening: - return "opening" - case .open: - return "open" - case .reading: - return "reading" - case .writing: - return "writing" - case .atEnd: - return "atEnd" - case .closed: - return "closed" - case .error: - return "error" - @unknown default: - return "unknown" - } - } -} - -fileprivate extension Stream.Status { - var event: Stream.Event? { - switch self { - case .open: - return .openCompleted - case .error: - return .errorOccurred - default: - return nil - } - } -} - -fileprivate extension Date { - var zip: tm_zip { - let calendar = Calendar.current - let date = self - return tm_zip(tm_sec: UInt32(calendar.component(.second, from: date)), - tm_min: UInt32(calendar.component(.minute, from: date)), - tm_hour: UInt32(calendar.component(.hour, from: date)), - tm_mday: UInt32(calendar.component(.day, from: date)), - tm_mon: UInt32(calendar.component(.month, from: date)), - tm_year: UInt32(calendar.component(.year, from: date))) - } -} diff --git a/LoopTests/CriticalEventLogTests.swift b/LoopTests/CriticalEventLogTests.swift index fed0cc8dd4..9114e83d15 100644 --- a/LoopTests/CriticalEventLogTests.swift +++ b/LoopTests/CriticalEventLogTests.swift @@ -9,17 +9,25 @@ import Foundation import LoopKit -class MockOutputStream: OutputStream { - var status: Status = .open +class MockOutputStream: DataOutputStream { var error: Error? = nil var data: Data = Data() + var finished = false - override var streamStatus: Status { status } - override var streamError: Error? { error } + var streamError: Error? { return error } - override func write(_ buffer: UnsafePointer, maxLength len: Int) -> Int { - data.append(UnsafeBufferPointer(start: buffer, count: len)) - return len + func write(_ data: Data) throws { + if let error = self.error { + throw error + } + self.data.append(data) + } + + func finish(sync: Bool) throws { + if let error = self.error { + throw error + } + finished = true } var string: String { String(data: data, encoding: .utf8)! } diff --git a/LoopTests/Managers/CriticalEventLogExportManagerTests.swift b/LoopTests/Managers/CriticalEventLogExportManagerTests.swift index fa6ab478a6..a2f0a0e3a1 100644 --- a/LoopTests/Managers/CriticalEventLogExportManagerTests.swift +++ b/LoopTests/Managers/CriticalEventLogExportManagerTests.swift @@ -265,7 +265,7 @@ class MockCriticalEventLog: CriticalEventLog { return .success(Int64(days) * progressUnitCount) } - func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? { + func export(startDate: Date, endDate: Date, to stream: DataOutputStream, progress: Progress) -> Error? { exportExpectation?.fulfill() guard !progress.isCancelled else { diff --git a/LoopTests/Managers/ZipArchiveTests.swift b/LoopTests/Managers/ZipArchiveTests.swift index 3e6a950f62..aeca57fa4b 100644 --- a/LoopTests/Managers/ZipArchiveTests.swift +++ b/LoopTests/Managers/ZipArchiveTests.swift @@ -8,11 +8,14 @@ import XCTest import Loop +import LoopKit +import ZIPFoundation + class ZipArchiveTests: XCTestCase { var url: URL! var archive: ZipArchive! - var outputStream: OutputStream? + var outputStream: DataOutputStream? override func setUp() { url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) @@ -20,9 +23,7 @@ class ZipArchiveTests: XCTestCase { } override func tearDown() { - if outputStream?.streamStatus == .open { - outputStream?.close() - } + try? outputStream?.finish(sync: true) archive.close() try? FileManager.default.removeItem(at: url) } @@ -39,62 +40,17 @@ class ZipArchiveTests: XCTestCase { func testCreateWriteCloseArchiveFile() { outputStream = archive.createArchiveFile(withPath: "testCreateWriteCloseArchiveFile") XCTAssertNotNil(outputStream) - XCTAssertEqual(outputStream?.streamStatus, .notOpen) XCTAssertNil(outputStream?.streamError) - outputStream?.open() - XCTAssertEqual(outputStream?.streamStatus, .open) - XCTAssertEqual(outputStream?.hasSpaceAvailable, true) XCTAssertNoThrow(try outputStream?.write("testCreateWriteCloseArchiveFile")) - outputStream?.close() - XCTAssertEqual(outputStream?.streamStatus, .closed) + XCTAssertNoThrow(try outputStream?.finish(sync: true)) XCTAssertNil(archive.close()) } - func testCreateWriteArchiveFileWithoutOpen() { - outputStream = archive.createArchiveFile(withPath: "testCreateWriteArchiveFileWithoutOpen") - XCTAssertNotNil(outputStream) - XCTAssertThrowsError(try outputStream?.write("testCreateWriteArchiveFileWithoutOpen")) - XCTAssertEqual(outputStream?.streamStatus, .error) - XCTAssertEqual(outputStream?.streamError as? ZipArchiveError, ZipArchiveError.unexpectedStatus(.notOpen)) - XCTAssertEqual(archive.close() as? ZipArchiveError, ZipArchiveError.unexpectedStatus(.notOpen)) - } - func testCreateWriteArchiveFileAfterClose() { outputStream = archive.createArchiveFile(withPath: "testCreateWriteArchiveFileAfterClose") XCTAssertNotNil(outputStream) - outputStream?.open() - outputStream?.close() + XCTAssertNoThrow(try outputStream?.finish(sync: true)) XCTAssertThrowsError(try outputStream?.write("testCreateWriteArchiveFileAfterClose")) - XCTAssertEqual(outputStream?.streamStatus, .error) - XCTAssertEqual(outputStream?.streamError as? ZipArchiveError, ZipArchiveError.unexpectedStatus(.closed)) - XCTAssertEqual(archive.close() as? ZipArchiveError, ZipArchiveError.unexpectedStatus(.closed)) - } - - func testCreateWriteCloseArchiveFileWithCompressionNone() { - outputStream = archive.createArchiveFile(withPath: "testCreateWriteCloseArchiveFileWithCompressionNone", compression: .bestSpeed) - XCTAssertNotNil(outputStream) - outputStream?.open() - XCTAssertNoThrow(try outputStream?.write("testCreateWriteCloseArchiveFileWithCompressionNone")) - outputStream?.close() - XCTAssertNil(archive.close()) - } - - func testCreateWriteCloseArchiveFileWithCompressionBestSpeed() { - outputStream = archive.createArchiveFile(withPath: "testCreateWriteCloseArchiveFileWithCompressionBestSpeed", compression: .bestSpeed) - XCTAssertNotNil(outputStream) - outputStream?.open() - XCTAssertNoThrow(try outputStream?.write("testCreateWriteCloseArchiveFileWithCompressionBestSpeed")) - outputStream?.close() - XCTAssertNil(archive.close()) - } - - func testCreateWriteCloseArchiveFileWithCompressionBestCompression() { - outputStream = archive.createArchiveFile(withPath: "testCreateWriteCloseArchiveFileWithCompressionBestCompression", compression: .bestCompression) - XCTAssertNotNil(outputStream) - outputStream?.open() - XCTAssertNoThrow(try outputStream?.write("testCreateWriteCloseArchiveFileWithCompressionBestCompression")) - outputStream?.close() - XCTAssertNil(archive.close()) } func testCreateArchiveFileWithContents() { @@ -102,28 +58,59 @@ class ZipArchiveTests: XCTestCase { XCTAssertNoThrow(try "testCreateArchiveFileWithContents".data(using: .utf8)!.write(to: contentsURL)) XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContents", contentsOf: contentsURL)) XCTAssertNil(archive.close()) - } - func testCreateArchiveFileWithContentsCompressionNone() { - let contentsURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - XCTAssertNoThrow(try "testCreateArchiveFileWithContentsCompressionNone".data(using: .utf8)!.write(to: contentsURL)) - XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContentsCompressionNone", contentsOf: contentsURL, compression: .none)) - XCTAssertNil(archive.close()) - } + let archive = Archive(url: url, accessMode: .read) + XCTAssertNotNil(archive) - func testCreateArchiveFileWithContentsCompressionBestSpeed() { - let contentsURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - XCTAssertNoThrow(try "testCreateArchiveFileWithContentsCompressionBestSpeed".data(using: .utf8)!.write(to: contentsURL)) - XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContentsCompressionBestSpeed", contentsOf: contentsURL, compression: .bestSpeed)) - XCTAssertNil(archive.close()) + let entry = archive!["testCreateArchiveFileWithContents"] + XCTAssertNotNil(entry) + + XCTAssertEqual(entry!.type, .file) + XCTAssertEqual(entry!.path, "testCreateArchiveFileWithContents") + + var extractedData = Data() + + let _ = try? archive!.extract(entry!, consumer: { (data) in + extractedData.append(data) + }) + + XCTAssertEqual(String(data: extractedData, encoding: .utf8), "testCreateArchiveFileWithContents") } - func testCreateArchiveFileWithContentsCompressionBestCompression() { - let contentsURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - XCTAssertNoThrow(try "testCreateArchiveFileWithContents".data(using: .utf8)!.write(to: contentsURL)) - XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContents", contentsOf: contentsURL, compression: .bestCompression)) + func testCreateArchiveFileWithMultipleFiles() { + let contentsURL1 = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + XCTAssertNoThrow(try "testCreateArchiveFileWithContents1".data(using: .utf8)!.write(to: contentsURL1)) + let contentsURL2 = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + XCTAssertNoThrow(try "testCreateArchiveFileWithContents2".data(using: .utf8)!.write(to: contentsURL2)) + + XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContents1", contentsOf: contentsURL1)) + XCTAssertNil(archive.createArchiveFile(withPath: "testCreateArchiveFileWithContents2", contentsOf: contentsURL2)) XCTAssertNil(archive.close()) + + let archive = Archive(url: url, accessMode: .read) + XCTAssertNotNil(archive) + + let entry1 = archive!["testCreateArchiveFileWithContents1"] + XCTAssertNotNil(entry1) + XCTAssertEqual(entry1!.type, .file) + XCTAssertEqual(entry1!.path, "testCreateArchiveFileWithContents1") + var extractedData1 = Data() + let _ = try? archive!.extract(entry1!, consumer: { (data) in + extractedData1.append(data) + }) + XCTAssertEqual(String(data: extractedData1, encoding: .utf8), "testCreateArchiveFileWithContents1") + + let entry2 = archive!["testCreateArchiveFileWithContents2"] + XCTAssertNotNil(entry2) + XCTAssertEqual(entry2!.type, .file) + XCTAssertEqual(entry2!.path, "testCreateArchiveFileWithContents2") + var extractedData2 = Data() + let _ = try? archive!.extract(entry2!, consumer: { (data) in + extractedData2.append(data) + }) + XCTAssertEqual(String(data: extractedData2, encoding: .utf8), "testCreateArchiveFileWithContents2") } + } fileprivate extension OutputStream {