Skip to content

Commit e8dd2e7

Browse files
correct semantic version parsing
The semantic versioning specification 2.0.0 [states](https://semver.org/#spec-item-9) that pre-release identifiers must be positioned after the version core, and build metadata identifiers after pre-release identifiers. In the old implementation, if a version core was appended with metadata identifiers that contain hyphens ("-"), the first hyphen would be mistaken as an indication of pre-release identifiers thereafter. Then, the position of the first hyphen would be treated as where the version core ends, resulting in a false negative after it was found that the "version core" contained non-numeric characters. For example: the semantic version `1.2.3+some-meta.data` is a well-formed, with `1.2.3` being the version core and `some-meta.data` the metadata identifiers. However, the old implementation of `Version.init?(_ versionString: String)` would falsely treat `1.2.3+some` as the version core and `meta.data` the pre-release identifiers. The new implementation fixes this problem by restricting the search area for "+" to the substring after the first "-". In addition, the logic for breaking up the version core into numeric identifiers has been rewritten to be more understandable. --- Currently, the test fails because of a problem with overloaded initialisers (`Version.init(_ version: Version)` and `Version.init?(_ versionString: String`). After this problem is resolved, a new commit containing a complete set of test cases is likely to be squashed into this one.
1 parent f808059 commit e8dd2e7

File tree

2 files changed

+43
-30
lines changed

2 files changed

+43
-30
lines changed

Sources/PackageDescription/Version+StringLiteralConvertible.swift

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -59,36 +59,42 @@ extension Version {
5959
}
6060

6161
/// Initializes a version struct with the provided version string.
62-
///
63-
/// - Parameters:
64-
/// - version: A version string to use for creating a new version struct.
62+
/// - Parameter version: A version string to use for creating a new version struct.
6563
public init?(_ versionString: String) {
66-
let prereleaseStartIndex = versionString.firstIndex(of: "-")
67-
let metadataStartIndex = versionString.firstIndex(of: "+")
68-
69-
let requiredEndIndex = prereleaseStartIndex ?? metadataStartIndex ?? versionString.endIndex
70-
let requiredCharacters = versionString.prefix(upTo: requiredEndIndex)
71-
let requiredComponents = requiredCharacters
72-
.split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false)
73-
.map(String.init)
74-
.compactMap({ Int($0) })
75-
.filter({ $0 >= 0 })
76-
77-
guard requiredComponents.count == 3 else { return nil }
78-
79-
self.major = requiredComponents[0]
80-
self.minor = requiredComponents[1]
81-
self.patch = requiredComponents[2]
82-
83-
func identifiers(start: String.Index?, end: String.Index) -> [String] {
84-
guard let start = start else { return [] }
85-
let identifiers = versionString[versionString.index(after: start)..<end]
86-
return identifiers.split(separator: ".").map(String.init)
87-
}
88-
89-
self.prereleaseIdentifiers = identifiers(
90-
start: prereleaseStartIndex,
91-
end: metadataStartIndex ?? versionString.endIndex)
92-
self.buildMetadataIdentifiers = identifiers(start: metadataStartIndex, end: versionString.endIndex)
64+
// SemVer 2.0.0 allows only ASCII alphanumerical characters and "-" in the version string, except for "." and "+" as delimiters. ("-" is used as a delimiter between the version core and pre-release identifiers, but it's allowed within pre-release and metadata identifiers as well.)
65+
// Alphanumerics check will come later, after each identifier is split out (i.e. after the delimiters are removed).
66+
guard versionString.allSatisfy( \.isASCII ) else { return nil }
67+
68+
let prereleaseDelimiterIndex = versionString.firstIndex(of: "-")
69+
// SemVer 2.0.0 requires that metadata identifiers come after pre-release identifiers
70+
let metadataDelimiterIndex = versionString[(versionString.firstIndex(of: "-") ?? versionString.startIndex)...].firstIndex(of: "+")
71+
72+
let prereleaseStartIndex = prereleaseDelimiterIndex.map(versionString.index(after:)) ?? versionString.endIndex
73+
let metadataStartIndex = metadataDelimiterIndex.map(versionString.index(after:)) ?? versionString.endIndex
74+
75+
let versionCore = versionString[..<(prereleaseDelimiterIndex ?? versionString.endIndex)]
76+
let versionCoreIdentifiers = versionCore.split(separator: ".", omittingEmptySubsequences: false)
77+
78+
guard
79+
versionCoreIdentifiers.count == 3,
80+
// Major, minor, and patch versions must be ASCII numbers, according to the semantic versioning standard.
81+
// Converting each identifier from a substring to an integer fails if the identifiers have illegal characters (except for "-").
82+
// Comparisons against 0 fail if the identifiers have "-".
83+
let major = Int(versionCoreIdentifiers[0]), major >= 0,
84+
let minor = Int(versionCoreIdentifiers[1]), minor >= 0,
85+
let patch = Int(versionCoreIdentifiers[2]), patch >= 0
86+
else { return nil }
87+
88+
self.major = major
89+
self.minor = minor
90+
self.patch = patch
91+
92+
let prereleaseIdentifiers = versionString[prereleaseStartIndex..<(metadataDelimiterIndex ?? versionString.endIndex)].split(separator: ".")
93+
guard prereleaseIdentifiers.allSatisfy( { $0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } ) else { return nil }
94+
self.prereleaseIdentifiers = prereleaseIdentifiers.map { String($0) }
95+
96+
let buildMetadataIdentifiers = versionString[metadataStartIndex...].split(separator: ".")
97+
guard buildMetadataIdentifiers.allSatisfy( { $0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } ) else { return nil }
98+
self.buildMetadataIdentifiers = buildMetadataIdentifiers.map { String($0) }
9399
}
94100
}

Tests/PackageDescriptionTests/VersionTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ class VersionTests: XCTestCase {
2222
XCTAssertLessThan(Version("1.2.3-alpha.beta.2"), Version("1.2.3-alpha.beta.3"))
2323

2424
XCTAssertEqual(Version("1.2.3-alpha.beta.2").description, "1.2.3-alpha.beta.2")
25+
26+
27+
28+
XCTAssertNotNil(Version("0.0.0"))
29+
XCTAssertNotNil(Version("1.2.3"))
30+
XCTAssertNil(Version("4.2"))
31+
2532
}
2633
}
2734

0 commit comments

Comments
 (0)