Skip to content

Commit 48ed9c7

Browse files
committed
Add RegistryManager
Add RegistryManagerTests
1 parent 965c7fe commit 48ed9c7

File tree

5 files changed

+528
-52
lines changed

5 files changed

+528
-52
lines changed

Sources/PackageRegistry/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
add_library(PackageRegistry
1010
Registry.swift
11-
RegistryConfiguration.swift)
11+
RegistryConfiguration.swift
12+
RegistryManager.swift)
1213
target_link_libraries(PackageRegistry PUBLIC
1314
TSCBasic
1415
PackageLoading
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Basics
12+
import PackageLoading
13+
import PackageModel
14+
15+
import TSCBasic
16+
import TSCUtility
17+
18+
import struct Foundation.URL
19+
import struct Foundation.URLComponents
20+
import struct Foundation.URLQueryItem
21+
22+
import Dispatch
23+
24+
public enum RegistryError: Error {
25+
case registryNotConfigured(scope: String)
26+
case invalidPackage(PackageReference)
27+
case invalidOperation
28+
case invalidResponse
29+
case invalidURL
30+
case invalidChecksum(expected: String, actual: String)
31+
}
32+
33+
public final class RegistryManager {
34+
internal static var archiverFactory: (FileSystem) -> Archiver = { fileSystem in
35+
return ZipArchiver(fileSystem: fileSystem)
36+
}
37+
38+
private static let sharedClient: HTTPClientProtocol = HTTPClient()
39+
40+
var configuration: RegistryConfiguration
41+
var client: HTTPClientProtocol
42+
var identityResolver: IdentityResolver
43+
var authorizationProvider: HTTPClientAuthorizationProvider?
44+
var diagnosticEngine: DiagnosticsEngine?
45+
46+
public init(configuration: RegistryConfiguration,
47+
identityResolver: IdentityResolver,
48+
authorizationProvider: HTTPClientAuthorizationProvider? = nil,
49+
diagnosticEngine: DiagnosticsEngine? = nil)
50+
{
51+
self.configuration = configuration
52+
self.client = Self.sharedClient
53+
self.identityResolver = identityResolver
54+
self.authorizationProvider = authorizationProvider
55+
self.diagnosticEngine = diagnosticEngine
56+
}
57+
58+
public func fetchVersions(
59+
of package: PackageReference,
60+
on queue: DispatchQueue,
61+
completion: @escaping (Result<[Version], Error>) -> Void
62+
) {
63+
guard case let (scope, name)? = package.identity.scopeAndName else {
64+
return completion(.failure(RegistryError.invalidPackage(package)))
65+
}
66+
67+
guard let registry = configuration.registry(for: scope) else {
68+
return completion(.failure(RegistryError.registryNotConfigured(scope: scope)))
69+
}
70+
71+
var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true)
72+
components?.appendPathComponents(scope, name)
73+
74+
guard let url = components?.url else {
75+
return completion(.failure(RegistryError.invalidURL))
76+
}
77+
78+
var request = HTTPClient.Request(
79+
method: .get,
80+
url: url,
81+
headers: [
82+
"Accept": "application/vnd.swift.registry.v1+json"
83+
]
84+
)
85+
86+
request.options.authorizationProvider = authorizationProvider
87+
88+
client.execute(request, progress: nil) { result in
89+
completion(result.tryMap { response in
90+
if response.statusCode == 200,
91+
response.headers.get("Content-Version").first == "1",
92+
response.headers.get("Content-Type").first?.hasPrefix("application/json") == true,
93+
let data = response.body,
94+
case .dictionary(let payload) = try? JSON(data: data),
95+
case .dictionary(let releases) = payload["releases"]
96+
{
97+
let versions = releases.filter { (try? $0.value.getJSON("problem")) == nil }
98+
.compactMap { Version(string: $0.key) }
99+
.sorted(by: >)
100+
return versions
101+
} else {
102+
throw RegistryError.invalidResponse
103+
}
104+
})
105+
}
106+
}
107+
108+
public func fetchManifest(
109+
for version: Version,
110+
of package: PackageReference,
111+
using manifestLoader: ManifestLoaderProtocol,
112+
toolsVersion: ToolsVersion = .currentToolsVersion,
113+
swiftLanguageVersion: SwiftLanguageVersion? = nil,
114+
on queue: DispatchQueue,
115+
completion: @escaping (Result<Manifest, Error>) -> Void
116+
) {
117+
guard case let (scope, name)? = package.identity.scopeAndName else {
118+
return completion(.failure(RegistryError.invalidPackage(package)))
119+
}
120+
121+
guard let registry = configuration.registry(for: scope) else {
122+
return completion(.failure(RegistryError.registryNotConfigured(scope: scope)))
123+
}
124+
125+
var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true)
126+
components?.appendPathComponents(scope, name, "\(version)", "Package.swift")
127+
if let swiftLanguageVersion = swiftLanguageVersion {
128+
components?.queryItems = [
129+
URLQueryItem(name: "swift-version", value: swiftLanguageVersion.rawValue)
130+
]
131+
}
132+
133+
guard let url = components?.url else {
134+
return completion(.failure(RegistryError.invalidURL))
135+
}
136+
137+
var request = HTTPClient.Request(
138+
method: .get,
139+
url: url,
140+
headers: [
141+
"Accept": "application/vnd.swift.registry.v1+swift"
142+
]
143+
)
144+
145+
request.options.authorizationProvider = authorizationProvider
146+
147+
client.execute(request, progress: nil) { result in
148+
do {
149+
if case .failure(let error) = result {
150+
throw error
151+
} else if case .success(let response) = result,
152+
response.statusCode == 200,
153+
response.headers.get("Content-Version").first == "1",
154+
response.headers.get("Content-Type").first?.hasPrefix("text/x-swift") == true,
155+
let data = response.body
156+
{
157+
let fileSystem = InMemoryFileSystem()
158+
159+
let filename: String
160+
if let swiftLanguageVersion = swiftLanguageVersion {
161+
filename = Manifest.basename + "@swift-\(swiftLanguageVersion).swift"
162+
} else {
163+
filename = Manifest.basename + ".swift"
164+
}
165+
166+
try fileSystem.writeFileContents(.root.appending(component: filename), bytes: ByteString(data))
167+
manifestLoader.load(
168+
at: .root,
169+
packageIdentity: package.identity,
170+
packageKind: .remote,
171+
packageLocation: package.location,
172+
version: version,
173+
revision: nil,
174+
toolsVersion: .currentToolsVersion,
175+
identityResolver: self.identityResolver,
176+
fileSystem: fileSystem,
177+
diagnostics: self.diagnosticEngine,
178+
on: .sharedConcurrent,
179+
completion: completion
180+
)
181+
} else {
182+
throw RegistryError.invalidResponse
183+
}
184+
} catch {
185+
queue.async {
186+
completion(.failure(error))
187+
}
188+
}
189+
}
190+
}
191+
192+
public func downloadSourceArchive(
193+
for version: Version,
194+
of package: PackageReference,
195+
into fileSystem: FileSystem,
196+
at destinationPath: AbsolutePath,
197+
expectedChecksum: ByteString? = nil,
198+
on queue: DispatchQueue,
199+
completion: @escaping (Result<Void, Error>) -> Void
200+
) {
201+
guard case let (scope, name)? = package.identity.scopeAndName else {
202+
return completion(.failure(RegistryError.invalidPackage(package)))
203+
}
204+
205+
guard let registry = configuration.registry(for: scope) else {
206+
return completion(.failure(RegistryError.registryNotConfigured(scope: scope)))
207+
}
208+
209+
var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true)
210+
components?.appendPathComponents(scope, name, "\(version).zip")
211+
212+
guard let url = components?.url else {
213+
return completion(.failure(RegistryError.invalidURL))
214+
}
215+
216+
var request = HTTPClient.Request(
217+
method: .get,
218+
url: url,
219+
headers: [
220+
"Accept": "application/vnd.swift.registry.v1+zip"
221+
]
222+
)
223+
224+
request.options.authorizationProvider = authorizationProvider
225+
226+
client.execute(request, progress: nil) { result in
227+
switch result {
228+
case .success(let response):
229+
if response.statusCode == 200,
230+
response.headers.get("Content-Version").first == "1",
231+
response.headers.get("Content-Type").first?.hasPrefix("application/zip") == true,
232+
let digest = response.headers.get("Digest").first,
233+
let data = response.body
234+
{
235+
do {
236+
let contents = ByteString(data)
237+
let advertisedChecksum = digest.spm_dropPrefix("sha-256=")
238+
let actualChecksum = SHA256().hash(contents).hexadecimalRepresentation
239+
240+
guard (expectedChecksum?.hexadecimalRepresentation ?? actualChecksum) == actualChecksum,
241+
advertisedChecksum == actualChecksum
242+
else {
243+
throw RegistryError.invalidChecksum(
244+
expected: expectedChecksum?.hexadecimalRepresentation ?? advertisedChecksum,
245+
actual: actualChecksum
246+
)
247+
}
248+
249+
let archivePath = destinationPath.withExtension("zip")
250+
try fileSystem.writeFileContents(archivePath, bytes: contents)
251+
252+
try fileSystem.createDirectory(destinationPath, recursive: true)
253+
254+
let archiver = Self.archiverFactory(fileSystem)
255+
// TODO: Bail if archive contains relative paths or overlapping files
256+
archiver.extract(from: archivePath, to: destinationPath) { result in
257+
completion(result)
258+
try? fileSystem.removeFileTree(archivePath)
259+
}
260+
} catch {
261+
try? fileSystem.removeFileTree(destinationPath)
262+
completion(.failure(error))
263+
}
264+
} else {
265+
completion(.failure(RegistryError.invalidResponse))
266+
}
267+
case .failure(let error):
268+
completion(.failure(error))
269+
}
270+
}
271+
}
272+
}
273+
274+
private extension String {
275+
/// Drops the given suffix from the string, if present.
276+
func spm_dropPrefix(_ prefix: String) -> String {
277+
if hasPrefix(prefix) {
278+
return String(dropFirst(prefix.count))
279+
}
280+
return self
281+
}
282+
}
283+
284+
private extension AbsolutePath {
285+
func withExtension(_ extension: String) -> AbsolutePath {
286+
guard !self.isRoot else { return self }
287+
let `extension` = `extension`.spm_dropPrefix(".")
288+
return AbsolutePath(self, RelativePath("..")).appending(component: "\(basename).\(`extension`)")
289+
}
290+
}
291+
292+
private extension URLComponents {
293+
mutating func appendPathComponents(_ components: String...) {
294+
path += (path.last == "/" ? "" : "/") + components.joined(separator: "/")
295+
}
296+
}

Sources/SPMTestSupport/misc.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,5 @@ public func loadPackageGraph(
247247
createREPLProduct: createREPLProduct
248248
)
249249
}
250+
251+
public let emptyZipFile = ByteString([0x80, 0x75, 0x05, 0x06] + [UInt8](repeating: 0x00, count: 18))

0 commit comments

Comments
 (0)