Files
purchases-ios/Sources/FoundationExtensions/String+Extensions.swift
T
Antonio Pallares e9e5f791cb Show redacted Test Api key in alert when detected in Release configuration (#5897)
* Include redacted version of Test Store API key in alert and log when running in Release configuration

* Add unit tests for redacting API keys

* Support redaction in old api keys with no prefix

* Fix lint

* Use constants instead of magic numbers
2025-12-02 20:40:34 +01:00

134 lines
3.7 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// String+Extensions.swift
//
// Created by Juanpe Catalán on 9/7/21.
//
import Foundation
extension String {
func rot13() -> String {
ROT13.string(self)
}
var trimmedAndEscaped: String {
return self
.trimmingWhitespacesAndNewLines
.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
}
/// Returns `nil` if `self` is an empty string.
var notEmpty: String? {
return self.isEmpty
? nil
: self
}
/// Returns `nil` if `self` is an empty string or it only contains whitespaces.
var notEmptyOrWhitespaces: String? {
return self.trimmingWhitespacesAndNewLines.isEmpty
? nil
: self
}
/// Returns `true` if it contains anything other than whitespaces.
var isNotEmpty: Bool {
return self.notEmptyOrWhitespaces != nil
}
var trimmingWhitespacesAndNewLines: String {
return self.trimmingCharacters(in: .whitespacesAndNewlines)
}
var asData: Data {
return Data(self.utf8)
}
func countOccurences(of character: Character) -> Int {
return self.reduce(0) {
return $1 == character ? $0 + 1 : $0
}
}
private static let remainderStartLength = 2
private static let remainderEndLength = 4
private static let redactionPlaceholder = "********"
var asRedactedAPIKey: String {
let prefix: String.SubSequence
let remainder: String.SubSequence
if let underscoreIndex = self.firstIndex(of: "_") {
// Prefix including underscore
prefix = self[..<self.index(after: underscoreIndex)]
// Remainder after underscore
remainder = self[self.index(after: underscoreIndex)...]
} else {
// Legacy API keys without a prefix
prefix = ""
remainder = self[...]
}
// If fewer than 6 characters to redact → return unredacted
let minimumLengthToRedact = Self.remainderStartLength + Self.remainderEndLength
guard remainder.count >= minimumLengthToRedact else {
return self
}
// Take first 2 and last 4 characters
let start = remainder.prefix(Self.remainderStartLength)
let end = remainder.suffix(Self.remainderEndLength)
return "\(prefix)\(start)********\(end)"
}
}
// MARK: -
internal extension Optional where Wrapped == String {
/// Returns `nil` if `self` is an empty string.
var notEmpty: String? {
return self.flatMap { $0.notEmpty }
}
}
private enum ROT13 {
private static let key: [Character: Character] = {
let size = Self.lowercase.count
let halfSize: Int = size / 2
var result: [Character: Character] = .init(minimumCapacity: size)
for number in 0 ..< size {
let index = (number + halfSize) % size
result[Self.uppercase[number]] = Self.uppercase[index]
result[Self.lowercase[number]] = Self.lowercase[index]
}
return result
}()
private static let lowercase: [Character] = Array("abcdefghijklmnopqrstuvwxyz")
// swiftlint:disable:next force_unwrapping
private static let uppercase: [Character] = Self.lowercase.map { $0.uppercased().first! }
fileprivate static func string(_ string: String) -> String {
let transformed = string.map { Self.key[$0] ?? $0 }
return String(transformed)
}
}