Created
February 27, 2023 01:55
-
-
Save mokagio/69965c42df77b79f21096d0a82410938 to your computer and use it in GitHub Desktop.
Two methods to create random strings in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension String { | |
enum RandomStringGenerationError: Error { | |
case secRandomCopyBytesFailed(status: Int32) | |
} | |
/// Returns a cryptographically secure string generated with characters from the given `Set<Character>` and with length | |
/// `length`. | |
/// | |
/// - Complexity: O(n) where n is the given `length`. | |
static func secureRandomString(using characters: Set<Character>, withLength length: Int) throws -> String { | |
let allowedCharactersCount = UInt32(characters.count) | |
var randomBytes = [UInt8](repeating: 0, count: length) | |
// Use `SecRandomCopyBytes` to generate cryptographically secure random bytes to use as | |
// offsets to create the random string. | |
let status = SecRandomCopyBytes(kSecRandomDefault, length, &randomBytes) | |
guard status == errSecSuccess else { | |
throw RandomStringGenerationError.secRandomCopyBytesFailed(status: status) | |
} | |
return randomBytes.reduce("") { accumulator, randomByte in | |
let randomOffset = Int(randomByte) % Int(allowedCharactersCount) | |
let randomCharacter = characters[characters.index(characters.startIndex, offsetBy: randomOffset)] | |
return accumulator + String(randomCharacter) | |
} | |
} | |
/// Returns a random string generated with characters from the given `Set<Character>` and with length `length`. | |
/// | |
/// - Complexity: O(n) where n is the given `length`. | |
static func randomString(using characters: Set<Character>, withLength length: Int) -> String { | |
let allowedCharactersCount = characters.count | |
let startIndex = characters.startIndex | |
return (0..<length).reduce("") { accumulator, _ in | |
let randomOffset = Int.random(in: 0..<allowedCharactersCount) | |
let randomCharacter = characters[characters.index(startIndex, offsetBy: randomOffset)] | |
return accumulator + String(randomCharacter) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
class StringRandomTests: XCTestCase { | |
func testSecureRandomStringLengthIsAsRequested() throws { | |
XCTAssertEqual( | |
try XCTUnwrap(String.secureRandomString(using: Set("abc"), withLength: 10)).count, | |
10 | |
) | |
} | |
func testSecureRandomStringUsesGivenCharactersOnly() throws { | |
// Using length 300 with 3 characters be relatively sure sure we'll get all of the | |
// characters at least once. | |
let randomString = try XCTUnwrap(String.secureRandomString(using: Set("abc"), withLength: 30)) | |
XCTAssertEqual( | |
CharacterSet(charactersIn: randomString), | |
CharacterSet(charactersIn: "abc") | |
) | |
} | |
func testSecureRandomStringsAreDifferent() throws { | |
let characters = Set("abcdefghijklmnopqrstuvwxyz") | |
let length = 100 | |
// It is _possible_ for the strings to be equal. However, it should be so unlikely that a | |
// test failure will most definitely mean an error in the code, rather than a legit case of | |
// the same random string being generated twice. | |
XCTAssertNotEqual( | |
try String.secureRandomString(using: characters, withLength: length), | |
try String.secureRandomString(using: characters, withLength: length) | |
) | |
} | |
// MARK: - | |
func testRandomStringLengthIsAsRequested() { | |
XCTAssertEqual( | |
String.randomString(using: Set("abc"), withLength: 10).count, | |
10 | |
) | |
} | |
func testRandomStringUsesGivenCharactersOnly() { | |
// Using length 300 with 3 characters be relatively sure sure we'll get all of the | |
// characters at least once. | |
let randomString = String.randomString(using: Set("abc"), withLength: 30) | |
XCTAssertEqual( | |
CharacterSet(charactersIn: randomString), | |
CharacterSet(charactersIn: "abc") | |
) | |
} | |
func testRandomStringsAreDifferent() { | |
let characters = Set("abcdefghijklmnopqrstuvwxyz") | |
let length = 100 | |
// It is _possible_ for the strings to be equal. However, it should be so unlikely that a | |
// test failure will most definitely mean an error in the code, rather than a legit case of | |
// the same random string being generated twice. | |
XCTAssertNotEqual( | |
String.randomString(using: characters, withLength: length), | |
String.randomString(using: characters, withLength: length) | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment