Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E.164 parser fixes #2052

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 15 additions & 12 deletions StripeUICore/StripeUICore/Source/PhoneNumber/PhoneNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ import Foundation

extension PhoneNumber {

/// A regular expression for validating E.164 phone numbers.
///
/// Matches a plus sign followed by up to 17 digits, where the first digit cannot be zero.
private static let e164ValidationRegex: NSRegularExpression? = {
do {
return try NSRegularExpression(pattern: "^\\+[1-9]\\d{0,16}$")
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()

/// Parses phone numbers in (*globalized*) E.164 format.
///
/// - Note: Our metadata lacks national destination code (area code) ranges, because of this we fallback to
Expand All @@ -84,21 +96,12 @@ extension PhoneNumber {
/// - locale: User's locale.
/// - Returns: `PhoneNumber`, or `nil` if the number is not parsable.
public static func fromE164(_ number: String, locale: Locale = .current) -> PhoneNumber? {
let characters: [Character] = .init(number)

// Matching regex: ^\+[1-9]\d{2,14}$
guard
characters.count > 4,
characters.count <= Constants.e164MaxDigits + 1,
characters[0] == "+",
characters[1] != "0",
characters[1...].allSatisfy({
$0.unicodeScalars.allSatisfy(CharacterSet.stp_asciiDigit.contains(_:))
})
else {
guard e164ValidationRegex?.numberOfMatches(in: number, range: NSRange(location: 0, length: number.count)) == 1 else {
return nil
}

let characters: [Character] = .init(number)

let makePhoneNumber: (Metadata) -> PhoneNumber = { metadata in
return PhoneNumber(
number: String(characters[metadata.code.count...]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,19 @@ class PhoneNumberTests: XCTestCase {

func testFromE164_shouldHandleInvalidInput() {
XCTAssertNil(PhoneNumber.fromE164(""))
XCTAssertNil(PhoneNumber.fromE164("+"))
XCTAssertNil(PhoneNumber.fromE164("++"))
XCTAssertNil(PhoneNumber.fromE164("+13"))
XCTAssertNil(PhoneNumber.fromE164("1 (555) 555 5555"))
XCTAssertNil(PhoneNumber.fromE164("+1 (555) 555 5555"))
XCTAssertNil(PhoneNumber.fromE164("+155555555555555555")) // too long
}

func testFromE164_shouldParsePartialNumbers() {
let phoneNumber = PhoneNumber.fromE164("+44")
XCTAssertEqual(phoneNumber?.countryCode, "GB")
XCTAssertEqual(phoneNumber?.number, "")
}

func testFromE164_shouldDisambiguateUsingLocale() {
// This test number is very ambiguous, it can belong to ~25 countries/territories due to
// the "+1" calling code/prefix being shared by many countries.
Expand Down