Skip to content

Commit 5e638b0

Browse files
authored
[Collections] 'describe' package matching is too loose (#3543)
* [Collections] 'describe' package matching is too loose Motivation: rdar://79069839 To distinguish between package vs. collection URL, we first search for it as if it were a package URL. The package search, however, is done using `PackageIdentity` and not the actual URL itself. The problem with identity is that it only uses the last URL path component, so `bar/foo` and `baz/foo` would end up with the same identity `foo`. Modifications: Update `findPackage` to allow multiple results since a package identity (the way it is) can be associated with multiple repository URLs. Caller of `findPackage` should then filter out packages by repository URL if needed.
1 parent 248636a commit 5e638b0

File tree

5 files changed

+48
-18
lines changed

5 files changed

+48
-18
lines changed

Sources/PackageCollections/PackageCollections+Storage.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@
1111
import TSCBasic
1212

1313
extension PackageCollections {
14-
public struct Storage: Closable {
14+
struct Storage: Closable {
1515
let sources: PackageCollectionsSourcesStorage
1616
let collections: PackageCollectionsStorage
1717

18-
public init(sources: PackageCollectionsSourcesStorage, collections: PackageCollectionsStorage) {
18+
init(sources: PackageCollectionsSourcesStorage, collections: PackageCollectionsStorage) {
1919
self.sources = sources
2020
self.collections = collections
2121
}
2222
}
2323
}
2424

2525
extension PackageCollections.Storage {
26-
public func close() throws {
26+
func close() throws {
2727
var errors = [Error]()
2828

2929
let tryClose = { (item: Any) in

Sources/PackageCollections/PackageCollections.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
318318
}
319319

320320
// first find in storage
321-
self.findPackage(identifier: reference.identity, collections: collections) { result in
321+
self.findPackage(reference: reference, collections: collections) { result in
322322
switch result {
323323
case .failure(let error):
324324
callback(.failure(error))
@@ -479,7 +479,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
479479
}
480480
}
481481

482-
func findPackage(identifier: PackageIdentity,
482+
func findPackage(reference: PackageReference,
483483
collections: Set<PackageCollectionsModel.CollectionIdentifier>?,
484484
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult.Item, Error>) -> Void) {
485485
self.storage.sources.list { result in
@@ -492,9 +492,21 @@ public struct PackageCollections: PackageCollectionsProtocol {
492492
collectionIdentifiers = collectionIdentifiers.filter { collections.contains($0) }
493493
}
494494
if collectionIdentifiers.isEmpty {
495-
return callback(.failure(NotFoundError("\(identifier)")))
495+
return callback(.failure(NotFoundError("\(reference)")))
496+
}
497+
self.storage.collections.findPackage(identifier: reference.identity, collectionIdentifiers: collectionIdentifiers) { findPackageResult in
498+
switch findPackageResult {
499+
case .failure(let error):
500+
callback(.failure(error))
501+
case .success(let packagesCollections):
502+
// A package identity can be associated with multiple repository URLs
503+
let matches = packagesCollections.packages.filter { $0.reference.canonicalLocation == reference.canonicalLocation }
504+
guard let package = matches.first else {
505+
return callback(.failure(NotFoundError("\(reference)")))
506+
}
507+
callback(.success(.init(package: package, collections: packagesCollections.collections)))
508+
}
496509
}
497-
self.storage.collections.findPackage(identifier: identifier, collectionIdentifiers: collectionIdentifiers, callback: callback)
498510
}
499511
}
500512
}
@@ -584,3 +596,9 @@ private struct UnknownProvider: Error {
584596
self.sourceType = sourceType
585597
}
586598
}
599+
600+
private extension PackageReference {
601+
var canonicalLocation: String {
602+
(self.location.hasSuffix(".git") ? String(self.location.dropLast(4)) : self.location).lowercased()
603+
}
604+
}

Sources/PackageCollections/Storage/PackageCollectionsStorage.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2020 Apple Inc. and the Swift project authors
4+
Copyright (c) 2020-2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -53,15 +53,17 @@ public protocol PackageCollectionsStorage {
5353
query: String,
5454
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult, Error>) -> Void)
5555

56-
/// Returns `PackageSearchResult.Item` for the given package identity.
56+
/// Returns packages for the given package identity.
57+
///
58+
/// Since a package identity can be associated with more than one repository URL, the result may contain multiple items.
5759
///
5860
/// - Parameters:
5961
/// - identifier: The package identifier
6062
/// - collectionIdentifiers: Optional. The identifiers of the `PackageCollection`s
6163
/// - callback: The closure to invoke when result becomes available
6264
func findPackage(identifier: PackageIdentity,
6365
collectionIdentifiers: [PackageCollectionsModel.CollectionIdentifier]?,
64-
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult.Item, Error>) -> Void)
66+
callback: @escaping (Result<(packages: [PackageCollectionsModel.Package], collections: [PackageCollectionsModel.CollectionIdentifier]), Error>) -> Void)
6567

6668
/// Returns `TargetSearchResult` for the given search criteria.
6769
///

Sources/PackageCollections/Storage/SQLitePackageCollectionsStorage.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
405405

406406
func findPackage(identifier: PackageIdentity,
407407
collectionIdentifiers: [Model.CollectionIdentifier]?,
408-
callback: @escaping (Result<Model.PackageSearchResult.Item, Error>) -> Void) {
408+
callback: @escaping (Result<(packages: [PackageCollectionsModel.Package], collections: [PackageCollectionsModel.CollectionIdentifier]), Error>) -> Void) {
409409
let useSearchIndices: Bool
410410
do {
411411
useSearchIndices = try self.shouldUseSearchIndices()
@@ -454,11 +454,17 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
454454
// Sort collections by processing date so the latest metadata is first
455455
.sorted(by: { lhs, rhs in lhs.lastProcessedAt > rhs.lastProcessedAt })
456456

457-
guard let package = collections.compactMap({ $0.packages.first { $0.reference.identity == identifier } }).first else {
457+
// rdar://79069839 - Package identities are not unique to repository URLs so there can be more than one result.
458+
// It's up to the caller to filter out the best-matched package(s). Results are sorted with the latest ones first.
459+
let packages = collections.flatMap { collection in
460+
collection.packages.filter { $0.reference.identity == identifier }
461+
}
462+
463+
guard !packages.isEmpty else {
458464
return callback(.failure(NotFoundError("\(identifier)")))
459465
}
460466

461-
callback(.success(.init(package: package, collections: collections.map { $0.identifier })))
467+
callback(.success((packages: packages, collections: collections.map { $0.identifier })))
462468
}
463469
}
464470
} else {
@@ -473,12 +479,16 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
473479
.first(where: { $0.reference.identity == identifier })
474480
.flatMap { (collection: collection.identifier, package: $0) }
475481
}
476-
// first package should have latest processing date
477-
guard let package = collectionPackages.first?.package else {
482+
483+
// rdar://79069839 - Package identities are not unique to repository URLs so there can be more than one result.
484+
// It's up to the caller to filter out the best-matched package(s). Results are sorted with the latest ones first.
485+
let packages = collectionPackages.map { $0.package }
486+
487+
guard !packages.isEmpty else {
478488
return callback(.failure(NotFoundError("\(identifier)")))
479489
}
480-
let collections = collectionPackages.map { $0.collection }
481-
callback(.success(.init(package: package, collections: collections)))
490+
491+
callback(.success((packages: packages, collections: collectionPackages.map { $0.collection })))
482492
}
483493
}
484494
}

Sources/PackageCollectionsModel/Formats/v1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To begin, define the top-level metadata about the collection:
2727

2828
Each item in the `packages` array is a package object with the following properties:
2929

30-
* `url`: The URL of the package. Currently only Git repository URLs are supported.
30+
* `url`: The URL of the package. Currently only Git repository URLs are supported. URL should be HTTPS and may contain `.git` suffix.
3131
* `summary`: A description of the package. **Optional.**
3232
* `keywords`: An array of keywords that the package is associated with. **Optional.**
3333
* `readmeURL`: The URL of the package's README. **Optional.**

0 commit comments

Comments
 (0)