Skip to content

Commit 75344ae

Browse files
authored
Ensure that bundle identifiers are valid URL host components (#1069)
* Add bundle identifier type that ensures a valid URL host component rdar://135335645 * Use new Identifier type in `DocumentationBundle/Info` * Update code to not use deprecated `Info/identifier` property * Use new Identifier type in `DocumentationBundle` * Update code to not use deprecated `identifier` property * Use new Identifier type in `[Unresolved|Resolved]TopicReference` * Update code to not use deprecated topic reference `bundleIdentifier` property * Deprecate `BundleIdentifier` in favor of `DocumentationBundle/Identifier` * Use new Identifier type in `BuildMetadata` * Use new Identifier type in `AssetReference` * Use new Identifier type in `ResourceReference` * Use new Identifier type in `ConvertServiceFallbackResolver` * Use new Identifier type in `SerializableLinkResolutionInformation` * Use new Identifier type in `ConvertAction/Indexer` * Prefer `bundleID` over `id` for types that scoped inside bundle * Use `bundleID` instead of `id` for property names outside the bundle * Use "bundle id" in local variable in fallback resolver
1 parent e0ce789 commit 75344ae

File tree

132 files changed

+1144
-911
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+1144
-911
lines changed

Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public struct ConvertService: DocumentationService {
128128

129129
if let linkResolvingServer {
130130
let resolver = try OutOfProcessReferenceResolver(
131-
bundleIdentifier: request.bundleInfo.identifier,
131+
bundleID: request.bundleInfo.id,
132132
server: linkResolvingServer,
133133
convertRequestIdentifier: messageIdentifier
134134
)
@@ -267,7 +267,7 @@ public struct ConvertService: DocumentationService {
267267
.compactMap { (value, isDocumentationExtensionContent) -> (ResolvedTopicReference, RenderReferenceStore.TopicContent)? in
268268
let (topicReference, article) = value
269269

270-
guard let bundle = context.bundle, bundle.identifier == topicReference.bundleIdentifier else { return nil }
270+
guard let bundle = context.bundle, bundle.id == topicReference.bundleID else { return nil }
271271
let renderer = DocumentationContentRenderer(documentationContext: context, bundle: bundle)
272272

273273
let documentationNodeKind: DocumentationNode.Kind = isDocumentationExtensionContent ? .unknownSymbol : .article

Sources/SwiftDocC/DocumentationService/Convert/Fallback Link Resolution/ConvertServiceFallbackResolver.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protocol ConvertServiceFallbackResolver {
2626
/// The bundle identifier for the fallback resolver.
2727
///
2828
/// The fallback resolver will only resolve links with this bundle identifier.
29-
var bundleIdentifier: String { get }
29+
var bundleID: DocumentationBundle.Identifier { get }
3030

3131
// MARK: References
3232

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ extension ResolvedTopicReference {
438438
let normalizedPath = NodeURLGenerator.fileSafeReferencePath(self, lowercased: true)
439439

440440
return NavigatorIndex.Identifier(
441-
bundleIdentifier: bundleIdentifier.lowercased(),
441+
bundleIdentifier: bundleID.rawValue.lowercased(),
442442
path: "/" + normalizedPath,
443443
fragment: fragment,
444444
languageIdentifier: languageIdentifier

Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift

+15-3
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,24 @@ fileprivate extension NSRegularExpression {
346346
public struct AssetReference: Hashable, Codable {
347347
/// The name of the asset.
348348
public var assetName: String
349+
@available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released")
350+
public var bundleIdentifier: String {
351+
bundleID.rawValue
352+
}
353+
349354
/// The identifier of the bundle the asset is apart of.
350-
public var bundleIdentifier: String
355+
public let bundleID: DocumentationBundle.Identifier
351356

352357
/// Creates a reference from a given asset name and the bundle it is apart of.
353-
public init(assetName: String, bundleIdentifier: String) {
358+
public init(assetName: String, bundleID: DocumentationBundle.Identifier) {
354359
self.assetName = assetName
355-
self.bundleIdentifier = bundleIdentifier
360+
self.bundleID = bundleID
361+
}
362+
@available(*, deprecated, renamed: "init(assetName:bundleID:)", message: "Use 'init(assetName:bundleID:)' instead. This deprecated API will be removed after 6.2 is released")
363+
public init(assetName: String, bundleIdentifier: String) {
364+
self.init(
365+
assetName: assetName,
366+
bundleID: .init(rawValue: bundleIdentifier)
367+
)
356368
}
357369
}

Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,19 @@ extension DocumentationContext {
1818

1919
@available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.sources", message: "Use 'configuration.externalDocumentationConfiguration.sources' instead. This deprecated API will be removed after Swift 6.2 is released.")
2020
public var externalDocumentationSources: [BundleIdentifier: ExternalDocumentationSource] {
21-
get { configuration.externalDocumentationConfiguration.sources }
22-
set { configuration.externalDocumentationConfiguration.sources = newValue }
21+
get {
22+
var result = [BundleIdentifier: ExternalDocumentationSource]()
23+
for (key, value) in configuration.externalDocumentationConfiguration.sources {
24+
result[key.rawValue] = value
25+
}
26+
return result
27+
}
28+
set {
29+
configuration.externalDocumentationConfiguration.sources.removeAll()
30+
for (key, value) in newValue {
31+
configuration.externalDocumentationConfiguration.sources[.init(rawValue: key)] = value
32+
}
33+
}
2334
}
2435

2536
@available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.globalSymbolResolver", message: "Use 'configuration.externalDocumentationConfiguration.globalSymbolResolver' instead. This deprecated API will be removed after Swift 6.2 is released.")

Sources/SwiftDocC/Infrastructure/Context/DocumentationContext+Configuration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ extension DocumentationContext {
7676
/// A collection of configuration related to external sources of documentation.
7777
public struct ExternalDocumentationConfiguration {
7878
/// The lookup of external documentation sources by their bundle identifiers.
79-
public var sources: [BundleIdentifier: ExternalDocumentationSource] = [:]
79+
public var sources: [DocumentationBundle.Identifier: ExternalDocumentationSource] = [:]
8080
/// A type that resolves all symbols that are referenced in symbol graph files but can't be found in any of the locally available symbol graph files.
8181
public var globalSymbolResolver: GlobalExternalSymbolResolver?
8282
/// A list of URLs to documentation archives that the local documentation depends on.

Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ package enum ConvertActionConverter {
161161

162162
if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
163163
do {
164-
let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.identifier)
164+
let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id)
165165
try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation)
166166

167167
if !emitDigest {
@@ -191,7 +191,7 @@ package enum ConvertActionConverter {
191191
break
192192
}
193193

194-
try outputConsumer.consume(buildMetadata: BuildMetadata(bundleDisplayName: bundle.displayName, bundleIdentifier: bundle.identifier))
194+
try outputConsumer.consume(buildMetadata: BuildMetadata(bundleDisplayName: bundle.displayName, bundleID: bundle.id))
195195

196196
// Log the finalized topic graph checksum.
197197
benchmark(add: Benchmark.TopicGraphHash(context: context))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
extension DocumentationBundle {
14+
/// A stable and locally unique identifier for a collection of documentation inputs.
15+
public struct Identifier: RawRepresentable {
16+
public let rawValue: String
17+
18+
public init(rawValue: String) {
19+
// To ensure that the identifier can be used as a valid "host" component of a resolved topic reference's url,
20+
// replace any consecutive sequence of unsupported characters with a "-".
21+
self.rawValue = rawValue
22+
.components(separatedBy: Self.charactersToReplace)
23+
.filter { !$0.isEmpty }
24+
.joined(separator: "-")
25+
}
26+
27+
private static let charactersToReplace = CharacterSet.urlHostAllowed.inverted
28+
}
29+
}
30+
31+
extension DocumentationBundle.Identifier: Hashable {}
32+
extension DocumentationBundle.Identifier: Sendable {}
33+
34+
// Support creating an identifier from a string literal.
35+
extension DocumentationBundle.Identifier: ExpressibleByStringLiteral {
36+
public init(stringLiteral value: StringLiteralType) {
37+
self.init(rawValue: value)
38+
}
39+
}
40+
41+
// Sort identifiers based on their raw string value.
42+
extension DocumentationBundle.Identifier: Comparable {
43+
public static func < (lhs: Self, rhs: Self) -> Bool {
44+
lhs.rawValue < rhs.rawValue
45+
}
46+
}
47+
48+
// Print as a single string value
49+
extension DocumentationBundle.Identifier: CustomStringConvertible {
50+
public var description: String {
51+
rawValue
52+
}
53+
}
54+
55+
// Encode and decode the identifier as a single string value.
56+
extension DocumentationBundle.Identifier: Codable {
57+
public init(from decoder: Decoder) throws {
58+
let container = try decoder.singleValueContainer()
59+
let rawValue = try container.decode(String.self)
60+
self.init(rawValue: rawValue)
61+
}
62+
63+
public func encode(to encoder: Encoder) throws {
64+
var container = encoder.singleValueContainer()
65+
try container.encode(rawValue)
66+
}
67+
}

Sources/SwiftDocC/Infrastructure/DocumentationBundle.swift

+10-8
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,14 @@ public struct DocumentationBundle {
5757
info.displayName
5858
}
5959

60-
/// A identifier for this unit of documentation
61-
///
62-
/// The string is typically in reverse DNS format using only the Roman alphabet in upper and lower case (A–Z, a–z), the dot (“.”), and the hyphen (“-”).
60+
@available(*, deprecated, renamed: "id", message: "Use 'id' instead. This deprecated API will be removed after 6.2 is released")
6361
public var identifier: String {
64-
info.identifier
62+
id.rawValue
63+
}
64+
65+
/// The documentation bundle's stable and locally unique identifier.
66+
public var id: DocumentationBundle.Identifier {
67+
info.id
6568
}
6669

6770
/**
@@ -107,7 +110,6 @@ public struct DocumentationBundle {
107110

108111
/// A custom JSON settings file used to theme renderer output.
109112
public let themeSettings: URL?
110-
111113
/// A URL prefix to be appended to the relative presentation URL.
112114
///
113115
/// This is used when a built documentation is hosted in a known location.
@@ -142,9 +144,9 @@ public struct DocumentationBundle {
142144
self.customHeader = customHeader
143145
self.customFooter = customFooter
144146
self.themeSettings = themeSettings
145-
self.rootReference = ResolvedTopicReference(bundleIdentifier: info.identifier, path: "/", sourceLanguage: .swift)
146-
self.documentationRootReference = ResolvedTopicReference(bundleIdentifier: info.identifier, path: NodeURLGenerator.Path.documentationFolder, sourceLanguage: .swift)
147-
self.tutorialTableOfContentsContainer = ResolvedTopicReference(bundleIdentifier: info.identifier, path: NodeURLGenerator.Path.tutorialsFolder, sourceLanguage: .swift)
147+
self.rootReference = ResolvedTopicReference(bundleID: info.id, path: "/", sourceLanguage: .swift)
148+
self.documentationRootReference = ResolvedTopicReference(bundleID: info.id, path: NodeURLGenerator.Path.documentationFolder, sourceLanguage: .swift)
149+
self.tutorialTableOfContentsContainer = ResolvedTopicReference(bundleID: info.id, path: NodeURLGenerator.Path.tutorialsFolder, sourceLanguage: .swift)
148150
self.tutorialsContainerReference = tutorialTableOfContentsContainer.appendingPath(urlReadablePath(info.displayName))
149151
self.articlesDocumentationRootReference = documentationRootReference.appendingPath(urlReadablePath(info.displayName))
150152
}

0 commit comments

Comments
 (0)