// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation public import LibSignalClient // MARK: - @objc public class SignalServiceAddress: NSObject, NSCopying, NSSecureCoding, Codable { private let cachedAddress: CachedAddress @objc public var phoneNumber: String? { cachedAddress.identifiers.get().phoneNumber } public var e164: E164? { phoneNumber.flatMap { E164($0) } } @objc public var e164ObjC: E164ObjC? { phoneNumber.flatMap { E164ObjC($0) } } /// The "service id" (could be an ACI or PNI). /// /// This value is optional since it may not be present in all cases. Some /// examples: /// /// * A really old recipient, who you may have message history with, may not /// have re-registered since UUIDs were added. /// /// * When doing "find by phone number", there will be a window of time /// where all we know about a recipient is their e164. public var serviceId: ServiceId? { cachedAddress.identifiers.get().serviceId } @objc public var serviceIdObjC: ServiceIdObjC? { serviceId.map { ServiceIdObjC.wrapValue($0) } } /// Returns the `serviceId` if it's an ACI. /// /// - Note: Call this only if you **expect** an `Aci` (or nil). If the /// result could be a `Pni`, you shouldn't call this method. public var aci: Aci? { guard let result = serviceId as? Aci? else { owsFailDebug("Expected an ACI but found something else.") return nil } return result } @objc public var aciString: String? { aci?.serviceIdString } @objc public var aciUppercaseString: String? { aci?.serviceIdUppercaseString } @objc public var serviceIdString: String? { serviceId?.serviceIdString } @objc public var serviceIdUppercaseString: String? { serviceId?.serviceIdUppercaseString } /// Returns a canonical address from the cache. /// /// If you initialize an address with the wrong phone number, that address /// will keep that phone number. This method will update that phone number /// to match what's currently in the cache (ie what's on SignalRecipient). public func withNormalizedPhoneNumber(cache: SignalServiceAddressCache? = nil) -> SignalServiceAddress { let identifiers = cachedAddress.identifiers.get() return SignalServiceAddress( serviceId: identifiers.serviceId, // If there's no ServiceId, then we look up the phone number in the cache. phoneNumber: (identifiers.serviceId == nil) ? identifiers.phoneNumber : nil, cache: cache ?? SSKEnvironment.shared.signalServiceAddressCacheRef ) } /// Returns a source-of-truth canonicalized address. /// /// If an address is initialized with the wrong phone number, it'll keep it /// until the phone number for the ACI changes; this method will update it /// immediately. If you initialize an address with a PNI, it'll keep the PNI /// forever; this method will update it to the ACI (but only if the ACI, /// PNI, and phone number are all known and linked to one another). public func withNormalizedPhoneNumberAndServiceId(cache: SignalServiceAddressCache? = nil) -> SignalServiceAddress { return withNormalizedPhoneNumber(cache: cache).withNormalizedServiceId(cache: cache) } private func withNormalizedServiceId(cache: SignalServiceAddressCache?) -> SignalServiceAddress { let identifiers = cachedAddress.identifiers.get() guard let phoneNumber = identifiers.phoneNumber, identifiers.serviceId is Pni else { // This is a private method, and `self` is already built against `cache`. return self } return SignalServiceAddress( serviceId: nil, phoneNumber: phoneNumber, cache: cache ?? SSKEnvironment.shared.signalServiceAddressCacheRef ) } // MARK: - Initializers /// Initializes a "legacy" address. /// /// Legacy addresses were saved by prior versions of the application before /// ACIs were known, so they generally contain only a phone number. They are /// sent through a migration path that allows them to be resolved to ACIs in /// cases where other SignalServiceAddresses can't be resolved to ACIs. /// /// "Modern legacy addresses" (ie modern builds writing to places that may /// also contain legacy addresses) will encode the ACI instead of the phone /// number, thus skipping the migration path in this initializer (good!). public convenience init( serviceId: ServiceId?, legacyPhoneNumber phoneNumber: String?, cache: SignalServiceAddressCache ) { let normalizedAddress = NormalizedDatabaseRecordAddress( serviceId: serviceId, phoneNumber: phoneNumber ) self.init( serviceId: normalizedAddress?.serviceId, phoneNumber: normalizedAddress?.phoneNumber, isLegacyPhoneNumber: true, cache: cache ) } public static func legacyAddress(serviceId: ServiceId?, phoneNumber: String?) -> SignalServiceAddress { return SignalServiceAddress( serviceId: serviceId, legacyPhoneNumber: phoneNumber, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } public static func legacyAddress(aciString: String?, phoneNumber: String?) -> SignalServiceAddress { return SignalServiceAddress( serviceId: Aci.parseFrom(aciString: aciString), legacyPhoneNumber: phoneNumber, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } @objc public static func legacyAddress(serviceIdString: String?, phoneNumber: String?) -> SignalServiceAddress { return SignalServiceAddress( serviceId: serviceIdString.flatMap { try? ServiceId.parseFrom(serviceIdString: $0) }, legacyPhoneNumber: phoneNumber, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } public convenience init(_ e164: E164) { self.init(phoneNumber: e164.stringValue) } @objc public convenience init(phoneNumber: String) { self.init(serviceId: nil, phoneNumber: phoneNumber) } /// Initializes an address that should refer to an Aci. /// /// - Note: Call this only if you **expect** an `Aci` in all cases. If the /// value might be a Pni, you shouldn't call this method. @objc public convenience init(aciString: String) { self.init(aciString: aciString, phoneNumber: nil) } /// Initializes an address that should refer to an Aci. /// /// - Note: Call this only if you **expect** an `Aci` (or nil) in all cases. /// If the value might be a Pni, you shouldn't call this method. @objc public convenience init(aciString: String?, phoneNumber: String?) { self.init(serviceIdString: aciString, allowPni: false, phoneNumber: phoneNumber) } /// Initializes an address for an Aci or Pni. @objc public convenience init(serviceIdString: String) { self.init(serviceIdString: serviceIdString, phoneNumber: nil) } /// Initializes an address for an Aci or Pni. @objc public convenience init(serviceIdString: String?, phoneNumber: String?) { self.init(serviceIdString: serviceIdString, allowPni: true, phoneNumber: phoneNumber) } /// Initializes an address for an Aci or Pni. /// /// - Parameter allowPni: If false, PNIs will be treated as invalid. @objc public convenience init(serviceIdString: String?, allowPni: Bool, phoneNumber: String?) { self.init( serviceId: serviceIdString.flatMap { let serviceId = try? ServiceId.parseFrom(serviceIdString: $0) if serviceId is Aci { return serviceId } if serviceId is Pni, allowPni { return serviceId } owsFailDebug("Unexpectedly initialized SignalServiceAddress with invalid serviceIdString.") return nil }, phoneNumber: phoneNumber ) } @objc public convenience init(serviceIdObjC: ServiceIdObjC) { self.init(serviceIdObjC.wrappedValue) } public convenience init(_ serviceId: ServiceId) { self.init(serviceId: serviceId, phoneNumber: nil) } public convenience init(serviceId: ServiceId?, e164: E164?) { self.init(serviceId: serviceId, phoneNumber: e164?.stringValue) } public convenience init(serviceId: ServiceId?, phoneNumber: String?) { self.init( serviceId: serviceId, phoneNumber: phoneNumber, isLegacyPhoneNumber: false, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } internal convenience init(from address: ProtocolAddress) { self.init(address.serviceId) } public convenience init( serviceId: ServiceId?, phoneNumber: String?, cache: SignalServiceAddressCache ) { self.init( serviceId: serviceId, phoneNumber: phoneNumber, isLegacyPhoneNumber: false, cache: cache ) } private init( serviceId: ServiceId?, phoneNumber: String?, isLegacyPhoneNumber: Bool, cache: SignalServiceAddressCache ) { if let phoneNumber, phoneNumber.isEmpty { owsFailDebug("Unexpectedly initialized signal service address with invalid phone number") } self.cachedAddress = cache.registerAddress( proposedIdentifiers: CachedAddress.Identifiers( serviceId: serviceId, phoneNumber: phoneNumber ), isLegacyPhoneNumber: isLegacyPhoneNumber ) super.init() if !isValid { owsFailDebug("Unexpectedly initialized address with no identifier") } } // MARK: - Codable private enum CodingKeys: String, CodingKey { case backingUuid, backingPhoneNumber } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) let identifiers = cachedAddress.identifiers.get() try container.encode(identifiers.serviceId?.serviceIdUppercaseString, forKey: .backingUuid) // Only encode the backingPhoneNumber if we don't know the UUID try container.encode(identifiers.serviceId != nil ? nil : identifiers.phoneNumber, forKey: .backingPhoneNumber) } public required convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let decodedServiceId = try container.decodeIfPresent(String.self, forKey: .backingUuid).map { return try ServiceId.parseFrom(serviceIdString: $0) } let decodedPhoneNumber = try container.decodeIfPresent(String.self, forKey: .backingPhoneNumber) self.init( serviceId: decodedServiceId, legacyPhoneNumber: decodedPhoneNumber, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } // MARK: - NSSecureCoding public static let supportsSecureCoding: Bool = true public func encode(with aCoder: NSCoder) { let identifiers = cachedAddress.identifiers.get() aCoder.encode(identifiers.serviceId.map { serviceId -> Any in // For now, encode Acis in a backwards-compatible manner. This can be // changed in the future, but it will prevent downgrades. switch serviceId { case is Aci: return serviceId.rawUUID default: return serviceId.serviceIdBinary } }, forKey: "backingUuid") // Only encode the backingPhoneNumber if we don't know the UUID aCoder.encode(identifiers.serviceId != nil ? nil : identifiers.phoneNumber, forKey: "backingPhoneNumber") } public convenience required init?(coder aDecoder: NSCoder) { let decodedServiceId: ServiceId? switch aDecoder.decodeObject(of: [NSUUID.self, NSData.self], forKey: "backingUuid") { case nil: decodedServiceId = nil case let serviceIdBinary as Data: do { decodedServiceId = try ServiceId.parseFrom(serviceIdBinary: serviceIdBinary) } catch { owsFailDebug("Couldn't parse serviceIdBinary.") return nil } case let deprecatedUuid as NSUUID: decodedServiceId = Aci(fromUUID: deprecatedUuid as UUID) default: return nil } let decodedPhoneNumber = aDecoder.decodeObject(of: NSString.self, forKey: "backingPhoneNumber") as String? self.init( serviceId: decodedServiceId, legacyPhoneNumber: decodedPhoneNumber, cache: SSKEnvironment.shared.signalServiceAddressCacheRef ) } // MARK: - @objc public func copy(with zone: NSZone? = nil) -> Any { return self } public override func isEqual(_ object: Any?) -> Bool { guard let otherAddress = object as? SignalServiceAddress else { return false } let result = isEqualToAddress(otherAddress) if result, cachedAddress.hashValue != otherAddress.cachedAddress.hashValue { Logger.warn("Equal addresses have different hashes: \(self), other: \(otherAddress).") } return result } @objc public func isEqualToAddress(_ otherAddress: SignalServiceAddress?) -> Bool { guard let otherAddress else { return false } let this = cachedAddress.identifiers.get() let other = otherAddress.cachedAddress.identifiers.get() if let thisServiceId = this.serviceId, let otherServiceId = other.serviceId { return thisServiceId == otherServiceId } if let thisPhoneNumber = this.phoneNumber, let otherPhoneNumber = other.phoneNumber { return thisPhoneNumber == otherPhoneNumber } return false } // In order to maintain a consistent hash, we use a constant value generated // by the cache that can be mapped back to the phone number OR the UUID. // // This allows us to dynamically update the backing values to maintain // the most complete address object as we learn phone <-> UUID mapping, // while also allowing addresses to live in hash tables. public override var hash: Int { return cachedAddress.hashValue } // MARK: - @objc public var isValid: Bool { let identifiers = cachedAddress.identifiers.get() if identifiers.serviceId != nil { return true } if let phoneNumber = identifiers.phoneNumber { return !phoneNumber.isEmpty } return false } @objc public var isLocalAddress: Bool { return DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress == self } @objc override public var description: String { let identifiers = cachedAddress.identifiers.get() return Self.addressComponentsDescription( uuidString: identifiers.serviceId?.logString, phoneNumber: identifiers.phoneNumber ) } public static func addressComponentsDescription(uuidString: String?, phoneNumber: String?) -> String { var splits = [String]() if let uuid = uuidString?.nilIfEmpty { splits.append("serviceId: " + uuid) } else if let phoneNumber = phoneNumber?.nilIfEmpty { splits.append("phoneNumber: " + phoneNumber) } return "<" + splits.joined(separator: ", ") + ">" } } // MARK: - private class CachedAddress { struct Identifiers: Equatable { var serviceId: ServiceId? var phoneNumber: String? } let hashValue: Int let identifiers: AtomicValue init(hashValue: Int, identifiers: Identifiers) { self.hashValue = hashValue self.identifiers = AtomicValue(identifiers, lock: .init()) } } public class SignalServiceAddressCache: NSObject { private let state = AtomicValue(CacheState(), lock: .init()) private let _phoneNumberVisibilityFetcher: PhoneNumberVisibilityFetcher? private var phoneNumberVisibilityFetcher: PhoneNumberVisibilityFetcher { return _phoneNumberVisibilityFetcher ?? DependenciesBridge.shared.phoneNumberVisibilityFetcher } private struct CacheState { var serviceIdHashValues = [ServiceId: Int]() var phoneNumberHashValues = [String: Int]() var serviceIdToPhoneNumber = [ServiceId: PotentiallyVisible]() var phoneNumberToServiceIds = [String: [PotentiallyVisible]]() var serviceIdCachedAddresses = [ServiceId: [CachedAddress]]() var phoneNumberOnlyCachedAddresses = [String: [CachedAddress]]() var phoneNumberOnlyLegacyCachedAddresses = [String: [CachedAddress]]() } /// Tracks a relationship that may or may not be visible. /// /// Hidden relationships are used to handle "legacy" address migrations. private struct PotentiallyVisible: Equatable { var wrappedValue: T var isVisible: Bool } // TODO: Remove this initializer after fixing DependenciesBridge setup. public override init() { self._phoneNumberVisibilityFetcher = nil } public init(phoneNumberVisibilityFetcher: any PhoneNumberVisibilityFetcher) { self._phoneNumberVisibilityFetcher = phoneNumberVisibilityFetcher } func prepareCache() { owsAssertDebug(GRDBSchemaMigrator.areMigrationsComplete) SSKEnvironment.shared.databaseStorageRef.read { tx in let bulkFetcher: BulkPhoneNumberVisibilityFetcher? do { bulkFetcher = try phoneNumberVisibilityFetcher.fetchAll(tx: tx) } catch { Logger.warn("Couldn't fetch visible phone numbers. Hiding all of them…") bulkFetcher = nil } let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable recipientDatabaseTable.enumerateAll(tx: tx) { recipient in updateRecipient( recipient, isPhoneNumberVisible: ( bulkFetcher?.isPhoneNumberVisible(for: recipient) ?? false ) ) } } } /// Updates the cache to reflect `signalRecipient`. /// /// This method doesn't require a write transaction to function, but there's /// an assumption throughout the application that SignalServiceAddresses /// won't change outside of a write transaction. Therefore, this method /// requires one to allow the compiler to help enforce this invariant. public func updateRecipient(_ signalRecipient: SignalRecipient, tx: DBWriteTransaction) { updateRecipient( signalRecipient, isPhoneNumberVisible: phoneNumberVisibilityFetcher.isPhoneNumberVisible(for: signalRecipient, tx: tx) ) } internal func updateRecipient(_ signalRecipient: SignalRecipient, isPhoneNumberVisible: Bool) { updateRecipient( aci: signalRecipient.aci, pni: signalRecipient.pni, phoneNumber: signalRecipient.phoneNumber?.stringValue, isPhoneNumberVisible: isPhoneNumberVisible ) } private func updateRecipient(aci: Aci?, pni: Pni?, phoneNumber: String?, isPhoneNumberVisible: Bool) { state.update { cacheState in // This cache associates phone numbers to the other identifiers. If we // don't have a phone number, there's nothing to associate. // // We never remove a phone number from one recipient without *also* adding // it to some other recipient; therefore, we handle the transfer when that // recipient is passed to this method. (This avoids a potential problem // that could occur if we learn about the "delete" after the "update".) guard let phoneNumber else { return } let oldPotentiallyVisibleServiceIds: [PotentiallyVisible] = ( cacheState.phoneNumberToServiceIds[phoneNumber] ?? [] ) let newPotentiallyVisibleServiceIds: [PotentiallyVisible] = [ aci.map { PotentiallyVisible(wrappedValue: $0, isVisible: isPhoneNumberVisible) }, pni.map { PotentiallyVisible(wrappedValue: $0, isVisible: true) }, ].compacted() // If this phone number still points at the same ServiceIds in the same // way, there's nothing to change. if newPotentiallyVisibleServiceIds == oldPotentiallyVisibleServiceIds { return } // Update the "phone number -> service ids" lookup table. cacheState.phoneNumberToServiceIds[phoneNumber] = newPotentiallyVisibleServiceIds // Update the "service id -> phone number" lookup table by clearing the // entries for any values that were removed and updating the entries for // any values that were added or changed. for oldServiceId in oldPotentiallyVisibleServiceIds { if newPotentiallyVisibleServiceIds.contains(where: { $0.wrappedValue == oldServiceId.wrappedValue }) { continue } cacheState.serviceIdToPhoneNumber[oldServiceId.wrappedValue] = nil } for newOrUpdatedServiceId in newPotentiallyVisibleServiceIds { let oldPhoneNumber = cacheState.serviceIdToPhoneNumber.updateValue( PotentiallyVisible(wrappedValue: phoneNumber, isVisible: newOrUpdatedServiceId.isVisible), forKey: newOrUpdatedServiceId.wrappedValue ) // If this ServiceId was associated with some other phone number, we need // to break that association. if let oldPhoneNumber, oldPhoneNumber.wrappedValue != phoneNumber { cacheState.phoneNumberToServiceIds[oldPhoneNumber.wrappedValue]?.removeAll(where: { return $0.wrappedValue == newOrUpdatedServiceId.wrappedValue }) } // This might be the first time we're learning about this ServiceId or // phone number. If a preferred hash value is available, make sure all // future SignalServiceAddress instances will be able to find it. _ = hashValue(cacheState: &cacheState, serviceId: newOrUpdatedServiceId.wrappedValue, phoneNumber: phoneNumber) } let oldVisibleServiceIds: [ServiceId] = oldPotentiallyVisibleServiceIds.compactMap { return $0.isVisible ? $0.wrappedValue : nil } let newVisibleServiceIds: [ServiceId] = newPotentiallyVisibleServiceIds.compactMap { return $0.isVisible ? $0.wrappedValue : nil } // These ServiceIds are no longer visibly associated with `phoneNumber`. for serviceId in Set(oldVisibleServiceIds).subtracting(newVisibleServiceIds) { cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in cachedAddress.identifiers.update { $0.phoneNumber = nil } } } // These ServiceIds are now visibly associated with `phoneNumber`. for serviceId in Set(newVisibleServiceIds).subtracting(oldVisibleServiceIds) { cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in cachedAddress.identifiers.update { $0.phoneNumber = phoneNumber } } } // "Legacy" addresses can be resolved using any ServiceId. updatePhoneNumberOnlyAddresses( phoneNumberOnlyCachedAddresses: &cacheState.phoneNumberOnlyLegacyCachedAddresses, serviceIdCachedAddresses: &cacheState.serviceIdCachedAddresses, phoneNumber: phoneNumber, serviceId: newPotentiallyVisibleServiceIds.first ) // Other addresses can only be resolved with a visible ServiceId. updatePhoneNumberOnlyAddresses( phoneNumberOnlyCachedAddresses: &cacheState.phoneNumberOnlyCachedAddresses, serviceIdCachedAddresses: &cacheState.serviceIdCachedAddresses, phoneNumber: phoneNumber, serviceId: newPotentiallyVisibleServiceIds.first(where: { $0.isVisible }) ) } } /// If we're adding a ServiceId to this recipient for the first time, we may /// have some addresses with only a phone number. We should add the "best" /// ServiceId available to those addresses. Once we add a ServiceId, that /// value is "sticky" and won't be changed if we get an even better /// identifier in the future. This maintains the existing (very useful) /// invariant that a ServiceId for a SignalServiceAddress remains stable. private func updatePhoneNumberOnlyAddresses( phoneNumberOnlyCachedAddresses: inout [String: [CachedAddress]], serviceIdCachedAddresses: inout [ServiceId: [CachedAddress]], phoneNumber: String, serviceId: PotentiallyVisible? ) { guard let serviceId else { return } phoneNumberOnlyCachedAddresses.removeValue(forKey: phoneNumber)?.forEach { cachedAddress in cachedAddress.identifiers.update { $0.serviceId = serviceId.wrappedValue if !serviceId.isVisible { $0.phoneNumber = nil } } // This address has a serviceId now -- track that serviceId for future updates. serviceIdCachedAddresses[serviceId.wrappedValue, default: []].append(cachedAddress) } } fileprivate func registerAddress(proposedIdentifiers: CachedAddress.Identifiers, isLegacyPhoneNumber: Bool) -> CachedAddress { state.update { cacheState in let resolvedIdentifiers = resolveIdentifiers( proposedIdentifiers, isLegacyPhoneNumber: isLegacyPhoneNumber, cacheState: cacheState ) // We try our best to share hash values for ServiceIds and phone numbers // that might be associated with one another. let hashValue = hashValue( cacheState: &cacheState, serviceId: resolvedIdentifiers.serviceId, phoneNumber: resolvedIdentifiers.phoneNumber ) func getOrCreateCachedAddress(key: T, in cachedAddresses: inout [T: [CachedAddress]]) -> CachedAddress { for cachedAddress in cachedAddresses[key, default: []] { if cachedAddress.hashValue == hashValue && cachedAddress.identifiers.get() == resolvedIdentifiers { return cachedAddress } } let result = CachedAddress(hashValue: hashValue, identifiers: resolvedIdentifiers) cachedAddresses[key, default: []].append(result) return result } if let serviceId = resolvedIdentifiers.serviceId { return getOrCreateCachedAddress(key: serviceId, in: &cacheState.serviceIdCachedAddresses) } if let phoneNumber = resolvedIdentifiers.phoneNumber { if isLegacyPhoneNumber { return getOrCreateCachedAddress(key: phoneNumber, in: &cacheState.phoneNumberOnlyLegacyCachedAddresses) } else { return getOrCreateCachedAddress(key: phoneNumber, in: &cacheState.phoneNumberOnlyCachedAddresses) } } return CachedAddress(hashValue: hashValue, identifiers: resolvedIdentifiers) } } /// Populates missing/stale identifiers populated from the cache. /// /// - Parameter isLegacyPhoneNumber: If true, phone numbers can be resolved /// to hidden ACIs. This ensures legacy values (eg, receipts for old /// messages) will continue to associate with the correct account. In these /// cases, the returned identifiers won't contain the proposed phone number. /// If false, phone numbers can't be resolved to hidden ACIs (but they can /// be resolved to PNIs which are always visible to phone numbers). private func resolveIdentifiers( _ proposedIdentifiers: CachedAddress.Identifiers, isLegacyPhoneNumber: Bool, cacheState: CacheState ) -> CachedAddress.Identifiers { var resolvedIdentifiers = proposedIdentifiers resolveServiceId( in: &resolvedIdentifiers, isLegacyPhoneNumber: isLegacyPhoneNumber, cacheState: cacheState ) resolvePhoneNumber(in: &resolvedIdentifiers, cacheState: cacheState) return resolvedIdentifiers } private func resolveServiceId( in identifiers: inout CachedAddress.Identifiers, isLegacyPhoneNumber: Bool, cacheState: CacheState ) { guard identifiers.serviceId == nil, let phoneNumber = identifiers.phoneNumber else { return } for serviceId in (cacheState.phoneNumberToServiceIds[phoneNumber] ?? []) { if serviceId.isVisible { identifiers.serviceId = serviceId.wrappedValue return } // A "legacy" value can be resolved, but we know it's hidden (because the // prior check didn't pass), so clear the phone number. if isLegacyPhoneNumber { identifiers.serviceId = serviceId.wrappedValue identifiers.phoneNumber = nil return } } } private func resolvePhoneNumber( in identifiers: inout CachedAddress.Identifiers, cacheState: CacheState ) { guard identifiers.phoneNumber == nil, let serviceId = identifiers.serviceId else { return } if let phoneNumber = cacheState.serviceIdToPhoneNumber[serviceId] { if phoneNumber.isVisible { identifiers.phoneNumber = phoneNumber.wrappedValue return } // Unlike the prior method, we don't need special handling for // isLegacyPhoneNumber in this case. The goal for "legacy" values is to // resolve to an ACI, but if we reach this point, `identifier.serviceId` is // an ACI (because PNIs don't set isVisible to false) and // `identifier.phoneNumber` is nil. } } /// Finds the best hash value for (serviceId, phoneNumber). /// /// In general, we'll return an existing hash value if one exists, and we'll /// generate a new random hash value if one doesn't exist. If we generate a /// random hash value, we associate it with the provided identifier(s) for /// future calls to this method. /// /// Some edge cases worth documenting explicitly: /// /// - If both identifiers are nonnil and have different hash values, the /// hash value for `serviceId` will be returned. The hash value for /// `phoneNumber` won't be updated for future calls to this method. /// /// - If both identifiers are nonnil and only one of them has a hash value, /// that value will be returned. The returned hash value will also be /// associated with the other identifier for future calls to this method. /// /// - If both identifiers are nil, this method is equivalent to /// `Int.random(in: Int.min...Int.max)`. An address must have at least one /// identifier to be considered valid, and addresses without identifiers /// always return `false` from `isEqual:`, so it's perfectly acceptable for /// each of these addresses to have its own hash value. private func hashValue(cacheState: inout CacheState, serviceId: ServiceId?, phoneNumber: String?) -> Int { let hashValue = ( serviceId.flatMap { cacheState.serviceIdHashValues[$0] } ?? phoneNumber.flatMap { cacheState.phoneNumberHashValues[$0] } ?? Int.random(in: Int.min...Int.max) ) // We *never* change a hash value once it's been generated. if let serviceId, cacheState.serviceIdHashValues[serviceId] == nil { cacheState.serviceIdHashValues[serviceId] = hashValue } if let phoneNumber, cacheState.phoneNumberHashValues[phoneNumber] == nil { cacheState.phoneNumberHashValues[phoneNumber] = hashValue } return hashValue } } // MARK: - Unit Tests #if TESTABLE_BUILD extension SignalServiceAddress { static func randomForTesting() -> SignalServiceAddress { SignalServiceAddress(Aci.randomForTesting()) } static func isolatedRandomForTesting() -> SignalServiceAddress { SignalServiceAddress( serviceId: Aci.randomForTesting(), phoneNumber: nil, cache: SignalServiceAddressCache() ) } static func isolatedForTesting( serviceId: ServiceId? = nil, phoneNumber: String? = nil ) -> SignalServiceAddress { SignalServiceAddress( serviceId: serviceId, phoneNumber: phoneNumber, cache: SignalServiceAddressCache() ) } } extension SignalServiceAddressCache { func makeAddress(serviceId: ServiceId?, phoneNumber: E164?) -> SignalServiceAddress { SignalServiceAddress( serviceId: serviceId, phoneNumber: phoneNumber?.stringValue, cache: self ) } } #endif