@@ -13,14 +13,30 @@ import Foundation
13
13
import TSCBasic
14
14
import TSCUtility
15
15
16
+ //@warn_unqualified_access
17
+ /// When set to `false`,
18
+ /// `PackageIdentity` uses the canonical location of package dependencies as its identity.
19
+ /// Otherwise, only the last path component is used to identify package dependencies.
20
+ public var _useLegacyIdentities : Bool = true {
21
+ willSet {
22
+ PackageIdentity . provider = newValue ? LegacyPackageIdentity . self : CanonicalPackageIdentity . self
23
+ }
24
+ }
25
+
26
+ internal protocol PackageIdentityProvider : LosslessStringConvertible {
27
+ init ( _ string: String )
28
+ }
29
+
16
30
/// The canonical identifier for a package, based on its source location.
17
31
public struct PackageIdentity : LosslessStringConvertible , Hashable {
32
+ internal static var provider : PackageIdentityProvider . Type = LegacyPackageIdentity . self
33
+
18
34
/// A textual representation of this instance.
19
35
public let description : String
20
36
21
37
/// Instantiates an instance of the conforming type from a string representation.
22
- public init ( _ url : String ) {
23
- self . description = PackageReference . computeDefaultName ( fromURL : url ) . lowercased ( )
38
+ public init ( _ string : String ) {
39
+ self . description = Self . provider . init ( string ) . description
24
40
}
25
41
}
26
42
@@ -56,3 +72,259 @@ extension PackageIdentity: JSONMappable, JSONSerializable {
56
72
return . string( self . description)
57
73
}
58
74
}
75
+
76
+ // MARK: -
77
+
78
+ struct LegacyPackageIdentity : PackageIdentityProvider , Equatable {
79
+ /// A textual representation of this instance.
80
+ public let description : String
81
+
82
+ /// Instantiates an instance of the conforming type from a string representation.
83
+ public init ( _ string: String ) {
84
+ self . description = PackageReference . computeDefaultName ( fromURL: string) . lowercased ( )
85
+ }
86
+ }
87
+
88
+ /// A canonicalized package identity.
89
+ ///
90
+ /// A package may declare external packages as dependencies in its manifest.
91
+ /// Each external package is uniquely identified by the location of its source code.
92
+ ///
93
+ /// An external package dependency may itself have one or more external package dependencies,
94
+ /// known as _transitive dependencies_.
95
+ /// When multiple packages have dependencies in common,
96
+ /// Swift Package Manager determines which version of that package should be used
97
+ /// (if any exist that satisfy all specified requirements)
98
+ /// in a process called package resolution.
99
+ ///
100
+ /// External package dependencies are located by a URL
101
+ /// (which may be an implicit `file://` URL in the form of a file path).
102
+ /// For the purposes of package resolution,
103
+ /// package URLs are case-insensitive (mona ≍ MONA)
104
+ /// and normalization-insensitive (n + ◌̃ ≍ ñ).
105
+ /// Swift Package Manager takes additional steps to canonicalize URLs
106
+ /// to resolve insignificant differences between URLs.
107
+ /// For example,
108
+ /// the URLs `https://example.com/Mona/LinkedList` and `
[email protected] :mona/linkedlist`
109
+ /// are equivalent, in that they both resolve to the same source code repository,
110
+ /// despite having different scheme, authority, and path components.
111
+ ///
112
+ /// The `PackageIdentity` type canonicalizes package locations by
113
+ /// performing the following operations:
114
+ ///
115
+ /// * Removing the scheme component, if present
116
+ /// ```
117
+ /// https://example.com/mona/LinkedList → example.com/mona/LinkedList
118
+ /// ```
119
+ /// * Removing the userinfo component (preceded by `@`), if present:
120
+ /// ```
121
+ ///
[email protected] /mona/LinkedList → example.com/mona/LinkedList
122
+ /// ```
123
+ /// * Removing the port subcomponent, if present:
124
+ /// ```
125
+ /// example.com:443/mona/LinkedList → example.com/mona/LinkedList
126
+ /// ```
127
+ /// * Replacing the colon (`:`) preceding the path component in "`scp`-style" URLs:
128
+ /// ```
129
+ ///
[email protected] :mona/LinkedList.git → example.com/mona/LinkedList
130
+ /// ```
131
+ /// * Expanding the tilde (`~`) to the provided user, if applicable:
132
+ /// ```
133
+ /// ssh://
[email protected] /~/LinkedList.git → example.com/~mona/LinkedList
134
+ /// ```
135
+ /// * Removing percent-encoding from the path component, if applicable:
136
+ /// ```
137
+ /// example.com/mona/%F0%9F%94%97List → example.com/mona/🔗List
138
+ /// ```
139
+ /// * Removing the `.git` file extension from the path component, if present:
140
+ /// ```
141
+ /// example.com/mona/LinkedList.git → example.com/mona/LinkedList
142
+ /// ```
143
+ /// * Removing the trailing slash (`/`) in the path component, if present:
144
+ /// ```
145
+ /// example.com/mona/LinkedList/ → example.com/mona/LinkedList
146
+ /// ```
147
+ /// * Removing the fragment component (preceded by `#`), if present:
148
+ /// ```
149
+ /// example.com/mona/LinkedList#installation → example.com/mona/LinkedList
150
+ /// ```
151
+ /// * Removing the query component (preceded by `?`), if present:
152
+ /// ```
153
+ /// example.com/mona/LinkedList?utm_source=forums.swift.org → example.com/mona/LinkedList
154
+ /// ```
155
+ /// * Adding a leading slash (`/`) for `file://` URLs and absolute file paths:
156
+ /// ```
157
+ /// file:///Users/mona/LinkedList → /Users/mona/LinkedList
158
+ /// ```
159
+ struct CanonicalPackageIdentity : PackageIdentityProvider , Equatable {
160
+ /// A textual representation of this instance.
161
+ public let description : String
162
+
163
+ /// Instantiates an instance of the conforming type from a string representation.
164
+ public init ( _ string: String ) {
165
+ var description = string. precomposedStringWithCanonicalMapping. lowercased ( )
166
+
167
+ // Remove the scheme component, if present.
168
+ let detectedScheme = description. dropSchemeComponentPrefixIfPresent ( )
169
+
170
+ // Remove the userinfo subcomponent (user / password), if present.
171
+ if case ( let user, _) ? = description. dropUserinfoSubcomponentPrefixIfPresent ( ) {
172
+ // If a user was provided, perform tilde expansion, if applicable.
173
+ description. replaceFirstOccurenceIfPresent ( of: " /~/ " , with: " /~ \( user) / " )
174
+ }
175
+
176
+ // Remove the port subcomponent, if present.
177
+ description. removePortComponentIfPresent ( )
178
+
179
+ // Remove the fragment component, if present.
180
+ description. removeFragmentComponentIfPresent ( )
181
+
182
+ // Remove the query component, if present.
183
+ description. removeQueryComponentIfPresent ( )
184
+
185
+ // Accomodate "`scp`-style" SSH URLs
186
+ if detectedScheme == nil || detectedScheme == " ssh " {
187
+ description. replaceFirstOccurenceIfPresent ( of: " : " , before: description. firstIndex ( of: " / " ) , with: " / " )
188
+ }
189
+
190
+ // Split the remaining string into path components,
191
+ // filtering out empty path components and removing valid percent encodings.
192
+ var components = description. split ( omittingEmptySubsequences: true , whereSeparator: isSeparator)
193
+ . compactMap { $0. removingPercentEncoding ?? String ( $0) }
194
+
195
+ // Remove the `.git` suffix from the last path component.
196
+ var lastPathComponent = components. popLast ( ) ?? " "
197
+ lastPathComponent. removeSuffixIfPresent ( " .git " )
198
+ components. append ( lastPathComponent)
199
+
200
+ description = components. joined ( separator: " / " )
201
+
202
+ // Prepend a leading slash for file URLs and paths
203
+ if detectedScheme == " file " || string. first. flatMap ( isSeparator) ?? false {
204
+ description. insert ( " / " , at: description. startIndex)
205
+ }
206
+
207
+ self . description = description
208
+ }
209
+ }
210
+
211
+ #if os(Windows)
212
+ fileprivate let isSeparator : ( Character ) -> Bool = { $0 == " / " || $0 == " \\ " }
213
+ #else
214
+ fileprivate let isSeparator : ( Character ) -> Bool = { $0 == " / " }
215
+ #endif
216
+
217
+ private extension Character {
218
+ var isDigit : Bool {
219
+ switch self {
220
+ case " 0 " , " 1 " , " 2 " , " 3 " , " 4 " , " 5 " , " 6 " , " 7 " , " 8 " , " 9 " :
221
+ return true
222
+ default :
223
+ return false
224
+ }
225
+ }
226
+
227
+ var isAllowedInURLScheme : Bool {
228
+ return isLetter || self . isDigit || self == " + " || self == " - " || self == " . "
229
+ }
230
+ }
231
+
232
+ private extension String {
233
+ @discardableResult
234
+ mutating func removePrefixIfPresent< T: StringProtocol > ( _ prefix: T ) -> Bool {
235
+ guard hasPrefix ( prefix) else { return false }
236
+ removeFirst ( prefix. count)
237
+ return true
238
+ }
239
+
240
+ @discardableResult
241
+ mutating func removeSuffixIfPresent< T: StringProtocol > ( _ suffix: T ) -> Bool {
242
+ guard hasSuffix ( suffix) else { return false }
243
+ removeLast ( suffix. count)
244
+ return true
245
+ }
246
+
247
+ @discardableResult
248
+ mutating func dropSchemeComponentPrefixIfPresent( ) -> String ? {
249
+ if let rangeOfDelimiter = range ( of: " :// " ) ,
250
+ self [ startIndex] . isLetter,
251
+ self [ ..< rangeOfDelimiter. lowerBound] . allSatisfy ( { $0. isAllowedInURLScheme } )
252
+ {
253
+ defer { self . removeSubrange ( ..< rangeOfDelimiter. upperBound) }
254
+
255
+ return String ( self [ ..< rangeOfDelimiter. lowerBound] )
256
+ }
257
+
258
+ return nil
259
+ }
260
+
261
+ @discardableResult
262
+ mutating func dropUserinfoSubcomponentPrefixIfPresent( ) -> ( user: String , password: String ? ) ? {
263
+ if let indexOfAtSign = firstIndex ( of: " @ " ) ,
264
+ let indexOfFirstPathComponent = firstIndex ( where: isSeparator) ,
265
+ indexOfAtSign < indexOfFirstPathComponent
266
+ {
267
+ defer { self . removeSubrange ( ... indexOfAtSign) }
268
+
269
+ let userinfo = self [ ..< indexOfAtSign]
270
+ var components = userinfo. split ( separator: " : " , maxSplits: 2 , omittingEmptySubsequences: false )
271
+ guard components. count > 0 else { return nil }
272
+ let user = String ( components. removeFirst ( ) )
273
+ let password = components. last. map ( String . init)
274
+
275
+ return ( user, password)
276
+ }
277
+
278
+ return nil
279
+ }
280
+
281
+ @discardableResult
282
+ mutating func removePortComponentIfPresent( ) -> Bool {
283
+ if let indexOfFirstPathComponent = firstIndex ( where: isSeparator) ,
284
+ let startIndexOfPort = firstIndex ( of: " : " ) ,
285
+ startIndexOfPort < endIndex,
286
+ let endIndexOfPort = self [ index ( after: startIndexOfPort) ... ] . lastIndex ( where: { $0. isDigit } ) ,
287
+ endIndexOfPort <= indexOfFirstPathComponent
288
+ {
289
+ self . removeSubrange ( startIndexOfPort ... endIndexOfPort)
290
+ return true
291
+ }
292
+
293
+ return false
294
+ }
295
+
296
+ @discardableResult
297
+ mutating func removeFragmentComponentIfPresent( ) -> Bool {
298
+ if let index = firstIndex ( of: " # " ) {
299
+ self . removeSubrange ( index... )
300
+ }
301
+
302
+ return false
303
+ }
304
+
305
+ @discardableResult
306
+ mutating func removeQueryComponentIfPresent( ) -> Bool {
307
+ if let index = firstIndex ( of: " ? " ) {
308
+ self . removeSubrange ( index... )
309
+ }
310
+
311
+ return false
312
+ }
313
+
314
+ @discardableResult
315
+ mutating func replaceFirstOccurenceIfPresent< T: StringProtocol , U: StringProtocol > (
316
+ of string: T ,
317
+ before index: Index ? = nil ,
318
+ with replacement: U
319
+ ) -> Bool {
320
+ guard let range = range ( of: string) else { return false }
321
+
322
+ if let index = index, range. lowerBound >= index {
323
+ return false
324
+ }
325
+
326
+ self . replaceSubrange ( range, with: replacement)
327
+ return true
328
+ }
329
+ }
330
+
0 commit comments