From c3bc7c87aaa3cb25541c61944c6d9947775b9ec4 Mon Sep 17 00:00:00 2001 From: awxkee Date: Sun, 23 Oct 2022 09:15:39 +0400 Subject: [PATCH] release 1.0.1 --- README.md | 9 +- Sources/bzip2.swift/BZip2+Errors.swift | 54 ++++++- Sources/bzip2.swift/BZip2.swift | 205 ++++++++++++++++++++++++- Tests/bzip2.swiftTests/bzip2test.swift | 24 +++ 4 files changed, 286 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 75c6c26..0b5f8f5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## What's This? bzip2.swift the package to easy compress(decompress) bz2 Data in *Swift* - + Deflate and decompress bz2 data was never been as easy. ```swift @@ -13,6 +13,13 @@ let compressed = try BZip2.compress(Data(count: 10)) // Decompress data let decompressed = try Data(count: 10).fromBZ2() let decompressed = try BZip2.decompress(Data(count: 10)) + +// Decompressing done by Streaming API +try BZip2.decompress(src: InputStream(), dst: OutputStream()) + +// Compressing done by Streaming API +try BZip2.compress(src: InputStream(), dst: OutputStream()) + ``` ## TODO diff --git a/Sources/bzip2.swift/BZip2+Errors.swift b/Sources/bzip2.swift/BZip2+Errors.swift index 2cfad63..ddc0921 100644 --- a/Sources/bzip2.swift/BZip2+Errors.swift +++ b/Sources/bzip2.swift/BZip2+Errors.swift @@ -7,15 +7,38 @@ import Foundation -struct BZip2InitializationError: Error, Equatable { } -struct BZip2CompressionError: LocalizedError, Equatable { +struct BZip2InitializationError: Error { } + +public struct BZip2CannotOpenURLError: LocalizedError { + let url: URL + + public var errorDescription: String? { + if url.scheme != "file" { + return "Only 'file' scheme is supported. Can't open URL: \(url.absoluteString)" + } else { + return "Can't open URL: \(url.absoluteString)" + } + } +} + +struct BZip2CompressionError: LocalizedError { let code: Int32 var errorDescription: String? { "BZip2 compression error with code: \(code)" } } -struct BZip2DecompressionError: LocalizedError, Equatable { + +struct BZip2UnderlyingError: LocalizedError { + let code: Int32 + let message: String + var errorDescription: String? { + "BZip2 compression error with \(code): \(message)" + } +} + + +struct BZip2DecompressionError: LocalizedError { let code: Int32 var errorDescription: String? { "BZip2 compression error with code: \(code)" @@ -23,3 +46,28 @@ struct BZip2DecompressionError: LocalizedError, Equatable { } +public struct BZip2OutOfMemoryError: LocalizedError { + let requiredSize: Int + + public var errorDescription: String? { + "Can't allocate memory in size: \(requiredSize)" + } +} + +public struct BZip2InvalidInputStreamError: LocalizedError { + public var errorDescription: String? { + "Invalid input stream was provided" + } +} + +public struct BZip2ReadingFromStreamSignalledError: LocalizedError { + public var errorDescription: String? { + "While reading of stream error was occur" + } +} + +public struct BZip2WritingFromStreamSignalledError: LocalizedError { + public var errorDescription: String? { + "While writing of stream error was occur" + } +} diff --git a/Sources/bzip2.swift/BZip2.swift b/Sources/bzip2.swift/BZip2.swift index 0fd8c6e..85bc401 100644 --- a/Sources/bzip2.swift/BZip2.swift +++ b/Sources/bzip2.swift/BZip2.swift @@ -10,11 +10,196 @@ import Foundation import bzip2objc #endif +// Will return size of passed input stream for compressing or decompressing +public typealias BZip2ProgressHandler = (Int64) -> Void + public class BZip2 { - public static let BZipCompressionBufferSize: Int = 1024 + public static let BZipCompressionBufferSize: Int = 1024 * 10 public static let BZipDefaultBlockSize: UInt = 7 public static let BZipWorkFactor: Int32 = 0 + + public static func compress(data: Data, to: URL, + minimumReportingBlock: Int64 = 1024 * 100, + progressHandler: BZip2ProgressHandler? = nil) throws { + let inputStream = InputStream(data: data) + guard let outputStream = OutputStream(url: to, append: false) else { + throw BZip2CannotOpenURLError(url: to) + } + do { + return try compress(src: inputStream, dst: outputStream, + minimumReportingBlock: minimumReportingBlock, + progressHandler: progressHandler) + } catch { + inputStream.close() + outputStream.close() + throw error + } + } + + public static func compress(from: URL, to: URL, + minimumReportingBlock: Int64 = 1024 * 100, + progressHandler: BZip2ProgressHandler? = nil) throws { + guard let inputStream = InputStream(url: from) else { + throw BZip2CannotOpenURLError(url: from) + } + guard let outputStream = OutputStream(url: to, append: false) else { + throw BZip2CannotOpenURLError(url: to) + } + do { + return try compress(src: inputStream, dst: outputStream, + minimumReportingBlock: minimumReportingBlock, + progressHandler: progressHandler) + } catch { + inputStream.close() + outputStream.close() + throw error + } + } + + public static func decompress(src: URL, + minimumReportingBlock: Int64 = 1024 * 100, + progressHandler: BZip2ProgressHandler? = nil) throws -> Data { + guard let inputStream = InputStream(url: src) else { + throw BZip2CannotOpenURLError(url: src) + } + let outputStream = OutputStream(toMemory: ()) + do { + try decompress(src: inputStream, dst: outputStream, + minimumReportingBlock: minimumReportingBlock, + progressHandler: progressHandler) + guard let content = outputStream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as? NSData else { + throw BZip2WritingFromStreamSignalledError() + } + return content as Data + } catch { + inputStream.close() + outputStream.close() + throw error + } + } + + public static func decompress(src: InputStream, dst: OutputStream, + minimumReportingBlock: Int64 = 1024 * 100, + progressHandler: BZip2ProgressHandler? = nil) throws { + var stream = bz_stream() + bzero(&stream, MemoryLayout.size) + guard let srcBuffer = malloc(BZipCompressionBufferSize) else { + throw BZip2OutOfMemoryError(requiredSize: BZipCompressionBufferSize) + } + guard let dstBuffer = malloc(BZipCompressionBufferSize) else { + throw BZip2OutOfMemoryError(requiredSize: BZipCompressionBufferSize) + } + src.open() + dst.open() + var readData = BZipCompressionBufferSize + stream.next_in = srcBuffer.assumingMemoryBound(to: CChar.self) + stream.next_out = dstBuffer.assumingMemoryBound(to: CChar.self) + stream.avail_out = UInt32(BZipCompressionBufferSize) + var status = BZ2_bzDecompressInit(&stream, 0, 0) + guard status == BZ_OK else { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } + throw BZip2InitializationError() + } + var compressedSize: Int64 = 0 + var lastReportedSize: Int64 = 0 + while readData == BZipCompressionBufferSize { + readData = src.read(srcBuffer, maxLength: BZipCompressionBufferSize) + compressedSize += Int64(readData) + if readData == -1 { + free(srcBuffer) + free(dstBuffer) + src.close() + dst.close() + BZ2_bzDecompressEnd(&stream) + throw BZip2InvalidInputStreamError() + } + stream.avail_in = UInt32(readData) + status = BZ2_bzDecompress(&stream) + if status < BZ_OK { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } + throw BZip2CompressionError(code: status) + } + dst.write(dstBuffer, maxLength: BZip2.BZipCompressionBufferSize - Int(stream.avail_out)) + if let progressHandler, compressedSize - lastReportedSize >= minimumReportingBlock { + lastReportedSize = compressedSize + progressHandler(compressedSize) + } + } + free(srcBuffer) + free(dstBuffer) + src.close() + dst.close() + BZ2_bzDecompressEnd(&stream) + } + + + public static func compress(src: InputStream, dst: OutputStream, + minimumReportingBlock: Int64 = 1024 * 100, + progressHandler: BZip2ProgressHandler? = nil) throws { + var stream = bz_stream() + bzero(&stream, MemoryLayout.size) + guard let srcBuffer = malloc(BZipCompressionBufferSize) else { + throw BZip2OutOfMemoryError(requiredSize: BZipCompressionBufferSize) + } + guard let dstBuffer = malloc(BZipCompressionBufferSize) else { + throw BZip2OutOfMemoryError(requiredSize: BZipCompressionBufferSize) + } + src.open() + dst.open() + var readData = BZipCompressionBufferSize + stream.next_in = srcBuffer.assumingMemoryBound(to: CChar.self) + stream.next_out = dstBuffer.assumingMemoryBound(to: CChar.self) + stream.avail_out = UInt32(BZipCompressionBufferSize) + var status = BZ2_bzCompressInit(&stream, Int32(BZip2.BZipDefaultBlockSize), 0, BZipWorkFactor) + guard status == BZ_OK else { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } + throw BZip2InitializationError() + } + var compressedSize: Int64 = 0 + var lastReportedSize: Int64 = 0 + while readData == BZipCompressionBufferSize { + readData = src.read(srcBuffer, maxLength: BZipCompressionBufferSize) + compressedSize += Int64(readData) + if readData == -1 { + free(srcBuffer) + free(dstBuffer) + src.close() + dst.close() + BZ2_bzCompressEnd(&stream) + throw BZip2InvalidInputStreamError() + } + stream.avail_in = UInt32(readData) + status = BZ2_bzCompress(&stream, readData == BZipCompressionBufferSize ? BZ_RUN : BZ_FINISH) + if status < BZ_OK { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } + throw BZip2CompressionError(code: status) + } + let length = BZip2.BZipCompressionBufferSize - Int(stream.avail_out) + dst.write(dstBuffer, maxLength: length) + if let progressHandler, compressedSize - lastReportedSize >= minimumReportingBlock { + lastReportedSize = compressedSize + progressHandler(compressedSize) + } + } + free(srcBuffer) + free(dstBuffer) + src.close() + dst.close() + BZ2_bzCompressEnd(&stream) + } /*** - Parameter toCompressData: Data to compress @@ -40,16 +225,24 @@ public class BZip2 { var status = BZ2_bzCompressInit(&stream, Int32(BZip2.BZipDefaultBlockSize), 0, BZipWorkFactor) guard status == BZ_OK else { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } throw BZip2InitializationError() } var compressedData = Data() repeat { status = BZ2_bzCompress(&stream, stream.avail_in != 0 ? BZ_RUN : BZ_FINISH) if status < BZ_OK { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } throw BZip2CompressionError(code: status) } buffer.withUnsafeBytes { pointer in - compressedData.append(UnsafePointer.init(pointer.bindMemory(to: UInt8.self).baseAddress!), count: BZip2.BZipCompressionBufferSize - Int(stream.avail_out)) + compressedData.append(UnsafePointer(pointer.bindMemory(to: UInt8.self).baseAddress!), count: BZip2.BZipCompressionBufferSize - Int(stream.avail_out)) } buffer.withUnsafeMutableBytes { pointer in @@ -86,12 +279,20 @@ public class BZip2 { var status = BZ2_bzDecompressInit(&stream, 0, 0) guard status == BZ_OK else { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } throw BZip2InitializationError() } var decompressedData = Data() repeat { status = BZ2_bzDecompress(&stream) if status < BZ_OK { + var errnum: Int32 = status + if let string = BZ2_bzerror(&stream, &errnum), let swiftString = String(utf8String: string) { + throw BZip2UnderlyingError(code: status, message: swiftString) + } throw BZip2DecompressionError(code: status) } buffer.withUnsafeBytes { pointer in diff --git a/Tests/bzip2.swiftTests/bzip2test.swift b/Tests/bzip2.swiftTests/bzip2test.swift index bf496e3..484a8de 100644 --- a/Tests/bzip2.swiftTests/bzip2test.swift +++ b/Tests/bzip2.swiftTests/bzip2test.swift @@ -39,4 +39,28 @@ class BZip2CompressionTests: XCTestCase { try testCompress("XZ") } + func testCompressionFromURLToURL() throws { + let sourceURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("src") + print(sourceURL.path) + let testString = "Performance will only suffer significantly for very tiny buffers." + let testData = testString.data(using: .utf8)! + try testData.write(to: sourceURL) + let dstURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("dst.zst") + try BZip2.compress(from: sourceURL, to: dstURL) + + let decompressedData = try BZip2.decompress(src: dstURL) + assert(decompressedData.count != 0) + let decompressedString = String(data: decompressedData, encoding: .utf8)! + assert(decompressedString == testString, "compression and decompression must be loseless") + + try FileManager.default.removeItem(at: sourceURL) + try FileManager.default.removeItem(at: dstURL) + } + + func testSimpleCompress() throws { + let testString = "Performance will only suffer significantly for very tiny buffers." + let initialData = testString.data(using: .utf8)! + _ = try BZip2.compress(initialData) + } + }