diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 9d9ddb63..69c3edd3 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -45,7 +45,6 @@ public struct ContainerManager: Sendable { @available(macOS 26.0, *) public struct VmnetNetwork: Network { private var allocator: Allocator - // `reference` isn't used concurrently. nonisolated(unsafe) private let reference: vmnet_network_ref /// The IPv4 subnet of this network. @@ -95,7 +94,6 @@ public struct ContainerManager: Sendable { public let gateway: String? public let macAddress: String? - // `reference` isn't used concurrently. nonisolated(unsafe) private let reference: vmnet_network_ref public init( @@ -480,4 +478,9 @@ extension CIDRAddress { } } +@available(macOS 26.0, *) +private struct SendableReference: Sendable { + nonisolated(unsafe) private let reference: vmnet_network_ref +} + #endif diff --git a/Sources/Containerization/Image/ImageStore/ImageStore+Export.swift b/Sources/Containerization/Image/ImageStore/ImageStore+Export.swift index 92e520c9..43eff952 100644 --- a/Sources/Containerization/Image/ImageStore/ImageStore+Export.swift +++ b/Sources/Containerization/Image/ImageStore/ImageStore+Export.swift @@ -24,14 +24,14 @@ import Crypto import Foundation extension ImageStore { - public struct ExportOperation: Sendable { + internal struct ExportOperation { let name: String let tag: String let contentStore: ContentStore let client: ContentClient let progress: ProgressHandler? - public init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) { + init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) { self.contentStore = contentStore self.client = client self.progress = progress @@ -40,7 +40,7 @@ extension ImageStore { } @discardableResult - public func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor { + internal func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor { var pushQueue: [[Descriptor]] = [] var current: [Descriptor] = [index] while !current.isEmpty { diff --git a/Sources/Containerization/Image/ImageStore/ImageStore+Import.swift b/Sources/Containerization/Image/ImageStore/ImageStore+Import.swift index 1adfedf5..7dc4eb4a 100644 --- a/Sources/Containerization/Image/ImageStore/ImageStore+Import.swift +++ b/Sources/Containerization/Image/ImageStore/ImageStore+Import.swift @@ -22,7 +22,7 @@ import ContainerizationOCI import Foundation extension ImageStore { - public struct ImportOperation: Sendable { + internal struct ImportOperation { static let decoder = JSONDecoder() let client: ContentClient @@ -31,7 +31,7 @@ extension ImageStore { let progress: ProgressHandler? let name: String - public init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) { + init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) { self.client = client self.ingestDir = ingestDir self.contentStore = contentStore @@ -40,7 +40,7 @@ extension ImageStore { } /// Pull the required image layers for the provided descriptor and platform(s) into the given directory using the provided client. Returns a descriptor to the Index manifest. - public func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor { + internal func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor { var toProcess = [root] while !toProcess.isEmpty { // Count the total number of blobs and their size @@ -123,14 +123,14 @@ extension ImageStore { for _ in 0..<8 { if let desc = iterator.next() { group.addTask { - try await self.fetch(desc) + try await fetch(desc) } } } for try await _ in group { if let desc = iterator.next() { group.addTask { - try await self.fetch(desc) + try await fetch(desc) } } } diff --git a/Sources/Containerization/NATNetworkInterface.swift b/Sources/Containerization/NATNetworkInterface.swift index 845fc8f3..825d3cc0 100644 --- a/Sources/Containerization/NATNetworkInterface.swift +++ b/Sources/Containerization/NATNetworkInterface.swift @@ -31,7 +31,6 @@ public final class NATNetworkInterface: Interface, Sendable { public let macAddress: String? @available(macOS 26, *) - // `reference` isn't used concurrently. public nonisolated(unsafe) let reference: vmnet_network_ref! @available(macOS 26, *) diff --git a/Sources/Containerization/UnixSocketRelay.swift b/Sources/Containerization/UnixSocketRelay.swift index 0cd75419..d9ae971f 100644 --- a/Sources/Containerization/UnixSocketRelay.swift +++ b/Sources/Containerization/UnixSocketRelay.swift @@ -267,7 +267,6 @@ extension SocketRelay { ) } - // `buf1` isn't used concurrently. nonisolated(unsafe) let buf1 = UnsafeMutableBufferPointer.allocate(capacity: Int(getpagesize())) connSource.setEventHandler { Self.fdCopyHandler( @@ -279,7 +278,6 @@ extension SocketRelay { } nonisolated(unsafe) let buf2 = UnsafeMutableBufferPointer.allocate(capacity: Int(getpagesize())) - // `buf2` isn't used concurrently. vsockConnectionSource.setEventHandler { Self.fdCopyHandler( buffer: buf2, diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index 0046bc18..72155fc3 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -68,7 +68,6 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable { } } - // `vm` isn't used concurrently. private nonisolated(unsafe) let vm: VZVirtualMachine private let queue: DispatchQueue private let group: MultiThreadedEventLoopGroup diff --git a/Sources/ContainerizationExtras/ProxyUtils.swift b/Sources/ContainerizationExtras/ProxyUtils.swift new file mode 100644 index 00000000..ccd9691b --- /dev/null +++ b/Sources/ContainerizationExtras/ProxyUtils.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// A small utility to resolve proxy settings (HTTP(S)_PROXY / NO_PROXY). +public enum ProxyUtils { + /// Resolves the proxy URL for a given host based on environment variables. + /// + /// - Parameters: + /// - host: The target hostname (without scheme). + /// - env: Optional environment dictionary; defaults to process environment. + /// - Returns: The proxy URL to use, or `nil` for direct connection. + public static func proxy(for host: String, env: [String: String]? = nil) -> URL? { + let env = env ?? ProcessInfo.processInfo.environment + + // Case-insensitive lookup for both upper/lower keys + let httpProxy = env["HTTP_PROXY"] ?? env["http_proxy"] + let httpsProxy = env["HTTPS_PROXY"] ?? env["https_proxy"] + + let noProxy = env["NO_PROXY"] ?? env["no_proxy"] + + // If NO_PROXY matches → skip proxy + if let noProxy, shouldBypassProxy(host: host, noProxy: noProxy) { + return nil + } + + // Prefer HTTPS proxy if set, otherwise fall back to HTTP proxy + let proxyStr = httpsProxy ?? httpProxy + guard let proxyStr, let url = URL(string: proxyStr) else { + return nil + } + return url + } + + /// Check if a host should bypass proxy according to NO_PROXY. + /// - Example: NO_PROXY=".example.com,localhost,127.0.0.1" + private static func shouldBypassProxy(host: String, noProxy: String) -> Bool { + let entries = noProxy.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } + for entry in entries { + if entry.isEmpty { continue } + if entry == "*" { return true } + if host == entry { return true } + if entry.hasPrefix(".") && host.hasSuffix(entry) { return true } + } + return false + } +} diff --git a/Sources/ContainerizationOS/AsyncSignalHandler.swift b/Sources/ContainerizationOS/AsyncSignalHandler.swift index 5f2fd328..895a1503 100644 --- a/Sources/ContainerizationOS/AsyncSignalHandler.swift +++ b/Sources/ContainerizationOS/AsyncSignalHandler.swift @@ -53,7 +53,6 @@ public final class AsyncSignalHandler: Sendable { struct State: Sendable { var conts: [AsyncStream.Continuation] = [] - // `sources` isn't used concurrently. nonisolated(unsafe) var sources: [any DispatchSourceSignal] = [] } diff --git a/Sources/Integration/ProcessTests.swift b/Sources/Integration/ProcessTests.swift index f8a46689..7030cbe6 100644 --- a/Sources/Integration/ProcessTests.swift +++ b/Sources/Integration/ProcessTests.swift @@ -62,7 +62,6 @@ extension IntegrationSuite { } final class BufferWriter: Writer { - // `data` isn't used concurrently. nonisolated(unsafe) var data = Data() func write(_ data: Data) throws { diff --git a/Tests/ContainerizationExtrasTests/ProxyUtilsTests.swift b/Tests/ContainerizationExtrasTests/ProxyUtilsTests.swift new file mode 100644 index 00000000..44223450 --- /dev/null +++ b/Tests/ContainerizationExtrasTests/ProxyUtilsTests.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import Testing + +@testable import ContainerizationExtras + +struct ProxyUtilsTests { + + @Test("HTTP proxy resolution") + func testHttpProxy() { + let env = ["http_proxy": "http://proxy.local:8080"] + let proxy = ProxyUtils.proxy(for: "example.com", env: env) + #expect(proxy?.absoluteString == "http://proxy.local:8080") + } + + @Test("HTTPS proxy resolution") + func testHttpsProxy() { + let env = ["https_proxy": "https://secureproxy.local:8443"] + let proxy = ProxyUtils.proxy(for: "secure.com", env: env) + #expect(proxy?.absoluteString == "https://secureproxy.local:8443") + } + + @Test("NO_PROXY exact match") + func testNoProxyExactMatch() { + let env = [ + "http_proxy": "http://proxy.local:8080", + "NO_PROXY": "example.com", + ] + let proxy = ProxyUtils.proxy(for: "example.com", env: env) + #expect(proxy == nil) + } + + @Test("Uppercase HTTP_PROXY is respected") + func testUppercaseHttpProxy() { + let env = ["HTTP_PROXY": "http://upper.local:8081"] + let proxy = ProxyUtils.proxy(for: "upper.com", env: env) + #expect(proxy?.absoluteString == "http://upper.local:8081") + } + + @Test("Lowercase no_proxy is respected") + func testLowercaseNoProxy() { + let env = [ + "http_proxy": "http://proxy.local:8080", + "no_proxy": "lower.com", + ] + let proxy = ProxyUtils.proxy(for: "lower.com", env: env) + #expect(proxy == nil) + } + + @Test("HTTPS proxy has higher priority than HTTP proxy") + func testHttpsPreferredOverHttp() { + let env = [ + "http_proxy": "http://proxy.local:8080", + "https_proxy": "https://secureproxy.local:8443", + ] + let proxy = ProxyUtils.proxy(for: "secure.com", env: env) + #expect(proxy?.absoluteString == "https://secureproxy.local:8443") + } + + @Test("Uppercase HTTP_PROXY overrides lowercase http_proxy") + func testUppercaseOverridesLowercaseHttp() { + let env = [ + "http_proxy": "http://lower.local:8080", + "HTTP_PROXY": "http://upper.local:8081", + ] + let proxy = ProxyUtils.proxy(for: "example.com", env: env) + #expect(proxy?.absoluteString == "http://upper.local:8081") + } + + @Test("Uppercase HTTPS_PROXY overrides lowercase https_proxy") + func testUppercaseOverridesLowercaseHttps() { + let env = [ + "https_proxy": "https://lower.local:8443", + "HTTPS_PROXY": "https://upper.local:8444", + ] + let proxy = ProxyUtils.proxy(for: "secure.com", env: env) + #expect(proxy?.absoluteString == "https://upper.local:8444") + } + + @Test("Uppercase NO_PROXY overrides lowercase no_proxy") + func testUppercaseOverridesLowercaseNoProxy() { + let env = [ + "http_proxy": "http://proxy.local:8080", + "no_proxy": "foo.com", + "NO_PROXY": "bar.com", + ] + let proxyFoo = ProxyUtils.proxy(for: "foo.com", env: env) + let proxyBar = ProxyUtils.proxy(for: "bar.com", env: env) + + #expect(proxyFoo != nil) + #expect(proxyBar == nil) + } +}