From 111a89d66601ac0531888560907fc6b5c24aa8fd Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Tue, 7 Apr 2026 22:46:30 -0500 Subject: [PATCH 01/12] test gzip compression for static routes --- Package.swift | 8 +- .../Destiny/http/HTTPResponseMessage.swift | 15 +- .../HTTPResponseMessageExtensions.swift | 7 +- .../router/Router+Routes+Static.swift | 2 +- ...sponseBody+ResponderDebugDescription.swift | 187 ++++++++++++ .../util/IntermediateResponseBody.swift | 269 +----------------- .../util/IntermediateResponseBodyType.swift | 109 +++++++ .../util/StaticRoute+Response.swift | 159 +++++++++++ Sources/DestinyMacros/util/StaticRoute.swift | 155 +--------- 9 files changed, 497 insertions(+), 414 deletions(-) create mode 100644 Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift create mode 100644 Sources/DestinyMacros/util/IntermediateResponseBodyType.swift create mode 100644 Sources/DestinyMacros/util/StaticRoute+Response.swift diff --git a/Package.swift b/Package.swift index 3c0f774c..f9f7138c 100644 --- a/Package.swift +++ b/Package.swift @@ -23,6 +23,11 @@ var pkgDependencies:[Package.Dependency] = [ // Metrics //.package(url: "https://github.com/apple/swift-metrics", from: "2.5.1"), + .package( + url: "https://github.com/RandomHashTags/swift-compression", + branch: "refactor" + ), + // Unlock more performance .package( url: "https://github.com/RandomHashTags/swift-unwrap-arithmetic-operators", @@ -433,7 +438,8 @@ var targets = [ .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax") + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "Zlib", package: "swift-compression") ] ), diff --git a/Sources/Destiny/http/HTTPResponseMessage.swift b/Sources/Destiny/http/HTTPResponseMessage.swift index 13bb1ba4..9e8212e0 100644 --- a/Sources/Destiny/http/HTTPResponseMessage.swift +++ b/Sources/Destiny/http/HTTPResponseMessage.swift @@ -113,6 +113,7 @@ extension HTTPResponseMessage { status: head.status, headers: head.headers, body: bodyString, + contentLength: body?.count ?? 0, contentType: contentType, charset: charset ) @@ -238,11 +239,21 @@ extension HTTPResponseMessage { status: HTTPResponseStatus.Code, headers: HTTPHeaders, body: String?, + contentLength: Int, contentType: String?, charset: Charset? ) -> String { let suffix = escapeLineBreak ? "\\r\\n" : "\r\n" - return create(suffix: suffix, version: version, status: status, headers: Self.headers(suffix: suffix, headers: headers), body: body, contentType: contentType, charset: charset) + return create( + suffix: suffix, + version: version, + status: status, + headers: Self.headers(suffix: suffix, headers: headers), + body: body, + contentLength: contentLength, + contentType: contentType, + charset: charset + ) } public static func create( @@ -251,12 +262,12 @@ extension HTTPResponseMessage { status: HTTPResponseStatus.Code, headers: String, body: String?, + contentLength: Int, contentType: String?, charset: Charset? ) -> String { var string = "\(version.string) \(status)\(suffix)\(headers)" if let body { - let contentLength = body.utf8Span.count if let contentType { string += "content-type: \(contentType)\((charset != nil ? "; charset=" + charset!.rawName : ""))\(suffix)" } diff --git a/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift b/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift index a03229c5..c11a9a52 100644 --- a/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift +++ b/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift @@ -3,7 +3,10 @@ import Destiny // MARK: HTTPResponseMessage extension HTTPResponseMessage { - public func intermediateString(escapeLineBreak: Bool) -> String { + public func headString( + escapeLineBreak: Bool, + contentLength: Int + ) -> String { let suffix = escapeLineBreak ? "\\r\\n" : "\r\n" var string = head.string(suffix: suffix) if let body { @@ -11,7 +14,7 @@ extension HTTPResponseMessage { string += "content-type: \(contentType)\((charset != nil ? "; charset=\(charset!.rawName)" : ""))\(suffix)" } if body.hasContentLength { - string += "content-length: \(body.count)\(suffix)\(suffix)" + string += "content-length: \(contentLength)\(suffix)\(suffix)" } } return string diff --git a/Sources/DestinyMacros/router/Router+Routes+Static.swift b/Sources/DestinyMacros/router/Router+Routes+Static.swift index df2add98..d252019e 100644 --- a/Sources/DestinyMacros/router/Router+Routes+Static.swift +++ b/Sources/DestinyMacros/router/Router+Routes+Static.swift @@ -186,7 +186,7 @@ extension RouterStorage { ) #endif } - for (route, function) in routes { + for (var route, function) in routes { let startLine = data.routeStartLine(route) #if StaticMiddleware let httpResponse = route.response(context: context, function: function, middleware: staticMiddleware) diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift new file mode 100644 index 00000000..80fc1332 --- /dev/null +++ b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift @@ -0,0 +1,187 @@ + +import Destiny +import SwiftSyntaxMacros + +extension IntermediateResponseBody { + private func preDateAndPostDateValues(_ string: String) -> (preDate: Substring, postDate: Substring) { + let preDate = string[string.startIndex.. String { + let prefix = isCopyable ? "" : "NonCopyable" + switch type { + case .bytes: + return "\(prefix)Bytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .inlineBytes: + return "\(prefix)InlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .macroExpansion: + responseString.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "RouteResponses.\(prefix)MacroExpansion(\"\(responseString)\", body: \(value))" + case .macroExpansionWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "\(prefix)MacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" + case .streamWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" + return "\(prefix)StreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" + case .stringWithDateHeader: + if interpolation == 0 { + // upgrade + return IntermediateResponseBody( + valueExpr: valueExpr, + type: .staticStringWithDateHeader, + value: escapedValue(), + count: count, + interpolation: interpolation, + rawValue: rawValue, + ).responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) + } + let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" + let (preDate, postDate) = preDateAndPostDateValues(responseString) + return "StringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter), value: \(delimiter)\"\(escapedValue())\"\(delimiter))" + case .staticString: + let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" + return "StaticString(\(delimiter)\"\(responseString)\(escapedValue())\"\(delimiter))" + case .staticStringWithDateHeader: + let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" + let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") + return "\(prefix)StaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" + + case .string: + var s = responseString + value + if s.first != "\"" { + s.insert("\"", at: s.startIndex) + } + if s.last != "\"" { + s.append("\"") + } + if let stringLiteral = valueExpr.stringLiteral, let openingPounds = stringLiteral.openingPounds, let closingPounds = stringLiteral.closingPounds { + s = openingPounds.text + s + closingPounds.text + } + return s + + case .nonCopyableBytes: + return "NonCopyableBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .nonCopyableInlineBytes: + return "NonCopyableInlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .nonCopyableMacroExpansionWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues("\(responseString)") + postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "NonCopyableMacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" + case .nonCopyableStreamWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" + return "NonCopyableStreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" + case .nonCopyableStaticStringWithDateHeader: + let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" + let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") + return "NonCopyableStaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" + } + } + private func escapedValue() -> String { + if let rawValue { + var s = "" + for b in rawValue { + let hex = Self.byteToHex(b) + s.append("\\u{\(hex.high)\(hex.low)}") + } + return s + } + var string = value + guard valueExpr.stringLiteral?.openingPounds == nil else { + // don't escape the string if it uses pound delimiters + return string + } + string.replace("\"", with: "\\\"") + return string + } + + private func bytesPayload( + context: some MacroExpansionContext, + responseString: inout String + ) -> [UInt8] { + var payload = [UInt8]() + payload.reserveCapacity(responseString.count) + responseString.withUTF8 { + payload.append(contentsOf: $0) + } + if let elements = valueExpr.array?.elements { + for element in elements { + if let s = element.expression.integerLiteral?.literal.text, let byte = UInt8(s) { + payload.append(byte) + } else if let s = element.expression.memberAccess?.declName.baseName.text, let byte = UInt8(convenientName: s) { + payload.append(byte) + } else { + context.diagnose(DiagnosticMsg.unhandled(node: element.expression)) + } + } + } else { + context.diagnose(DiagnosticMsg.unhandled(node: valueExpr)) + } + return payload + } +} + +extension IntermediateResponseBody { + #if hasFeature(Embedded) || EMBEDDED + public func responderDebugDescription( + context: some MacroExpansionContext, + isCopyable: Bool, + response: HTTPResponseMessage + ) -> String { + let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) + var responseString = response.intermediateString( + escapeLineBreak: escapeLineBreak, + contentLength: count + ) + return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) + } + #else + public func responderDebugDescription( + context: some MacroExpansionContext, + isCopyable: Bool, + response: HTTPResponseMessage + ) -> String { + let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) + var responseString = response.headString( + escapeLineBreak: escapeLineBreak, + contentLength: count + ) + return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) + } + #endif +} + +// MARK: Byte to hex +extension IntermediateResponseBody { + private static let hexDigits:[16 of Character] = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F" + ] + private static func byteToHex(_ byte: UInt8) -> (high: Character, low: Character) { + let high = PercentEncoding.hexDigits[unchecked: Int(byte >> 4)] + let low = PercentEncoding.hexDigits[unchecked: Int(byte & 0x0F)] + return (high, low) + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody.swift b/Sources/DestinyMacros/util/IntermediateResponseBody.swift index 4a284c04..821f7c87 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody.swift @@ -8,8 +8,13 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { public let valueExpr:ExprSyntax public var type:IntermediateResponseBodyType let value:String - public let count:Int - private var interpolation = 0 + public private(set) var count:Int + var interpolation = 0 + var rawValue:[UInt8]? = nil { + didSet { + count = rawValue?.count ?? count + } + } public init( type: IntermediateResponseBodyType, @@ -29,18 +34,20 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { self.value = valueString self.count = valueString.count - count } - private init( + init( valueExpr: ExprSyntax, type: IntermediateResponseBodyType, value: String, count: Int, - interpolation: Int + interpolation: Int, + rawValue: [UInt8]? = nil ) { self.valueExpr = valueExpr self.type = type self.value = value - self.count = count + self.count = rawValue?.count ?? count self.interpolation = interpolation + self.rawValue = rawValue } private static func upgradeSegments(_ list: StringLiteralSegmentListSyntax) -> (String, Int) { @@ -91,12 +98,6 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { public func write(to buffer: UnsafeMutableBufferPointer, at index: inout Int) { } - private func preDateAndPostDateValues(_ string: String) -> (preDate: Substring, postDate: Substring) { - let preDate = string[string.startIndex.. String { - let prefix = isCopyable ? "" : "NonCopyable" - switch type { - case .bytes: - return "\(prefix)Bytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .inlineBytes: - return "\(prefix)InlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .macroExpansion: - responseString.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "RouteResponses.\(prefix)MacroExpansion(\"\(responseString)\", body: \(value))" - case .macroExpansionWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "\(prefix)MacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" - case .streamWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" - return "\(prefix)StreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .stringWithDateHeader: - if interpolation == 0 { - // upgrade - return IntermediateResponseBody( - valueExpr: valueExpr, - type: .staticStringWithDateHeader, - value: escapedValue(), - count: count, - interpolation: interpolation - ).responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues(responseString) - return "StringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter), value: \(delimiter)\"\(escapedValue())\"\(delimiter))" - case .staticString: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - return "StaticString(\(delimiter)\"\(responseString)\(escapedValue())\"\(delimiter))" - case .staticStringWithDateHeader: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "\(prefix)StaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" - - case .string: - var s = responseString + value - if s.first != "\"" { - s.insert("\"", at: s.startIndex) - } - if s.last != "\"" { - s.append("\"") - } - if let stringLiteral = valueExpr.stringLiteral, let openingPounds = stringLiteral.openingPounds, let closingPounds = stringLiteral.closingPounds { - s = openingPounds.text + s + closingPounds.text - } - return s - - case .nonCopyableBytes: - return "NonCopyableBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .nonCopyableInlineBytes: - return "NonCopyableInlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .nonCopyableMacroExpansionWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues("\(responseString)") - postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "NonCopyableMacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" - case .nonCopyableStreamWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" - return "NonCopyableStreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .nonCopyableStaticStringWithDateHeader: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "NonCopyableStaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" - } - } - func escapedValue() -> String { - var string = value - guard valueExpr.stringLiteral?.openingPounds == nil else { - // don't escape the string if it uses pound delimiters - return string - } - string.replace("\"", with: "\\\"") - return string - } - - private func bytesPayload( - context: some MacroExpansionContext, - responseString: inout String - ) -> [UInt8] { - var payload = [UInt8]() - payload.reserveCapacity(responseString.count) - responseString.withUTF8 { - payload.append(contentsOf: $0) - } - if let elements = valueExpr.array?.elements { - for element in elements { - if let s = element.expression.integerLiteral?.literal.text, let byte = UInt8(s) { - payload.append(byte) - } else if let s = element.expression.memberAccess?.declName.baseName.text, let byte = UInt8(convenientName: s) { - payload.append(byte) - } else { - context.diagnose(DiagnosticMsg.unhandled(node: element.expression)) - } - } - } else { - context.diagnose(DiagnosticMsg.unhandled(node: valueExpr)) - } - return payload - } -} - -extension IntermediateResponseBody { - #if hasFeature(Embedded) || EMBEDDED - public func responderDebugDescription( - context: some MacroExpansionContext, - isCopyable: Bool, - response: HTTPResponseMessage - ) -> String { - let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) - var responseString = response.intermediateString(escapeLineBreak: escapeLineBreak) - return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - #else - public func responderDebugDescription( - context: some MacroExpansionContext, - isCopyable: Bool, - response: HTTPResponseMessage - ) -> String { - let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) - var responseString = response.intermediateString(escapeLineBreak: escapeLineBreak) - return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - #endif -} - -// MARK: IntermediateResponseBodyType -public enum IntermediateResponseBodyType: String, Sendable { - case bytes - case inlineBytes = "inlinebytes" - case macroExpansion = "macroexpansion" - case macroExpansionWithDateHeader = "macroexpansionwithdateheader" - case streamWithDateHeader = "streamwithdateheader" - case staticString = "staticstring" - case staticStringWithDateHeader = "staticstringwithdateheader" - case stringWithDateHeader = "stringwithdateheader" - - case string - - case nonCopyableBytes = "noncopyablebytes" - case nonCopyableInlineBytes = "noncopyableinlinebytes" - case nonCopyableMacroExpansionWithDateHeader = "noncopyablemacroexpansionwithdateheader" - case nonCopyableStreamWithDateHeader = "noncopyablestreamwithdateheader" - case nonCopyableStaticStringWithDateHeader = "noncopyablestaticstringwithdateheader" -} - -extension IntermediateResponseBodyType { - public var isEnabled: Bool? { - switch self { - case .bytes: - #if CopyableBytes - true - #else - false - #endif - case .inlineBytes: - #if CopyableInlineBytes - true - #else - false - #endif - case .macroExpansion: - #if CopyableMacroExpansion - true - #else - false - #endif - case .macroExpansionWithDateHeader: - #if CopyableMacroExpansionWithDateHeader - true - #else - false - #endif - case .streamWithDateHeader: - #if CopyableStreamWithDateHeader - true - #else - false - #endif - case .staticString: - true - case .staticStringWithDateHeader: - #if CopyableStaticStringWithDateHeader - true - #else - false - #endif - case .stringWithDateHeader: - #if CopyableStringWithDateHeader - true - #else - false - #endif - - case .string: - #if StringRouteResponder - true - #else - false - #endif - - case .nonCopyableBytes: - #if NonCopyableBytes - true - #else - false - #endif - case .nonCopyableInlineBytes: - #if NonCopyableInlineBytes - true - #else - false - #endif - case .nonCopyableMacroExpansionWithDateHeader: - #if NonCopyableMacroExpansionWithDateHeader - true - #else - false - #endif - case .nonCopyableStreamWithDateHeader: - #if NonCopyableStreamWithDateHeader - true - #else - false - #endif - case .nonCopyableStaticStringWithDateHeader: - #if NonCopyableStaticStringWithDateHeader - true - #else - false - #endif - } - } -} - // MARK: UInt8 init extension UInt8 { public init?(convenientName: String) { diff --git a/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift new file mode 100644 index 00000000..c5df1395 --- /dev/null +++ b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift @@ -0,0 +1,109 @@ + +public enum IntermediateResponseBodyType: String, Sendable { + case bytes + case inlineBytes = "inlinebytes" + case macroExpansion = "macroexpansion" + case macroExpansionWithDateHeader = "macroexpansionwithdateheader" + case streamWithDateHeader = "streamwithdateheader" + case staticString = "staticstring" + case staticStringWithDateHeader = "staticstringwithdateheader" + case stringWithDateHeader = "stringwithdateheader" + + case string + + case nonCopyableBytes = "noncopyablebytes" + case nonCopyableInlineBytes = "noncopyableinlinebytes" + case nonCopyableMacroExpansionWithDateHeader = "noncopyablemacroexpansionwithdateheader" + case nonCopyableStreamWithDateHeader = "noncopyablestreamwithdateheader" + case nonCopyableStaticStringWithDateHeader = "noncopyablestaticstringwithdateheader" +} + +// MARK: Is enabled +extension IntermediateResponseBodyType { + public var isEnabled: Bool { + switch self { + case .bytes: + #if CopyableBytes + true + #else + false + #endif + case .inlineBytes: + #if CopyableInlineBytes + true + #else + false + #endif + case .macroExpansion: + #if CopyableMacroExpansion + true + #else + false + #endif + case .macroExpansionWithDateHeader: + #if CopyableMacroExpansionWithDateHeader + true + #else + false + #endif + case .streamWithDateHeader: + #if CopyableStreamWithDateHeader + true + #else + false + #endif + case .staticString: + true + case .staticStringWithDateHeader: + #if CopyableStaticStringWithDateHeader + true + #else + false + #endif + case .stringWithDateHeader: + #if CopyableStringWithDateHeader + true + #else + false + #endif + + case .string: + #if StringRouteResponder + true + #else + false + #endif + + case .nonCopyableBytes: + #if NonCopyableBytes + true + #else + false + #endif + case .nonCopyableInlineBytes: + #if NonCopyableInlineBytes + true + #else + false + #endif + case .nonCopyableMacroExpansionWithDateHeader: + #if NonCopyableMacroExpansionWithDateHeader + true + #else + false + #endif + case .nonCopyableStreamWithDateHeader: + #if NonCopyableStreamWithDateHeader + true + #else + false + #endif + case .nonCopyableStaticStringWithDateHeader: + #if NonCopyableStaticStringWithDateHeader + true + #else + false + #endif + } + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift new file mode 100644 index 00000000..8379bd74 --- /dev/null +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -0,0 +1,159 @@ + +import Destiny +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros +import Zlib + +extension StaticRoute { + /// Builds the HTTP Message for this route. + /// + /// - Parameters: + /// - context: Macro expansion context where it was called. + /// - function: `FunctionCallExprSyntax` that represents this route. + /// - middleware: Static middleware this route will handle. + #if StaticMiddleware + public mutating func response( + context: some MacroExpansionContext, + function: FunctionCallExprSyntax, + middleware: [StaticMiddleware] + ) -> HTTPResponseMessage { + let result = response(middleware: middleware) + if result.statusCode() == 501 { // not implemented + Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) + } + return result + } + #else + public mutating func response( + context: some MacroExpansionContext, + function: FunctionCallExprSyntax + ) -> HTTPResponseMessage { + let result = response() + if result.statusCode() == 501 { // not implemented + Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) + } + return result + } + #endif +} + +extension StaticRoute { + #if StaticMiddleware + public mutating func response( + middleware: [StaticMiddleware] + ) -> HTTPResponseMessage { + var version = version + let path = path.joined(separator: "/") + var status = status + var contentType = contentType + var headers = HTTPHeaders() + if body?.hasDateHeader ?? false { + headers["date"] = HTTPDateFormat.placeholder + } + + #if HTTPCookie + var cookies = [HTTPCookie]() + #endif + + middleware.forEach { middleware in + if middleware.handles(version: version, path: path, method: method, contentType: contentType, status: status) { + #if HTTPCookie + middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers, cookies: &cookies) + #else + middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers) + #endif + } + } + headers["content-type"] = nil + headers["content-length"] = nil + + #if HTTPCookie + return Self.response( + version: version, + status: status, + headers: &headers, + cookies: cookies, + body: &body, + contentType: contentType, + charset: charset + ) + #else + return Self.response( + version: version, + status: status, + headers: &headers, + body: body, + contentType: contentType, + charset: charset + ) + #endif + } + #else + public mutating func response() -> HTTPResponseMessage { + var headers = HTTPHeaders() + if body?.hasDateHeader ?? false { + headers["date"] = HTTPDateFormat.placeholder + } + headers["content-type"] = nil + headers["content-length"] = nil + #if HTTPCooke + return Self.response(version: version, status: status, headers: &headers, cookies: [], body: body, contentType: contentType, charset: charset) + #else + return Self.response(version: version, status: status, headers: &headers, body: body, contentType: contentType, charset: charset) + #endif + } + #endif +} + +// MARK: Static +extension StaticRoute { + #if HTTPCookie + @inline(__always) + package static func response( + version: HTTPVersion, + status: HTTPResponseStatus.Code, + headers: inout HTTPHeaders, + cookies: [HTTPCookie], + body: inout IntermediateResponseBody?, + contentType: String?, + charset: Charset? + ) -> HTTPResponseMessage { + headers["content-type"] = nil + headers["content-length"] = nil + + if body != nil, contentType == "text/html" { + if let compressed = Gzip().compress(span: body!.value.utf8Span.span) { + headers["content-encoding"] = "gzip" + headers["vary"] = "Accept-Encoding" + body!.rawValue = compressed + } + } + return HTTPResponseMessage( + head: .init(headers: headers, cookies: cookies, status: status, version: version), + body: body, + contentType: contentType, + charset: charset + ) + } + #else + @inline(__always) + package static func response( + version: HTTPVersion, + status: HTTPResponseStatus.Code, + headers: inout HTTPHeaders, + body: IntermediateResponseBody?, + contentType: String?, + charset: Charset? + ) -> HTTPResponseMessage { + headers["content-type"] = nil + headers["content-length"] = nil + return HTTPResponseMessage( + head: .init(headers: headers, status: status, version: version), + body: body, + contentType: contentType, + charset: charset + ) + } + #endif +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute.swift b/Sources/DestinyMacros/util/StaticRoute.swift index e5c2563a..40f3a576 100644 --- a/Sources/DestinyMacros/util/StaticRoute.swift +++ b/Sources/DestinyMacros/util/StaticRoute.swift @@ -9,7 +9,7 @@ import SwiftSyntaxMacros public struct StaticRoute: Sendable { public var path:[String] public let contentType:String? - public let body:IntermediateResponseBody? + public internal(set) var body:IntermediateResponseBody? public var method:HTTPRequestMethod public let status:HTTPResponseStatus.Code @@ -71,120 +71,10 @@ extension StaticRoute { } } -// MARK: Response -extension StaticRoute { - #if StaticMiddleware - public func response( - middleware: [StaticMiddleware] - ) -> HTTPResponseMessage { - var version = version - let path = path.joined(separator: "/") - var status = status - var contentType = contentType - var headers = HTTPHeaders() - if body?.hasDateHeader ?? false { - headers["date"] = HTTPDateFormat.placeholder - } - - #if HTTPCookie - var cookies = [HTTPCookie]() - #endif - - middleware.forEach { middleware in - if middleware.handles(version: version, path: path, method: method, contentType: contentType, status: status) { - #if HTTPCookie - middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers, cookies: &cookies) - #else - middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers) - #endif - } - } - headers["content-type"] = nil - headers["content-length"] = nil - - #if HTTPCookie - return Self.response( - version: version, - status: status, - headers: &headers, - cookies: cookies, - body: body, - contentType: contentType, - charset: charset - ) - #else - return Self.response( - version: version, - status: status, - headers: &headers, - body: body, - contentType: contentType, - charset: charset - ) - #endif - } - #else - public func response() -> HTTPResponseMessage { - var headers = HTTPHeaders() - if body?.hasDateHeader ?? false { - headers["date"] = HTTPDateFormat.placeholder - } - headers["content-type"] = nil - headers["content-length"] = nil - #if HTTPCooke - return Self.response(version: version, status: status, headers: &headers, cookies: [], body: body, contentType: contentType, charset: charset) - #else - return Self.response(version: version, status: status, headers: &headers, body: body, contentType: contentType, charset: charset) - #endif - } - #endif - - #if HTTPCookie - @inline(__always) - package static func response( - version: HTTPVersion, - status: HTTPResponseStatus.Code, - headers: inout HTTPHeaders, - cookies: [HTTPCookie], - body: IntermediateResponseBody?, - contentType: String?, - charset: Charset? - ) -> HTTPResponseMessage { - headers["content-type"] = nil - headers["content-length"] = nil - return HTTPResponseMessage( - head: .init(headers: headers, cookies: cookies, status: status, version: version), - body: body, - contentType: contentType, - charset: charset - ) - } - #else - @inline(__always) - package static func response( - version: HTTPVersion, - status: HTTPResponseStatus.Code, - headers: inout HTTPHeaders, - body: IntermediateResponseBody?, - contentType: String?, - charset: Charset? - ) -> HTTPResponseMessage { - headers["content-type"] = nil - headers["content-length"] = nil - return HTTPResponseMessage( - head: .init(headers: headers, status: status, version: version), - body: body, - contentType: contentType, - charset: charset - ) - } - #endif -} - // MARK: Responder extension StaticRoute { #if StaticMiddleware - public func responder( + public mutating func responder( middleware: [StaticMiddleware] ) -> String? { return response(middleware: middleware).string(escapeLineBreak: true) @@ -194,44 +84,7 @@ extension StaticRoute { return response().string(escapeLineBreak: true) } #endif -} - -// MARK: Response -extension StaticRoute { - /// Builds the HTTP Message for this route. - /// - /// - Parameters: - /// - context: Macro expansion context where it was called. - /// - function: `FunctionCallExprSyntax` that represents this route. - /// - middleware: Static middleware this route will handle. - #if StaticMiddleware - public func response( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax, - middleware: [StaticMiddleware] - ) -> HTTPResponseMessage { - let result = response(middleware: middleware) - if result.statusCode() == 501 { // not implemented - Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) - } - return result - } - #else - public func response( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax - ) -> HTTPResponseMessage { - let result = response() - if result.statusCode() == 501 { // not implemented - Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) - } - return result - } - #endif -} -// MARK: Responder -extension StaticRoute { /// The `RouteResponderProtocol` responder for this route. /// /// - Parameters: @@ -239,7 +92,7 @@ extension StaticRoute { /// - function: `FunctionCallExprSyntax` that represents this route. /// - middleware: Static middleware that this route will handle. #if StaticMiddleware - public func responder( + public mutating func responder( context: some MacroExpansionContext, function: FunctionCallExprSyntax, middleware: [StaticMiddleware] @@ -247,7 +100,7 @@ extension StaticRoute { return response(context: context, function: function, middleware: middleware).string(escapeLineBreak: true) } #else - public func responder( + public mutating func responder( context: some MacroExpansionContext, function: FunctionCallExprSyntax ) throws(DestinyError) -> String? { From 249bf10e8aa7fdf05426d4c6c0f4773026991ce9 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Tue, 7 Apr 2026 23:08:53 -0500 Subject: [PATCH 02/12] add todo --- .../IntermediateResponseBody+ResponderDebugDescription.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift index 80fc1332..0603b5c0 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift @@ -90,7 +90,7 @@ extension IntermediateResponseBody { var s = "" for b in rawValue { let hex = Self.byteToHex(b) - s.append("\\u{\(hex.high)\(hex.low)}") + s.append("\\u{\(hex.high)\(hex.low)}") // TODO: fix | bytes > 127 get encoded as two bytes } return s } From b7b5269dbadd9e27b9d2b38aaeddf7d96a498335 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Wed, 8 Apr 2026 14:45:25 -0500 Subject: [PATCH 03/12] now supports compression --- .../Destiny/extensions/Int32Extensions.swift | 14 ++ Sources/Destiny/http/HTTPSocket.swift | 9 ++ .../NonCopyableCompressedBody.swift | 28 ++++ ...NonCopyableDateHeaderPayloadWithBody.swift | 71 ++++++++ ...tringWithDateHeaderAndCompressedBody.swift | 64 ++++++++ .../util/protocols/FileDescriptor.swift | 10 ++ Sources/DestinyMacros/parse/Route+Parse.swift | 6 +- .../DestinyMacros/router/Router+Compute.swift | 5 +- .../router/Router+Routes+PerfectHash.swift | 4 +- .../router/Router+Routes+Static.swift | 25 ++- ...sponseBody+ResponderDebugDescription.swift | 62 +++---- .../util/IntermediateResponseBody.swift | 27 ++-- .../util/IntermediateResponseBodyType.swift | 151 +++++++++++------- .../util/StaticRoute+Response.swift | 7 +- 14 files changed, 369 insertions(+), 114 deletions(-) create mode 100644 Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift create mode 100644 Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift create mode 100644 Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift diff --git a/Sources/Destiny/extensions/Int32Extensions.swift b/Sources/Destiny/extensions/Int32Extensions.swift index c2bb6a6e..e4eab869 100644 --- a/Sources/Destiny/extensions/Int32Extensions.swift +++ b/Sources/Destiny/extensions/Int32Extensions.swift @@ -73,6 +73,20 @@ extension Int32 { } } + public func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) { + let result = withUnsafePointer(to: (b1, b2, b3, b4)) { + writev(fileDescriptor, UnsafePointer(OpaquePointer($0)), 4) + } + if result <= 0 { + throw .socketWriteFailed(errno) + } + } + public func writeBuffers4( _ b1: iovec, _ b2: UnsafeBufferPointer, diff --git a/Sources/Destiny/http/HTTPSocket.swift b/Sources/Destiny/http/HTTPSocket.swift index 3e77f72a..3b3d84ba 100644 --- a/Sources/Destiny/http/HTTPSocket.swift +++ b/Sources/Destiny/http/HTTPSocket.swift @@ -128,6 +128,15 @@ extension HTTPSocket { try fileDescriptor.writeBuffers3(b1, b2, b3) } + public func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) { + try fileDescriptor.writeBuffers4(b1, b2, b3, b4) + } + public func writeBuffers4( _ b1: iovec, _ b2: UnsafeBufferPointer, diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift new file mode 100644 index 00000000..4e4a57bf --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift @@ -0,0 +1,28 @@ + +#if canImport(Android) +import Android +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Windows) +import Windows +#elseif canImport(WinSDK) +import WinSDK +#endif + +public struct NonCopyableCompressedBody: Sendable { + let bytes:[count of UInt8] + + public func iovec(_ closure: (iovec) -> Void) { + bytes.span.withUnsafeBufferPointer { + closure(.init(iov_base: .init(mutating: $0.baseAddress), iov_len: $0.count)) + } + } +} \ No newline at end of file diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift new file mode 100644 index 00000000..95c51bb0 --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift @@ -0,0 +1,71 @@ + +#if NonCopyableDateHeaderPayload + +#if canImport(Android) +import Android +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Windows) +import Windows +#elseif canImport(WinSDK) +import WinSDK +#endif + +/// Default storage to efficiently handle the `date` header payload for responders. +public struct NonCopyableDateHeaderPayloadWithBody: @unchecked Sendable, ~Copyable { + @usableFromInline let preDatePointer:UnsafePointer + @usableFromInline let postDatePointer:UnsafePointer + @usableFromInline let preDateIovec:iovec + @usableFromInline let postDateIovec:iovec + @usableFromInline let body:[count of UInt8] + + public init( + preDate: StaticString, + postDate: StaticString, + body: [count of UInt8] + ) { + self.preDatePointer = preDate.utf8Start + self.postDatePointer = postDate.utf8Start + self.preDateIovec = .init(iov_base: .init(mutating: preDate.utf8Start), iov_len: preDate.utf8CodeUnitCount) + self.postDateIovec = .init(iov_base: .init(mutating: postDate.utf8Start), iov_len: postDate.utf8CodeUnitCount) + self.body = body + } + + package init( + _ payload: borrowing Self + ) { + self.preDatePointer = payload.preDatePointer + self.postDatePointer = payload.postDatePointer + self.preDateIovec = payload.preDateIovec + self.postDateIovec = payload.postDateIovec + self.body = payload.body + } + + /// Efficiently writes the `preDate` value, `date` header and `postDate` value to a file descriptor. + /// + /// - Throws: `DestinyError` + public func write(to socket: some FileDescriptor) throws(DestinyError) { + do { // TODO: fix + try body.span.withUnsafeBufferPointer { + try socket.writeBuffers4( + preDateIovec, + HTTPDateFormat.nowIovec, + postDateIovec, + .init(iov_base: .init(mutating: $0.baseAddress), iov_len: $0.count) + ) + } + } catch { + throw .custom("\(error)") + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift new file mode 100644 index 00000000..abe52a0d --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift @@ -0,0 +1,64 @@ + +#if NonCopyableStaticStringWithDateHeader + +import UnwrapArithmeticOperators + +public struct NonCopyableStaticStringWithDateHeaderAndCompressedBody: Sendable, ~Copyable { + public let payload:NonCopyableDateHeaderPayloadWithBody + + public init( + preDateValue: StaticString, + postDateValue: StaticString, + body: [count of UInt8] + ) { + payload = .init( + preDate: preDateValue, + postDate: postDateValue, + body: body + ) + } + + public var count: Int { + payload.preDateIovec.iov_len +! HTTPDateFormat.InlineArrayResult.count +! payload.postDateIovec.iov_len + count + } + + public func string() -> String { + "\(String(cString: payload.preDatePointer))\(HTTPDateFormat.placeholder)\(String(cString: payload.postDatePointer))" + } + + public var hasDateHeader: Bool { + true + } +} + +// MARK: Write to buffer +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody { + public func write(to buffer: UnsafeMutableBufferPointer, at index: inout Int) { + index = 0 + buffer.copyBuffer(baseAddress: payload.preDatePointer, count: payload.preDateIovec.iov_len, at: &index) + buffer.copyBuffer(baseAddress: HTTPDateFormat.nowUnsafeBufferPointer.baseAddress!, count: HTTPDateFormat.count, at: &index) + buffer.copyBuffer(baseAddress: payload.postDatePointer, count: payload.postDateIovec.iov_len, at: &index) + } +} + +// MARK: Respond +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody { + public func respond( + provider: some SocketProvider, + router: borrowing some NonCopyableHTTPRouterProtocol & ~Copyable, + request: inout HTTPRequest + ) throws(DestinyError) { + try payload.write(to: request.fileDescriptor) + request.fileDescriptor.flush(provider: provider) + } +} + +#if Protocols + +// MARK: Conformances +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody: ResponseBodyProtocol {} +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody: NonCopyableRouteResponderProtocol {} + +#endif + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/protocols/FileDescriptor.swift b/Sources/Destiny/util/protocols/FileDescriptor.swift index 284b1842..0b2eb960 100644 --- a/Sources/Destiny/util/protocols/FileDescriptor.swift +++ b/Sources/Destiny/util/protocols/FileDescriptor.swift @@ -62,6 +62,16 @@ public protocol FileDescriptor: NetworkAddressable, ~Copyable { _ b3: iovec ) throws(DestinyError) + /// Efficiently writes 4 buffers to the file descriptor. + /// + /// - Throws: `DestinyError` + func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) + /// Efficiently writes 4 buffers to the file descriptor. /// /// - Throws: `DestinyError` diff --git a/Sources/DestinyMacros/parse/Route+Parse.swift b/Sources/DestinyMacros/parse/Route+Parse.swift index 1e33cfa0..01a68406 100644 --- a/Sources/DestinyMacros/parse/Route+Parse.swift +++ b/Sources/DestinyMacros/parse/Route+Parse.swift @@ -212,8 +212,10 @@ extension Route { if withDateHeader { // auto-upgrade switch body?.type { - case .string: body!.type = .stringWithDateHeader - case .staticString: body!.type = .staticStringWithDateHeader + case .string(let isNonCopyable, false, false, let withCompressedBody): + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: false, withDateHeader: true, withCompressedBody: withCompressedBody) + case .string(let isNonCopyable, true, false, let withCompressedBody): + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: true, withDateHeader: true, withCompressedBody: withCompressedBody) default: break } } diff --git a/Sources/DestinyMacros/router/Router+Compute.swift b/Sources/DestinyMacros/router/Router+Compute.swift index 8fa301ed..734d53a3 100644 --- a/Sources/DestinyMacros/router/Router+Compute.swift +++ b/Sources/DestinyMacros/router/Router+Compute.swift @@ -309,7 +309,10 @@ extension Router { let contentType = "text/plain" let charset = Charset.utf8 let stringLiteral = StringLiteralExprSyntax(content: body) - let intermediateBody = IntermediateResponseBody(type: .staticStringWithDateHeader, .init(stringLiteral)) + let intermediateBody = IntermediateResponseBody( + type: .string(isNonCopyable: false, isStatic: true, withDateHeader: true, withCompressedBody: false), + .init(stringLiteral) + ) #if hasFeature(Embedded) || EMBEDDED let response:HTTPResponseMessage diff --git a/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift b/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift index 255a018e..32720c31 100644 --- a/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift +++ b/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift @@ -83,7 +83,7 @@ extension RouterStorage { routePaths.reserveCapacity(reservedCapacity) routeResponders.reserveCapacity(reservedCapacity) - appendStaticRoutes( + let appendedStaticRoutes = appendStaticRoutes( context: context, isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, @@ -105,6 +105,7 @@ extension RouterStorage { staticConstants( isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, + routes: appendedStaticRoutes, routePaths: routePaths, members: &members, routeResponders: routeResponders @@ -134,6 +135,7 @@ extension RouterStorage { private func staticConstants( isCaseSensitive: Bool, isCopyable: Bool, + routes: StaticAppendedRoutes, routePaths: [String], members: inout MemberBlockItemListSyntax, routeResponders: [String] diff --git a/Sources/DestinyMacros/router/Router+Routes+Static.swift b/Sources/DestinyMacros/router/Router+Routes+Static.swift index d252019e..9fe88e7c 100644 --- a/Sources/DestinyMacros/router/Router+Routes+Static.swift +++ b/Sources/DestinyMacros/router/Router+Routes+Static.swift @@ -150,7 +150,7 @@ extension RouterStorage { routes: [(StaticRoute, FunctionCallExprSyntax)], routePaths: inout [String], routeResponders: inout [String] - ) { + ) -> StaticAppendedRoutes { appendStaticRoutes( context: context, isCaseSensitive: isCaseSensitive, @@ -169,16 +169,18 @@ extension RouterStorage { routes: [(StaticRoute, FunctionCallExprSyntax)], routePaths: inout [String], routeResponders: inout [String] - ) { + ) -> StaticAppendedRoutes { let getResponderValue:(RouterStorage.Route) -> String = { "// \($0.startLine)\nCompiledStaticResponderStorageRoute(\npath: \($0.buffer),\nresponder: \($0.responder)\n)" } + var appended = StaticAppendedRoutes() if !isCopyable { // always make redirects noncopyable for optimal performance #if StaticRedirectionRoute appendStaticRedirects( context: context, isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, + appended: &appended, routePaths: &routePaths, routeResponders: &routeResponders, data: data, @@ -211,6 +213,7 @@ extension RouterStorage { routePaths.append(startLine) routeResponders.append(responder) staticRouteStorage.remove(isCaseSensitive: isCaseSensitive, path: route.path, function: function) + appended.normal.append(route) } else { guard !registeredPaths.contains(startLine) else { Router.routePathAlreadyRegistered(context: context, node: function, startLine) @@ -229,6 +232,7 @@ extension RouterStorage { )*/ } } + return appended } } @@ -240,6 +244,7 @@ extension RouterStorage { context: some MacroExpansionContext, isCaseSensitive: Bool, isCopyable: Bool, + appended: inout StaticAppendedRoutes, routePaths: inout [String], routeResponders: inout [String], data: borrowing SharedStaticRouteResponderData, @@ -263,10 +268,11 @@ extension RouterStorage { let stringLiteral = StringLiteralExprSyntax(content: "") let responder = IntermediateResponseBody( - type: .staticStringWithDateHeader, + type: .string(isNonCopyable: false, isStatic: true, withDateHeader: true, withCompressedBody: false), .init(stringLiteral) ).responderDebugDescription(context: context, isCopyable: isCopyable, response: route.response()) routeResponders.append(responder) + appended.redirects.append(redirect.0) } for i in removedRedirects.reversed() { staticRedirects.remove(at: i) @@ -274,4 +280,15 @@ extension RouterStorage { } } -#endif \ No newline at end of file +#endif + +// MARK: Appended routes +extension RouterStorage { + struct StaticAppendedRoutes: Sendable { + #if StaticRedirectionRoute + var redirects = [StaticRedirectionRoute]() + #endif + + var normal = [StaticRoute]() + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift index 0603b5c0..eb259099 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift @@ -31,41 +31,6 @@ extension IntermediateResponseBody { var (preDate, postDate) = preDateAndPostDateValues(responseString) postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" return "\(prefix)StreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .stringWithDateHeader: - if interpolation == 0 { - // upgrade - return IntermediateResponseBody( - valueExpr: valueExpr, - type: .staticStringWithDateHeader, - value: escapedValue(), - count: count, - interpolation: interpolation, - rawValue: rawValue, - ).responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues(responseString) - return "StringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter), value: \(delimiter)\"\(escapedValue())\"\(delimiter))" - case .staticString: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - return "StaticString(\(delimiter)\"\(responseString)\(escapedValue())\"\(delimiter))" - case .staticStringWithDateHeader: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "\(prefix)StaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" - - case .string: - var s = responseString + value - if s.first != "\"" { - s.insert("\"", at: s.startIndex) - } - if s.last != "\"" { - s.append("\"") - } - if let stringLiteral = valueExpr.stringLiteral, let openingPounds = stringLiteral.openingPounds, let closingPounds = stringLiteral.closingPounds { - s = openingPounds.text + s + closingPounds.text - } - return s case .nonCopyableBytes: return "NonCopyableBytes(\(bytesPayload(context: context, responseString: &responseString)))" @@ -79,10 +44,31 @@ extension IntermediateResponseBody { var (preDate, postDate) = preDateAndPostDateValues(responseString) postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" return "NonCopyableStreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .nonCopyableStaticStringWithDateHeader: + + case .string(let isNonCopyable, let isStatic, let withDateHeader, let withCompressedBody): let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "NonCopyableStaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" + var (preDate, postDate):(Substring, Substring) + var trailingSuffix:Substring = "" + var targetType:String + if isStatic || interpolation == 0 { + targetType = "StaticString" + (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") + } else { + targetType = "String" + (preDate, postDate) = preDateAndPostDateValues(responseString) + } + if withDateHeader { + targetType += "WithDateHeader" + } + if !postDate.hasSuffix(delimiter) { + postDate += delimiter + } + if withCompressedBody { + targetType += "AndCompressedBody" + (preDate, postDate) = preDateAndPostDateValues(responseString) + trailingSuffix = ", body: \(rawValue ?? [])" + } + return "\(isNonCopyable ? "NonCopyable" : prefix)\(targetType)(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter)\(trailingSuffix))" } } private func escapedValue() -> String { diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody.swift b/Sources/DestinyMacros/util/IntermediateResponseBody.swift index 821f7c87..2f06f10b 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody.swift @@ -104,16 +104,16 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { .inlineBytes, .macroExpansion, .macroExpansionWithDateHeader, - .stringWithDateHeader, - .staticString, - .staticStringWithDateHeader, .streamWithDateHeader, .nonCopyableBytes, .nonCopyableInlineBytes, .nonCopyableMacroExpansionWithDateHeader, - .nonCopyableStaticStringWithDateHeader, .nonCopyableStreamWithDateHeader: true + case .string(false, false, true, _), + .string(false, true, _, _), + .string(true, _, _, _): + true default: false } @@ -123,12 +123,11 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { switch type { case .macroExpansionWithDateHeader, .streamWithDateHeader, - .staticStringWithDateHeader, - .stringWithDateHeader, .nonCopyableMacroExpansionWithDateHeader, - .nonCopyableStaticStringWithDateHeader, .nonCopyableStreamWithDateHeader: true + case .string(_, _, let withDateHeader, _): + withDateHeader default: false } @@ -154,9 +153,15 @@ extension IntermediateResponseBody { if let string = expr.stringLiteral { if string.segments.firstIndex(where: { $0.is(ExpressionSegmentSyntax.self) }) == nil { // can be upgraded to a `StaticString` - return Self(type: .staticString, .init(expr)) + return Self( + type: .string(isNonCopyable: false, isStatic: true, withDateHeader: false, withCompressedBody: false), + .init(expr) + ) } - return Self(type: .string, .init(expr)) + return Self( + type: .string(isNonCopyable: false, isStatic: false, withDateHeader: false, withCompressedBody: false), + .init(expr) + ) } return nil } @@ -165,8 +170,8 @@ extension IntermediateResponseBody { if key == nil { key = function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text.lowercased() } - if let key, let type = IntermediateResponseBodyType(rawValue: key) { - return Self(type: type, firstArg.expression) + if let key, let t = IntermediateResponseBodyType.parse(key: key, args: function.arguments) { + return Self(type: t, firstArg.expression) } context.diagnose(DiagnosticMsg.unhandled(node: expr)) return nil diff --git a/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift index c5df1395..08dae528 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift @@ -1,21 +1,20 @@ -public enum IntermediateResponseBodyType: String, Sendable { +import SwiftSyntax +import SwiftSyntaxMacros + +public enum IntermediateResponseBodyType: Equatable, Sendable { case bytes - case inlineBytes = "inlinebytes" - case macroExpansion = "macroexpansion" - case macroExpansionWithDateHeader = "macroexpansionwithdateheader" - case streamWithDateHeader = "streamwithdateheader" - case staticString = "staticstring" - case staticStringWithDateHeader = "staticstringwithdateheader" - case stringWithDateHeader = "stringwithdateheader" + case inlineBytes + case macroExpansion + case macroExpansionWithDateHeader + case streamWithDateHeader - case string + case string(isNonCopyable: Bool, isStatic: Bool, withDateHeader: Bool, withCompressedBody: Bool) - case nonCopyableBytes = "noncopyablebytes" - case nonCopyableInlineBytes = "noncopyableinlinebytes" - case nonCopyableMacroExpansionWithDateHeader = "noncopyablemacroexpansionwithdateheader" - case nonCopyableStreamWithDateHeader = "noncopyablestreamwithdateheader" - case nonCopyableStaticStringWithDateHeader = "noncopyablestaticstringwithdateheader" + case nonCopyableBytes + case nonCopyableInlineBytes + case nonCopyableMacroExpansionWithDateHeader + case nonCopyableStreamWithDateHeader } // MARK: Is enabled @@ -24,86 +23,128 @@ extension IntermediateResponseBodyType { switch self { case .bytes: #if CopyableBytes - true + return true #else - false + return false #endif case .inlineBytes: #if CopyableInlineBytes - true + return true #else - false + return false #endif case .macroExpansion: #if CopyableMacroExpansion - true + return true #else - false + return false #endif case .macroExpansionWithDateHeader: #if CopyableMacroExpansionWithDateHeader - true + return true #else - false + return false #endif case .streamWithDateHeader: #if CopyableStreamWithDateHeader - true - #else - false - #endif - case .staticString: - true - case .staticStringWithDateHeader: - #if CopyableStaticStringWithDateHeader - true + return true #else - false - #endif - case .stringWithDateHeader: - #if CopyableStringWithDateHeader - true - #else - false + return false #endif - case .string: + case .string(let isNonCopyable, let isStatic, let withDateHeader, let withCompressedBody): + if isNonCopyable { + if isStatic { + if withDateHeader { + if withCompressedBody { + #if NonCopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + #if NonCopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + } + } + // copyable + if isStatic { + if withDateHeader { + if withCompressedBody { + #if CopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + #if CopyableStringWithDateHeader + return true + #else + return false + #endif + } + return true + } + // copyable, not static + if withDateHeader { + #if CopyableStringWithDateHeader + return true + #else + return false + #endif + } #if StringRouteResponder - true + return true #else - false + return false #endif case .nonCopyableBytes: #if NonCopyableBytes - true + return true #else - false + return false #endif case .nonCopyableInlineBytes: #if NonCopyableInlineBytes - true + return true #else - false + return false #endif case .nonCopyableMacroExpansionWithDateHeader: #if NonCopyableMacroExpansionWithDateHeader - true + return true #else - false + return false #endif case .nonCopyableStreamWithDateHeader: #if NonCopyableStreamWithDateHeader - true - #else - false - #endif - case .nonCopyableStaticStringWithDateHeader: - #if NonCopyableStaticStringWithDateHeader - true + return true #else - false + return false #endif } } +} + +// MARK: Parse +extension IntermediateResponseBodyType { + public static func parse(key: String, args: LabeledExprListSyntax) -> Self? { + switch key { + case "bytes": .bytes + case "inlinebytes": .inlineBytes + case "macroexpansion": .macroExpansion + case "macroexpansionwithdateheader": .macroExpansionWithDateHeader + case "streamWithDateHeader": .streamWithDateHeader + case "noncopyablebytes": .nonCopyableBytes + case "noncopyableinlinebytes": .nonCopyableInlineBytes + case "noncopyablemacroexpansionwithdateheader": .nonCopyableMacroExpansionWithDateHeader + case "noncopyablestreamwithdateheader": .nonCopyableStreamWithDateHeader + default: nil + } + } } \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index 8379bd74..cf14badc 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -122,11 +122,14 @@ extension StaticRoute { headers["content-type"] = nil headers["content-length"] = nil - if body != nil, contentType == "text/html" { - if let compressed = Gzip().compress(span: body!.value.utf8Span.span) { + if body != nil, (contentType == "text/html" || contentType == "text/plain" || contentType == "application/json") { + if let compressed = Gzip().compress(span: body!.value.utf8Span.span), compressed.count < body!.count { headers["content-encoding"] = "gzip" headers["vary"] = "Accept-Encoding" body!.rawValue = compressed + if case let .string(isNonCopyable, isStatic, withDateHeader, _) = body!.type { + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: isStatic, withDateHeader: withDateHeader, withCompressedBody: true) + } } } return HTTPResponseMessage( From e547e2bf11dbd8af40fa2bc924c9aff5b413536c Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Wed, 8 Apr 2026 16:06:59 -0500 Subject: [PATCH 04/12] stuff - add `Compression` package trait - mark compression support as enabled in readme - add `CompressionSettings` and `CompressorSettings` - add compression support to `RouterSettings` - remove some unused code for `StaticRoute` --- Package.swift | 6 + README.md | 2 +- .../SwiftCompressionExtensions.swift | 98 +-------------- .../Destiny/util/CompressionSettings.swift | 62 ++++++++++ Sources/Destiny/util/CompressorSettings.swift | 23 ++++ Sources/Destiny/util/RouterSettings.swift | 70 ++++++++--- .../parse/CompressionAlgorithm+Parse.swift | 115 ++++++++++++++++++ .../parse/CompressionSettings+Parse.swift | 51 ++++++++ .../parse/CompressorSettings+Parse.swift | 35 ++++++ .../parse/RouterSettings+Parse.swift | 6 + .../router/Router+Routes+Static.swift | 4 +- .../util/StaticRoute+Response.swift | 42 +++++-- Sources/DestinyMacros/util/StaticRoute.swift | 38 ------ 13 files changed, 392 insertions(+), 160 deletions(-) create mode 100644 Sources/Destiny/util/CompressionSettings.swift create mode 100644 Sources/Destiny/util/CompressorSettings.swift create mode 100644 Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift create mode 100644 Sources/DestinyMacros/parse/CompressionSettings+Parse.swift create mode 100644 Sources/DestinyMacros/parse/CompressorSettings+Parse.swift diff --git a/Package.swift b/Package.swift index f9f7138c..fe19a206 100644 --- a/Package.swift +++ b/Package.swift @@ -92,6 +92,7 @@ defaultTraits.formUnion([ "UnwrapArithmetic", "Protocols", + "Compression", "Logging", "OpenAPI" ]) @@ -370,6 +371,10 @@ let traits:Set = [ description: "Enables the design protocols." ), + .trait( + name: "Compression", + description: "Enables compression support (using swift-compression)." + ), .trait( name: "Epoll", description: "Enables Epoll functionality (Linux only)." @@ -408,6 +413,7 @@ var targets = [ .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), + .product(name: "SwiftCompression", package: "swift-compression"), ] ), diff --git a/README.md b/README.md index 0fe188bc..38c996d3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Features like TLS/SSL, Web Sockets and embedded support are coming soon. - [x] Cookies (Aug 9, 2025) - [x] Header parsing (Sep 9, 2025) - [x] Request body streaming (Sep 10, 2025) +- [x] Compression support (Apr 8, 2026) ### WIP @@ -63,7 +64,6 @@ Features like TLS/SSL, Web Sockets and embedded support are coming soon. - [ ] Cache Middleware - [ ] Data Validation (form, POST, etc) - [ ] Authentication -- [ ] Compression support - [ ] OpenAPI support - [ ] Tracing support - [ ] TLS/SSL diff --git a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift index 365552b1..d0e709d3 100644 --- a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift +++ b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift @@ -1,7 +1,7 @@ -/* +#if Compression + import SwiftCompression -import SwiftSyntax extension CompressionAlgorithm { public var acceptEncodingName: String { @@ -10,100 +10,12 @@ extension CompressionAlgorithm { case .huffmanCoding: "huffman" case .lzw: "compress" + case .gzip: "gzip" + case ._7z: "7z" default: rawValue } } } -#if canImport(SwiftSyntax) -// MARK: SwiftSyntax -extension CompressionAlgorithm { - public static func parse(_ expr: some ExprSyntaxProtocol) -> Self? { - let key:String - guard let function = expr.functionCall else { return nil } - if let string = function.calledExpression.memberAccess?.declName.baseName.text { - key = string - } else { - return nil - } - let arguments = function.arguments - switch key { - /*case "aac": self = .aac - case "mp3": self = .mp3 - - case "arithmetic": self = .arithmetic - case "brotli": self = .brotli - - case "bwt": self = .bwt - case "deflate": self = .deflate - case "huffmanCoding": self = .huffman(rootNode: nil) - case "json": self = .json - case "lz4": self = .lz4*/ - case "lz77": - var windowSize:Int = 0, bufferSize:Int = 0, offsetBitWidth:Int = 0 - for child in arguments { - switch child.label?.text { - case "windowSize": windowSize = Int(child.expression.integerLiteral!.literal.text)! - case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text)! - case "offsetBitWidth": offsetBitWidth = Int(child.expression.integerLiteral!.literal.text)! - default: break - } - } - return .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: offsetBitWidth) - /*case "lz78": self = .lz78 - case "lzw": self = .lzw - case "mtf": self = .mtf*/ - case "runLengthEncoding": - var minRun:Int = 0, alwaysIncludeRunCount:Bool = false - for child in arguments { - switch child.label?.text { - case "minRun": minRun = Int(child.expression.integerLiteral!.literal.text)! - case "alwaysIncludeRunCount": alwaysIncludeRunCount = child.expression.booleanLiteral!.isTrue - default: break - } - } - return .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - case "snappy": return CompressionAlgorithm.snappy(windowSize: 32_000) - /*case "snappyFramed": self = .snappyFramed - case "zstd": self = .zstd - - case "_7z": self = ._7z - case "bzip2": self = .bzip2 - case "gzip": self = .gzip - case "rar": self = .rar - - case "h264": self = .h264 - case "h265": self = .h265 - case "jpeg": self = .jpeg - case "jpeg2000": self = .jpeg2000 - - case "eliasDelta": self = .eliasDelta - case "eliasGamma": self = .eliasGamma - case "eliasOmega": self = .eliasOmega - case "fibonacci": self = .fibonacci*/ - - case "dnaBinaryEncoding": - var baseBits:[UInt8:[Bool]] = [:] - for child in arguments { - switch child.label?.text { - case "baseBits": - child.expression.dictionary?.content.as(DictionaryElementListSyntax.self)!.forEach({ - baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = $0.value.array!.elements.map({ $0.expression.booleanLiteral!.isTrue }) - }) - default: break - } - } - return .dnaBinaryEncoding(baseBits: baseBits) - /*case "dnaSingleBlockEncoding": self = .dnaSingleBlockEncoding - - case "boringSSL": self = .boringSSL - - case "av1": self = .av1 - case "dirac": self = .dirac - case "mpeg": self = .mpeg*/ - default: return nil - } - } -} -#endif*/ \ No newline at end of file +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/CompressionSettings.swift b/Sources/Destiny/util/CompressionSettings.swift new file mode 100644 index 00000000..cfa4e1c1 --- /dev/null +++ b/Sources/Destiny/util/CompressionSettings.swift @@ -0,0 +1,62 @@ + +#if Compression + +import SwiftCompression +import ZlibShim + +public struct CompressionSettings: Sendable { + var flags:Flags.RawValue + public let supportedCompressionAlgorithms:[CompressionAlgorithm:CompressorSettings] + + public init( + enabled: Bool = true, + compressOnlyIfResultIsSmaller: Bool = true, + supportedCompressionAlgorithms: [CompressionAlgorithm:CompressorSettings] = [ + .gzip(bufferSize: 1024, level: Z_DEFAULT_COMPRESSION, memLevel: 8, strategy: Z_DEFAULT_STRATEGY): .init(contentTypePrefixWhitelist: "text/") + ] + ) { + flags = Flags.pack( + enabled: enabled, + compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller + ) + self.supportedCompressionAlgorithms = supportedCompressionAlgorithms + } + + public var isEnabled: Bool { + isFlag(.enabled) + } + public var compressOnlyIfResultIsSmaller: Bool { + isFlag(.compressOnlyIfResultIsSmaller) + } + + func isFlag(_ flag: Flags) -> Bool { + flags & flag.rawValue != 0 + } + + mutating func setFlag(_ flag: Flags, _ value: Bool) { + if value { + flags |= flag.rawValue + } else { + flags &= ~flag.rawValue + } + } +} + +// MARK: Flags +extension CompressionSettings { + enum Flags: UInt8 { + case enabled = 1 + case compressOnlyIfResultIsSmaller = 2 + } +} +extension CompressionSettings.Flags { + static func pack( + enabled: Bool, + compressOnlyIfResultIsSmaller: Bool + ) -> RawValue { + (enabled ? Self.enabled.rawValue : 0) + | (compressOnlyIfResultIsSmaller ? Self.compressOnlyIfResultIsSmaller.rawValue : 0) + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/CompressorSettings.swift b/Sources/Destiny/util/CompressorSettings.swift new file mode 100644 index 00000000..c82e5ff6 --- /dev/null +++ b/Sources/Destiny/util/CompressorSettings.swift @@ -0,0 +1,23 @@ + +#if Compression + +public struct CompressorSettings: Sendable { + public package(set) var contentTypePrefixWhitelist:String? + public package(set) var contentTypePrefixBlacklist:String? + public package(set) var contentTypeWhitelist:Set + public package(set) var contentTypeBlacklist:Set + + public init( + contentTypePrefixWhitelist: String? = nil, + contentTypeWhitelist: Set = [], + contentTypePrefixBlacklist: String? = nil, + contentTypeBlacklist: Set = [] + ) { + self.contentTypePrefixWhitelist = contentTypePrefixWhitelist + self.contentTypeWhitelist = contentTypeWhitelist + self.contentTypePrefixBlacklist = contentTypePrefixBlacklist + self.contentTypeBlacklist = contentTypeBlacklist + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/RouterSettings.swift b/Sources/Destiny/util/RouterSettings.swift index dee8329a..e1bc8280 100644 --- a/Sources/Destiny/util/RouterSettings.swift +++ b/Sources/Destiny/util/RouterSettings.swift @@ -12,25 +12,10 @@ public struct RouterSettings: Sendable { /// Access control for the router. public var visibility:RouterVisibility - public init( - mutable: Bool = false, - dynamicResponsesAreGeneric: Bool = true, - respondersAreComputedProperties: Bool = false, - protocolConformances: Bool = true, - logging: Bool = true, - visibility: RouterVisibility = .internal, - name: String = "CompiledHTTPRouter" - ) { - self.visibility = visibility - self.name = name - flags = Flags.pack( - mutable: mutable, - dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, - respondersAreComputedProperties: respondersAreComputedProperties, - protocolConformances: protocolConformances, - logging: logging - ) - } + #if Compression + package var compression:CompressionSettings + #endif + /// Whether or not this router is mutable. /// @@ -76,6 +61,53 @@ public struct RouterSettings: Sendable { } } +// MARK: Init +extension RouterSettings { + #if Compression + public init( + mutable: Bool = false, + dynamicResponsesAreGeneric: Bool = true, + respondersAreComputedProperties: Bool = false, + protocolConformances: Bool = true, + logging: Bool = true, + visibility: RouterVisibility = .internal, + name: String = "CompiledHTTPRouter", + compression: CompressionSettings = .init() + ) { + self.visibility = visibility + self.name = name + self.compression = compression + flags = Flags.pack( + mutable: mutable, + dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, + respondersAreComputedProperties: respondersAreComputedProperties, + protocolConformances: protocolConformances, + logging: logging + ) + } + #else + public init( + mutable: Bool = false, + dynamicResponsesAreGeneric: Bool = true, + respondersAreComputedProperties: Bool = false, + protocolConformances: Bool = true, + logging: Bool = true, + visibility: RouterVisibility = .internal, + name: String = "CompiledHTTPRouter" + ) { + self.visibility = visibility + self.name = name + flags = Flags.pack( + mutable: mutable, + dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, + respondersAreComputedProperties: respondersAreComputedProperties, + protocolConformances: protocolConformances, + logging: logging + ) + } + #endif +} + // MARK: Flags extension RouterSettings { @usableFromInline diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift new file mode 100644 index 00000000..f7432ab5 --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -0,0 +1,115 @@ + +#if Compression + +import SwiftCompression +import SwiftSyntax +import ZlibShim + +// MARK: SwiftSyntax +extension CompressionAlgorithm { + public static func parse( + _ expr: some ExprSyntaxProtocol + ) -> Self? { + let key:String + guard let function = expr.functionCall else { return nil } + if let string = function.calledExpression.memberAccess?.declName.baseName.text { + key = string + } else { + return nil + } + let arguments = function.arguments + switch key { + /*case "aac": self = .aac + case "mp3": self = .mp3 + + case "arithmetic": self = .arithmetic + case "brotli": self = .brotli + + case "bwt": self = .bwt + case "deflate": self = .deflate + case "huffmanCoding": self = .huffman(rootNode: nil) + case "json": self = .json + case "lz4": self = .lz4*/ + case "lz77": + var windowSize = 0, bufferSize = 0, offsetBitWidth = 0 + for child in arguments { + switch child.label?.text { + case "windowSize": windowSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "offsetBitWidth": offsetBitWidth = Int(child.expression.integerLiteral!.literal.text) ?? 0 + default: break + } + } + return .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: offsetBitWidth) + /*case "lz78": self = .lz78 + case "lzw": self = .lzw + case "mtf": self = .mtf*/ + + case "gzip": + var bufferSize = 1024, level = Z_DEFAULT_COMPRESSION, memLevel:Int32 = 8, strategy = Z_DEFAULT_STRATEGY + for child in arguments { + switch child.label?.text { + case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "level": level = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "memLevel": memLevel = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "strategy": strategy = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + default: + break + } + } + return .gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) + + case "runLengthEncoding": + var minRun = 0, alwaysIncludeRunCount:Bool = false + for child in arguments { + switch child.label?.text { + case "minRun": minRun = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "alwaysIncludeRunCount": alwaysIncludeRunCount = child.expression.booleanIsTrue + default: break + } + } + return .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) + case "snappy": return CompressionAlgorithm.snappy(windowSize: 32_000) + /*case "snappyFramed": self = .snappyFramed + case "zstd": self = .zstd + + case "_7z": self = ._7z + case "bzip2": self = .bzip2 + case "gzip": self = .gzip + case "rar": self = .rar + + case "h264": self = .h264 + case "h265": self = .h265 + case "jpeg": self = .jpeg + case "jpeg2000": self = .jpeg2000 + + case "eliasDelta": self = .eliasDelta + case "eliasGamma": self = .eliasGamma + case "eliasOmega": self = .eliasOmega + case "fibonacci": self = .fibonacci*/ + + case "dnaBinaryEncoding": + var baseBits:[UInt8:[Bool]] = [:] + for child in arguments { + switch child.label?.text { + case "baseBits": + child.expression.dictionary?.content.as(DictionaryElementListSyntax.self)!.forEach({ + baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = $0.value.array!.elements.map({ $0.expression.booleanIsTrue }) + }) + default: break + } + } + return .dnaBinaryEncoding(baseBits: baseBits) + /*case "dnaSingleBlockEncoding": self = .dnaSingleBlockEncoding + + case "boringSSL": self = .boringSSL + + case "av1": self = .av1 + case "dirac": self = .dirac + case "mpeg": self = .mpeg*/ + default: return nil + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift new file mode 100644 index 00000000..d37614d1 --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift @@ -0,0 +1,51 @@ + +#if Compression + +import Destiny +import SwiftCompression +import SwiftSyntax +import SwiftSyntaxMacros + +extension CompressionSettings { + public static func parse( + context: some MacroExpansionContext, + expr: some ExprSyntaxProtocol + ) -> Self { + guard let function = expr.functionCall else { + return Self(enabled: false, compressOnlyIfResultIsSmaller: false, supportedCompressionAlgorithms: [:]) + } + var enabled = true + var compressOnlyIfResultIsSmaller = true + var supportedCompressionAlgorithms = [CompressionAlgorithm:CompressorSettings]() + for arg in function.arguments { + switch arg.label?.text { + case "enabled": + enabled = arg.expression.booleanIsTrue + case "compressOnlyIfResultIsSmaller": + compressOnlyIfResultIsSmaller = arg.expression.booleanIsTrue + case "supportedCompressionAlgorithms": + guard let dict = arg.expression.dictionary else { continue } + let _:DictionaryElementListSyntax + switch dict.content { + case .elements(let elements): + for e in elements { + guard let algorithm = CompressionAlgorithm.parse(e.key) else { continue } + supportedCompressionAlgorithms[algorithm] = CompressorSettings.parse(context: context, expr: e.value) + } + default: + break + } + break + default: + break + } + } + return Self( + enabled: enabled, + compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller, + supportedCompressionAlgorithms: supportedCompressionAlgorithms + ) + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift new file mode 100644 index 00000000..563aaa39 --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift @@ -0,0 +1,35 @@ + +#if Compression + +import Destiny +import SwiftSyntax +import SwiftSyntaxMacros + +extension CompressorSettings { + public static func parse( + context: some MacroExpansionContext, + expr: some ExprSyntaxProtocol + ) -> Self { + var settings = Self() + guard let function = expr.functionCall else { return settings } + for arg in function.arguments { + switch arg.label?.text { + case "contentTypePrefixWhitelist": + settings.contentTypePrefixWhitelist = arg.expression.stringLiteralString(context: context) + case "contentTypeWhitelist": + guard let array = arg.expression.arrayElements(context: context)?.compactMap({ $0.expression.stringLiteralString(context: context) }) else { continue } + settings.contentTypeWhitelist = Set(array) + case "contentTypePrefixBlacklist": + settings.contentTypePrefixBlacklist = arg.expression.stringLiteralString(context: context) + case "contentTypeBlacklist": + guard let array = arg.expression.arrayElements(context: context)?.compactMap({ $0.expression.stringLiteralString(context: context) }) else { continue } + settings.contentTypeBlacklist = Set(array) + default: + break + } + } + return settings + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/RouterSettings+Parse.swift b/Sources/DestinyMacros/parse/RouterSettings+Parse.swift index f6666343..16192973 100644 --- a/Sources/DestinyMacros/parse/RouterSettings+Parse.swift +++ b/Sources/DestinyMacros/parse/RouterSettings+Parse.swift @@ -30,6 +30,12 @@ extension RouterSettings { } case "visibility": settings.visibility = .init(rawValue: arg.expression.memberAccess?.declName.baseName.text ?? "internal") ?? .internal + + #if Compression + case "compression": + settings.compression = .parse(context: context, expr: arg.expression) + #endif + default: context.diagnose(DiagnosticMsg.unhandled(node: arg)) } diff --git a/Sources/DestinyMacros/router/Router+Routes+Static.swift b/Sources/DestinyMacros/router/Router+Routes+Static.swift index 9fe88e7c..459a188e 100644 --- a/Sources/DestinyMacros/router/Router+Routes+Static.swift +++ b/Sources/DestinyMacros/router/Router+Routes+Static.swift @@ -161,6 +161,8 @@ extension RouterStorage { routeResponders: &routeResponders ) } + + @discardableResult mutating func appendStaticRoutes( context: some MacroExpansionContext, isCaseSensitive: Bool, @@ -191,7 +193,7 @@ extension RouterStorage { for (var route, function) in routes { let startLine = data.routeStartLine(route) #if StaticMiddleware - let httpResponse = route.response(context: context, function: function, middleware: staticMiddleware) + let httpResponse = route.response(context: context, function: function, routerStorage: self, middleware: staticMiddleware) #else let httpResponse = route.response(context: context, function: function) #endif diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index cf14badc..853c59d9 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -16,9 +16,10 @@ extension StaticRoute { public mutating func response( context: some MacroExpansionContext, function: FunctionCallExprSyntax, + routerStorage: RouterStorage, middleware: [StaticMiddleware] ) -> HTTPResponseMessage { - let result = response(middleware: middleware) + let result = response(routerStorage: routerStorage, middleware: middleware) if result.statusCode() == 501 { // not implemented Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) } @@ -41,6 +42,7 @@ extension StaticRoute { extension StaticRoute { #if StaticMiddleware public mutating func response( + routerStorage: RouterStorage, middleware: [StaticMiddleware] ) -> HTTPResponseMessage { var version = version @@ -70,6 +72,7 @@ extension StaticRoute { #if HTTPCookie return Self.response( + routerStorage: routerStorage, version: version, status: status, headers: &headers, @@ -111,6 +114,8 @@ extension StaticRoute { #if HTTPCookie @inline(__always) package static func response( + routerStorage: RouterStorage, + version: HTTPVersion, status: HTTPResponseStatus.Code, headers: inout HTTPHeaders, @@ -122,16 +127,37 @@ extension StaticRoute { headers["content-type"] = nil headers["content-length"] = nil - if body != nil, (contentType == "text/html" || contentType == "text/plain" || contentType == "application/json") { - if let compressed = Gzip().compress(span: body!.value.utf8Span.span), compressed.count < body!.count { - headers["content-encoding"] = "gzip" - headers["vary"] = "Accept-Encoding" - body!.rawValue = compressed - if case let .string(isNonCopyable, isStatic, withDateHeader, _) = body!.type { - body!.type = .string(isNonCopyable: isNonCopyable, isStatic: isStatic, withDateHeader: withDateHeader, withCompressedBody: true) + #if RouterSettings && Compression + if body != nil, let contentType, routerStorage.settings.compression.isEnabled { + for (algorithm, algorithmSettings) in routerStorage.settings.compression.supportedCompressionAlgorithms { + if let prefixBlacklist = algorithmSettings.contentTypePrefixBlacklist, contentType.hasPrefix(prefixBlacklist) { + continue + } + if algorithmSettings.contentTypeBlacklist.contains(contentType) { + continue } + if let prefixWhitelist = algorithmSettings.contentTypePrefixWhitelist, !contentType.hasPrefix(prefixWhitelist) { + continue + } + guard !algorithmSettings.contentTypeWhitelist.contains(contentType) else { continue } + guard let technique = algorithm.technique else { continue } // TODO: support embedded + // TODO: support | swift-compression needs span support for its protocol(s) + /*if let compressed = technique.compress(data: body!.value.utf8Span.span) { + if routerStorage.settings.compression.compressOnlyIfResultIsSmaller, compressed.count >= body!.count { + continue + } + headers["content-encoding"] = algorithm.acceptEncodingName + headers["vary"] = "Accept-Encoding" + body!.rawValue = compressed + if case let .string(isNonCopyable, isStatic, withDateHeader, _) = body!.type { + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: isStatic, withDateHeader: withDateHeader, withCompressedBody: true) + } + break + }*/ } } + #endif + return HTTPResponseMessage( head: .init(headers: headers, cookies: cookies, status: status, version: version), body: body, diff --git a/Sources/DestinyMacros/util/StaticRoute.swift b/Sources/DestinyMacros/util/StaticRoute.swift index 40f3a576..59819bb7 100644 --- a/Sources/DestinyMacros/util/StaticRoute.swift +++ b/Sources/DestinyMacros/util/StaticRoute.swift @@ -69,42 +69,4 @@ extension StaticRoute { public mutating func insertPath(contentsOf newElements: some Collection, at i: Int) { path.insert(contentsOf: newElements, at: i) } -} - -// MARK: Responder -extension StaticRoute { - #if StaticMiddleware - public mutating func responder( - middleware: [StaticMiddleware] - ) -> String? { - return response(middleware: middleware).string(escapeLineBreak: true) - } - #else - public func responder() -> String? { - return response().string(escapeLineBreak: true) - } - #endif - - /// The `RouteResponderProtocol` responder for this route. - /// - /// - Parameters: - /// - context: Macro expansion context where it was called. - /// - function: `FunctionCallExprSyntax` that represents this route. - /// - middleware: Static middleware that this route will handle. - #if StaticMiddleware - public mutating func responder( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax, - middleware: [StaticMiddleware] - ) throws(DestinyError) -> String? { - return response(context: context, function: function, middleware: middleware).string(escapeLineBreak: true) - } - #else - public mutating func responder( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax - ) throws(DestinyError) -> String? { - return response(context: context, function: function).string(escapeLineBreak: true) - } - #endif } \ No newline at end of file From cdfc5b2ca0c10e260b9e6c970f0954ec1061f4b7 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Wed, 8 Apr 2026 20:53:08 -0500 Subject: [PATCH 05/12] support Brotli compression by default and... use default values by default when parsing `CompressionSettings` --- Sources/Destiny/util/CompressionSettings.swift | 10 ++++++---- .../parse/CompressionAlgorithm+Parse.swift | 13 ++++++++++++- .../parse/CompressionSettings+Parse.swift | 14 +++++--------- .../DestinyMacros/util/StaticRoute+Response.swift | 3 +-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Sources/Destiny/util/CompressionSettings.swift b/Sources/Destiny/util/CompressionSettings.swift index cfa4e1c1..f015f81a 100644 --- a/Sources/Destiny/util/CompressionSettings.swift +++ b/Sources/Destiny/util/CompressionSettings.swift @@ -1,17 +1,19 @@ #if Compression +import BrotliShim import SwiftCompression import ZlibShim public struct CompressionSettings: Sendable { - var flags:Flags.RawValue - public let supportedCompressionAlgorithms:[CompressionAlgorithm:CompressorSettings] + package var flags:Flags.RawValue + public package(set) var supportedCompressionAlgorithms:[CompressionAlgorithm:CompressorSettings] public init( enabled: Bool = true, compressOnlyIfResultIsSmaller: Bool = true, supportedCompressionAlgorithms: [CompressionAlgorithm:CompressorSettings] = [ + .brotli(quality: BROTLI_DEFAULT_QUALITY, windowSize: BROTLI_DEFAULT_WINDOW, mode: BROTLI_MODE_GENERIC.rawValue): .init(contentTypePrefixWhitelist: "text/"), .gzip(bufferSize: 1024, level: Z_DEFAULT_COMPRESSION, memLevel: 8, strategy: Z_DEFAULT_STRATEGY): .init(contentTypePrefixWhitelist: "text/") ] ) { @@ -44,13 +46,13 @@ public struct CompressionSettings: Sendable { // MARK: Flags extension CompressionSettings { - enum Flags: UInt8 { + package enum Flags: UInt8 { case enabled = 1 case compressOnlyIfResultIsSmaller = 2 } } extension CompressionSettings.Flags { - static func pack( + package static func pack( enabled: Bool, compressOnlyIfResultIsSmaller: Bool ) -> RawValue { diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift index f7432ab5..64d5f73c 100644 --- a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -1,6 +1,7 @@ #if Compression +import BrotliShim import SwiftCompression import SwiftSyntax import ZlibShim @@ -23,13 +24,23 @@ extension CompressionAlgorithm { case "mp3": self = .mp3 case "arithmetic": self = .arithmetic - case "brotli": self = .brotli case "bwt": self = .bwt case "deflate": self = .deflate case "huffmanCoding": self = .huffman(rootNode: nil) case "json": self = .json case "lz4": self = .lz4*/ + case "brotli": + var quality:Int32 = BROTLI_DEFAULT_QUALITY, windowSize:Int32 = BROTLI_DEFAULT_WINDOW, mode:UInt32 = BROTLI_MODE_GENERIC.rawValue + for child in arguments { + switch child.label?.text { + case "quality": quality = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "windowSize": windowSize = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "mode": mode = UInt32(child.expression.integerLiteral!.literal.text) ?? 0 + default: break + } + } + return .brotli(quality: quality, windowSize: windowSize, mode: mode) case "lz77": var windowSize = 0, bufferSize = 0, offsetBitWidth = 0 for child in arguments { diff --git a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift index d37614d1..7072e60a 100644 --- a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift @@ -14,9 +14,9 @@ extension CompressionSettings { guard let function = expr.functionCall else { return Self(enabled: false, compressOnlyIfResultIsSmaller: false, supportedCompressionAlgorithms: [:]) } + var settings = Self() var enabled = true var compressOnlyIfResultIsSmaller = true - var supportedCompressionAlgorithms = [CompressionAlgorithm:CompressorSettings]() for arg in function.arguments { switch arg.label?.text { case "enabled": @@ -24,27 +24,23 @@ extension CompressionSettings { case "compressOnlyIfResultIsSmaller": compressOnlyIfResultIsSmaller = arg.expression.booleanIsTrue case "supportedCompressionAlgorithms": + settings.supportedCompressionAlgorithms = [:] guard let dict = arg.expression.dictionary else { continue } - let _:DictionaryElementListSyntax switch dict.content { case .elements(let elements): for e in elements { guard let algorithm = CompressionAlgorithm.parse(e.key) else { continue } - supportedCompressionAlgorithms[algorithm] = CompressorSettings.parse(context: context, expr: e.value) + settings.supportedCompressionAlgorithms[algorithm] = CompressorSettings.parse(context: context, expr: e.value) } default: break } - break default: break } } - return Self( - enabled: enabled, - compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller, - supportedCompressionAlgorithms: supportedCompressionAlgorithms - ) + settings.flags = Self.Flags.pack(enabled: enabled, compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller) + return settings } } diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index 853c59d9..c5922dd3 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -141,8 +141,7 @@ extension StaticRoute { } guard !algorithmSettings.contentTypeWhitelist.contains(contentType) else { continue } guard let technique = algorithm.technique else { continue } // TODO: support embedded - // TODO: support | swift-compression needs span support for its protocol(s) - /*if let compressed = technique.compress(data: body!.value.utf8Span.span) { + /*if let compressed = technique.compress(span: body!.value.utf8Span.span, configuration: .default) { if routerStorage.settings.compression.compressOnlyIfResultIsSmaller, compressed.count >= body!.count { continue } From 63f2e3bca4aaae959cd90a182c3515f8593180fc Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 9 Apr 2026 12:43:15 -0500 Subject: [PATCH 06/12] snappy fix --- Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift index 64d5f73c..ea9a1639 100644 --- a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -80,7 +80,7 @@ extension CompressionAlgorithm { } } return .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - case "snappy": return CompressionAlgorithm.snappy(windowSize: 32_000) + case "snappy": return CompressionAlgorithm.snappy /*case "snappyFramed": self = .snappyFramed case "zstd": self = .zstd From d2c523fa6ac9056e5cc62a30f149e5a832fb873b Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 9 Apr 2026 13:03:37 -0500 Subject: [PATCH 07/12] add `contentLengthThreshold` to allow only compressing responses if their length meets a threshold --- Sources/Destiny/util/CompressorSettings.swift | 3 +++ Sources/DestinyMacros/parse/CompressorSettings+Parse.swift | 2 ++ Sources/DestinyMacros/util/StaticRoute+Response.swift | 3 +++ 3 files changed, 8 insertions(+) diff --git a/Sources/Destiny/util/CompressorSettings.swift b/Sources/Destiny/util/CompressorSettings.swift index c82e5ff6..3b9b90e9 100644 --- a/Sources/Destiny/util/CompressorSettings.swift +++ b/Sources/Destiny/util/CompressorSettings.swift @@ -2,17 +2,20 @@ #if Compression public struct CompressorSettings: Sendable { + public package(set) var contentLengthThreshold:Int? = nil public package(set) var contentTypePrefixWhitelist:String? public package(set) var contentTypePrefixBlacklist:String? public package(set) var contentTypeWhitelist:Set public package(set) var contentTypeBlacklist:Set public init( + contentLengthThreshold: Int? = nil, contentTypePrefixWhitelist: String? = nil, contentTypeWhitelist: Set = [], contentTypePrefixBlacklist: String? = nil, contentTypeBlacklist: Set = [] ) { + self.contentLengthThreshold = contentLengthThreshold self.contentTypePrefixWhitelist = contentTypePrefixWhitelist self.contentTypeWhitelist = contentTypeWhitelist self.contentTypePrefixBlacklist = contentTypePrefixBlacklist diff --git a/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift index 563aaa39..2c749500 100644 --- a/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift @@ -14,6 +14,8 @@ extension CompressorSettings { guard let function = expr.functionCall else { return settings } for arg in function.arguments { switch arg.label?.text { + case "contentLengthThreshold": + settings.contentLengthThreshold = Int(arg.expression.integerLiteral!.literal.text) ?? 0 case "contentTypePrefixWhitelist": settings.contentTypePrefixWhitelist = arg.expression.stringLiteralString(context: context) case "contentTypeWhitelist": diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index c5922dd3..d6dd76dc 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -130,6 +130,9 @@ extension StaticRoute { #if RouterSettings && Compression if body != nil, let contentType, routerStorage.settings.compression.isEnabled { for (algorithm, algorithmSettings) in routerStorage.settings.compression.supportedCompressionAlgorithms { + if let contentLengthThreshold = algorithmSettings.contentLengthThreshold, body!.count < contentLengthThreshold { + continue + } if let prefixBlacklist = algorithmSettings.contentTypePrefixBlacklist, contentType.hasPrefix(prefixBlacklist) { continue } From 98ef2b1e5432e42694cee277c4cb5932a33e0035 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 9 Apr 2026 13:07:38 -0500 Subject: [PATCH 08/12] static route compression logic fix --- Sources/DestinyMacros/util/StaticRoute+Response.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index d6dd76dc..16e802ee 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -130,9 +130,6 @@ extension StaticRoute { #if RouterSettings && Compression if body != nil, let contentType, routerStorage.settings.compression.isEnabled { for (algorithm, algorithmSettings) in routerStorage.settings.compression.supportedCompressionAlgorithms { - if let contentLengthThreshold = algorithmSettings.contentLengthThreshold, body!.count < contentLengthThreshold { - continue - } if let prefixBlacklist = algorithmSettings.contentTypePrefixBlacklist, contentType.hasPrefix(prefixBlacklist) { continue } @@ -142,7 +139,12 @@ extension StaticRoute { if let prefixWhitelist = algorithmSettings.contentTypePrefixWhitelist, !contentType.hasPrefix(prefixWhitelist) { continue } - guard !algorithmSettings.contentTypeWhitelist.contains(contentType) else { continue } + guard algorithmSettings.contentTypeWhitelist.isEmpty || algorithmSettings.contentTypeWhitelist.contains(contentType) else { + continue + } + if let contentLengthThreshold = algorithmSettings.contentLengthThreshold, body!.count < contentLengthThreshold { + continue + } guard let technique = algorithm.technique else { continue } // TODO: support embedded /*if let compressed = technique.compress(span: body!.value.utf8Span.span, configuration: .default) { if routerStorage.settings.compression.compressOnlyIfResultIsSmaller, compressed.count >= body!.count { From dced6f0d16ecc1ade02206e2251bcdf9ebff994e Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 9 Apr 2026 15:11:18 -0500 Subject: [PATCH 09/12] avoid leaking unused `SwiftCompression` symbols into the binary --- Package.swift | 4 ++-- .../extensions/SwiftCompressionExtensions.swift | 2 +- Sources/Destiny/util/CompressionSettings.swift | 17 ++++++++++++----- .../parse/CompressionAlgorithm+Parse.swift | 2 +- .../parse/CompressionSettings+Parse.swift | 2 +- .../util/StaticRoute+Response.swift | 2 +- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index fe19a206..f3dc9d10 100644 --- a/Package.swift +++ b/Package.swift @@ -413,7 +413,7 @@ var targets = [ .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), - .product(name: "SwiftCompression", package: "swift-compression"), + .product(name: "SwiftCompressionUtilities", package: "swift-compression") ] ), @@ -445,7 +445,7 @@ var targets = [ .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "Zlib", package: "swift-compression") + .product(name: "SwiftCompression", package: "swift-compression") ] ), diff --git a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift index d0e709d3..ee415ae3 100644 --- a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift +++ b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift @@ -1,7 +1,7 @@ #if Compression -import SwiftCompression +import SwiftCompressionUtilities extension CompressionAlgorithm { public var acceptEncodingName: String { diff --git a/Sources/Destiny/util/CompressionSettings.swift b/Sources/Destiny/util/CompressionSettings.swift index f015f81a..fa61fc2b 100644 --- a/Sources/Destiny/util/CompressionSettings.swift +++ b/Sources/Destiny/util/CompressionSettings.swift @@ -1,9 +1,7 @@ #if Compression -import BrotliShim -import SwiftCompression -import ZlibShim +import SwiftCompressionUtilities public struct CompressionSettings: Sendable { package var flags:Flags.RawValue @@ -13,8 +11,17 @@ public struct CompressionSettings: Sendable { enabled: Bool = true, compressOnlyIfResultIsSmaller: Bool = true, supportedCompressionAlgorithms: [CompressionAlgorithm:CompressorSettings] = [ - .brotli(quality: BROTLI_DEFAULT_QUALITY, windowSize: BROTLI_DEFAULT_WINDOW, mode: BROTLI_MODE_GENERIC.rawValue): .init(contentTypePrefixWhitelist: "text/"), - .gzip(bufferSize: 1024, level: Z_DEFAULT_COMPRESSION, memLevel: 8, strategy: Z_DEFAULT_STRATEGY): .init(contentTypePrefixWhitelist: "text/") + .brotli( + quality: 11, // BROTLI_DEFAULT_QUALITY + windowSize: 22, // BROTLI_DEFAULT_WINDOW + mode: 0 // BROTLI_MODE_GENERIC + ): .init(contentTypePrefixWhitelist: "text/"), + .gzip( + bufferSize: 1024, + level: -1, // Z_DEFAULT_COMPRESSION + memLevel: 8, + strategy: 0 // Z_DEFAULT_STRATEGY + ): .init(contentTypePrefixWhitelist: "text/") ] ) { flags = Flags.pack( diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift index ea9a1639..77c52ea0 100644 --- a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -2,7 +2,7 @@ #if Compression import BrotliShim -import SwiftCompression +import SwiftCompressionUtilities import SwiftSyntax import ZlibShim diff --git a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift index 7072e60a..9a5a9512 100644 --- a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift @@ -2,7 +2,7 @@ #if Compression import Destiny -import SwiftCompression +import SwiftCompressionUtilities import SwiftSyntax import SwiftSyntaxMacros diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index 16e802ee..05be9ef9 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -1,9 +1,9 @@ import Destiny +import SwiftCompression import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -import Zlib extension StaticRoute { /// Builds the HTTP Message for this route. From 3594639a4755f2d83d49ab9f2f6897d840ebd4c0 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 9 Apr 2026 20:07:19 -0500 Subject: [PATCH 10/12] minor fixes --- Embedded/Package.swift | 13 ++++++++++++- Package.swift | 6 +++--- ...iateResponseBody+ResponderDebugDescription.swift | 4 ++-- .../DestinyMacros/util/StaticRoute+Response.swift | 5 ++++- Sources/TestRouter/TestRouter.swift | 1 + 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Embedded/Package.swift b/Embedded/Package.swift index 33f3de62..617d4945 100644 --- a/Embedded/Package.swift +++ b/Embedded/Package.swift @@ -23,6 +23,11 @@ var pkgDependencies:[Package.Dependency] = [ // Metrics //.package(url: "https://github.com/apple/swift-metrics", from: "2.5.1"), + .package( + url: "https://github.com/RandomHashTags/swift-compression", + branch: "refactor" + ), + // Unlock more performance .package( url: "https://github.com/RandomHashTags/swift-unwrap-arithmetic-operators", @@ -338,6 +343,10 @@ let traits:Set = [ description: "Enables the design protocols." ), + .trait( + name: "Compression", + description: "Enables compression support (using swift-compression)." + ), .trait( name: "Epoll", description: "Enables Epoll functionality (Linux only)." @@ -380,6 +389,7 @@ let package = Package( .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), + .product(name: "SwiftCompressionUtilities", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), @@ -410,7 +420,8 @@ let package = Package( .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax") + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompression", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), diff --git a/Package.swift b/Package.swift index f3dc9d10..2c270e7f 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ var pkgDependencies:[Package.Dependency] = [ // Media types .package( url: "https://github.com/RandomHashTags/swift-media-types", - from: "0.1.0", + exact: "0.1.0", traits: ["MediaTypes", "RawValues", "FileExtensionInits", "MediaTypeParsable"] ), @@ -413,7 +413,7 @@ var targets = [ .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), - .product(name: "SwiftCompressionUtilities", package: "swift-compression") + .product(name: "SwiftCompressionUtilities", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), @@ -445,7 +445,7 @@ var targets = [ .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompression", package: "swift-compression") + .product(name: "SwiftCompression", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift index eb259099..1f4a1a37 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift @@ -166,8 +166,8 @@ extension IntermediateResponseBody { "F" ] private static func byteToHex(_ byte: UInt8) -> (high: Character, low: Character) { - let high = PercentEncoding.hexDigits[unchecked: Int(byte >> 4)] - let low = PercentEncoding.hexDigits[unchecked: Int(byte & 0x0F)] + let high = hexDigits[unchecked: Int(byte >> 4)] + let low = hexDigits[unchecked: Int(byte & 0x0F)] return (high, low) } } \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index 05be9ef9..c606db94 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -1,10 +1,13 @@ import Destiny -import SwiftCompression import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros +#if Compression +import SwiftCompression +#endif + extension StaticRoute { /// Builds the HTTP Message for this route. /// diff --git a/Sources/TestRouter/TestRouter.swift b/Sources/TestRouter/TestRouter.swift index bbd36069..50efcd14 100644 --- a/Sources/TestRouter/TestRouter.swift +++ b/Sources/TestRouter/TestRouter.swift @@ -35,6 +35,7 @@ package final class TestRouter { routerSettings: .init( //dynamicResponsesAreGeneric: false, //protocolConformances: false, + logging: true, visibility: .package ), From 5ce43a9e2da19753cffd2ba86ac8fd79a62dc49e Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Mon, 3 Aug 2026 19:43:20 -0500 Subject: [PATCH 11/12] enable compression for static routes --- .../CompressionAlgorithm+Compress.swift | 21 +++++++++++++++++++ .../util/StaticRoute+Response.swift | 5 ++--- 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift diff --git a/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift new file mode 100644 index 00000000..8943b53b --- /dev/null +++ b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift @@ -0,0 +1,21 @@ + +#if Compression + +import SwiftCompression + +extension CompressionAlgorithm { + func compress(span: Span) -> [UInt8]? { + switch self { + case .brotli(let quality, let windowSize, let mode): + return Brotli(quality: quality, windowSize: windowSize, mode: mode) + .compress(span: span, configuration: .default) + case .gzip(let bufferSize, let level, let memLevel, let strategy): + return Gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) + .compress(span: span, configuration: .default) + default: + return nil + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift index c606db94..f375347d 100644 --- a/Sources/DestinyMacros/util/StaticRoute+Response.swift +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -148,8 +148,7 @@ extension StaticRoute { if let contentLengthThreshold = algorithmSettings.contentLengthThreshold, body!.count < contentLengthThreshold { continue } - guard let technique = algorithm.technique else { continue } // TODO: support embedded - /*if let compressed = technique.compress(span: body!.value.utf8Span.span, configuration: .default) { + if let compressed = algorithm.compress(span: body!.value.utf8Span.span) { if routerStorage.settings.compression.compressOnlyIfResultIsSmaller, compressed.count >= body!.count { continue } @@ -160,7 +159,7 @@ extension StaticRoute { body!.type = .string(isNonCopyable: isNonCopyable, isStatic: isStatic, withDateHeader: withDateHeader, withCompressedBody: true) } break - }*/ + } } } #endif From 928b3551230eafc007facbf39077c27d4d29e4e8 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Mon, 3 Aug 2026 21:54:11 -0500 Subject: [PATCH 12/12] fixes --- .../CompressionAlgorithm+Compress.swift | 45 ++++++++++++++++++- .../parse/CompressionAlgorithm+Parse.swift | 20 ++++++--- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift index 8943b53b..06fc8190 100644 --- a/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift +++ b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift @@ -6,12 +6,53 @@ import SwiftCompression extension CompressionAlgorithm { func compress(span: Span) -> [UInt8]? { switch self { + case .brotli(let quality, let windowSize, let mode): + #if canImport(Brotli) return Brotli(quality: quality, windowSize: windowSize, mode: mode) - .compress(span: span, configuration: .default) + .compress(span, configuration: .default) + #else + return nil + #endif + + case .deflate(let bufferSize, let level): + #if canImport(Zlib) + return Deflate(bufferSize: bufferSize, level: level).compress(span, configuration: .default) + #else + return nil + #endif + + case .lz77(let searchBufferSize, let lookaheadBufferSize, let offsetBitWidth): + #if canImport(CompressionLZ) + switch offsetBitWidth { + case 8: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 16: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 32: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 64: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 128: + if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + } + return nil + default: + return nil + } + #else + return nil + #endif + case .gzip(let bufferSize, let level, let memLevel, let strategy): + #if canImport(Zlib) return Gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) - .compress(span: span, configuration: .default) + .compress(span, configuration: .default) + #else + return nil + #endif + default: return nil } diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift index 77c52ea0..82e6f03d 100644 --- a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -31,7 +31,9 @@ extension CompressionAlgorithm { case "json": self = .json case "lz4": self = .lz4*/ case "brotli": - var quality:Int32 = BROTLI_DEFAULT_QUALITY, windowSize:Int32 = BROTLI_DEFAULT_WINDOW, mode:UInt32 = BROTLI_MODE_GENERIC.rawValue + var quality:Int32 = BROTLI_DEFAULT_QUALITY + var windowSize:Int32 = BROTLI_DEFAULT_WINDOW + var mode:UInt32 = BROTLI_MODE_GENERIC.rawValue for child in arguments { switch child.label?.text { case "quality": quality = Int32(child.expression.integerLiteral!.literal.text) ?? 0 @@ -42,7 +44,9 @@ extension CompressionAlgorithm { } return .brotli(quality: quality, windowSize: windowSize, mode: mode) case "lz77": - var windowSize = 0, bufferSize = 0, offsetBitWidth = 0 + var windowSize = 0 + var bufferSize = 0 + var offsetBitWidth = 0 for child in arguments { switch child.label?.text { case "windowSize": windowSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 @@ -57,7 +61,10 @@ extension CompressionAlgorithm { case "mtf": self = .mtf*/ case "gzip": - var bufferSize = 1024, level = Z_DEFAULT_COMPRESSION, memLevel:Int32 = 8, strategy = Z_DEFAULT_STRATEGY + var bufferSize = 1024 + var level = Z_DEFAULT_COMPRESSION + var memLevel:Int32 = 8 + var strategy = Z_DEFAULT_STRATEGY for child in arguments { switch child.label?.text { case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 @@ -71,7 +78,8 @@ extension CompressionAlgorithm { return .gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) case "runLengthEncoding": - var minRun = 0, alwaysIncludeRunCount:Bool = false + var minRun = 0 + var alwaysIncludeRunCount:Bool = false for child in arguments { switch child.label?.text { case "minRun": minRun = Int(child.expression.integerLiteral!.literal.text) ?? 0 @@ -100,12 +108,12 @@ extension CompressionAlgorithm { case "fibonacci": self = .fibonacci*/ case "dnaBinaryEncoding": - var baseBits:[UInt8:[Bool]] = [:] + var baseBits:[UInt8:UInt8] = [:] for child in arguments { switch child.label?.text { case "baseBits": child.expression.dictionary?.content.as(DictionaryElementListSyntax.self)!.forEach({ - baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = $0.value.array!.elements.map({ $0.expression.booleanIsTrue }) + baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = UInt8($0.value.integerLiteral!.literal.text) }) default: break }