Skip to content

Commit f770543

Browse files
authored
Replace CRC-32 with SHA-256 in our macro target (#1047)
This PR replaces CRC-32 with SHA-256 in our macro target. We currently use CRC-32 to disambiguate test functions that would otherwise generate identical derived symbol names (using `context.makeUniqueName()`.) By replacing it with the first 64 bits of a SHA-256 hash, we reduce the odds of a collision. We also currently use a 128-bit random number as a unique ID for exit tests during macro expansion (which is as unique as it gets, but unstable.) Replacing this random number with half of a SHA-256 hash allows us to generate _stable_ IDs (that are still statistically unique) which can help when debugging an exit test and may also improve cache quality for tools (e.g. an IDE's symbol cache.) Because there is no SHA-256 implementation available in the Swift standard library or other components we can reliably link to in the macro target, I've borrowed the implementation of SHA-256 in swift-tools-support-core. The original version is [here](https://github.com/swiftlang/swift-tools-support-core/blob/main/Sources/TSCBasic/HashAlgorithms.swift). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 0e3bdfd commit f770543

File tree

6 files changed

+245
-88
lines changed

6 files changed

+245
-88
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,20 @@ private import _TestingInternals
3535
#endif
3636
public struct ExitTest: Sendable, ~Copyable {
3737
/// A type whose instances uniquely identify instances of ``ExitTest``.
38+
///
39+
/// An instance of this type uniquely identifies an exit test within the
40+
/// context of the current test target. You can get an exit test's unique
41+
/// identifier from its ``id`` property.
42+
///
43+
/// The encoded form of an instance of this type is subject to change over
44+
/// time. Instances of this type are only guaranteed to be decodable by the
45+
/// same version of the testing library that encoded them.
3846
@_spi(ForToolsIntegrationOnly)
3947
public struct ID: Sendable, Equatable, Codable {
40-
/// An underlying UUID (stored as two `UInt64` values to avoid relying on
41-
/// `UUID` from Foundation or any platform-specific interfaces.)
48+
/// Storage for the underlying bits of the ID.
49+
///
50+
/// - Note: On Apple platforms, we deploy to OS versions that do not include
51+
/// support for `UInt128`, so we use two `UInt64`s for storage instead.
4252
private var _lo: UInt64
4353
private var _hi: UInt64
4454

Sources/TestingMacros/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ target_sources(TestingMacros PRIVATE
9999
Support/AvailabilityGuards.swift
100100
Support/CommentParsing.swift
101101
Support/ConditionArgumentParsing.swift
102-
Support/CRC32.swift
103102
Support/DiagnosticMessage.swift
104103
Support/DiagnosticMessage+Diagnosing.swift
104+
Support/SHA256.swift
105105
Support/SourceCodeCapturing.swift
106106
Support/SourceLocationGeneration.swift
107107
Support/TestContentGeneration.swift

Sources/TestingMacros/ConditionMacro.swift

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11+
import SwiftParser
1112
public import SwiftSyntax
1213
import SwiftSyntaxBuilder
1314
public import SwiftSyntaxMacros
@@ -446,9 +447,8 @@ extension ExitTestConditionMacro {
446447
context.diagnose(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro))
447448
}
448449

449-
// TODO: use UUID() here if we can link to Foundation
450-
let exitTestID = (UInt64.random(in: 0 ... .max), UInt64.random(in: 0 ... .max))
451-
let exitTestIDExpr: ExprSyntax = "(\(literal: exitTestID.0), \(literal: exitTestID.1))"
450+
// Generate a unique identifier for this exit test.
451+
let idExpr = _makeExitTestIDExpr(for: macro, in: context)
452452

453453
var decls = [DeclSyntax]()
454454

@@ -494,7 +494,7 @@ extension ExitTestConditionMacro {
494494
enum \(enumName) {
495495
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint, _ in
496496
Testing.ExitTest.__store(
497-
\(exitTestIDExpr),
497+
\(idExpr),
498498
\(bodyThunkName),
499499
into: outValue,
500500
asTypeAt: type,
@@ -525,10 +525,7 @@ extension ExitTestConditionMacro {
525525
// Insert the exit test's ID as the first argument. Note that this will
526526
// invalidate all indices into `arguments`!
527527
arguments.insert(
528-
Argument(
529-
label: "identifiedBy",
530-
expression: exitTestIDExpr
531-
),
528+
Argument(label: "identifiedBy", expression: idExpr),
532529
at: arguments.startIndex
533530
)
534531

@@ -541,6 +538,44 @@ extension ExitTestConditionMacro {
541538

542539
return try Base.expansion(of: macro, primaryExpression: bodyArgumentExpr, in: context)
543540
}
541+
542+
/// Make an expression representing an exit test ID that can be passed to the
543+
/// `ExitTest.__store()` function at runtime.
544+
///
545+
/// - Parameters:
546+
/// - macro: The exit test macro being inspected.
547+
/// - context: The macro context in which the expression is being parsed.
548+
///
549+
/// - Returns: An expression representing the exit test's unique ID.
550+
private static func _makeExitTestIDExpr(
551+
for macro: some FreestandingMacroExpansionSyntax,
552+
in context: some MacroExpansionContext
553+
) -> ExprSyntax {
554+
let exitTestID: (UInt64, UInt64)
555+
if let sourceLocation = context.location(of: macro, at: .afterLeadingTrivia, filePathMode: .fileID),
556+
let fileID = sourceLocation.file.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
557+
let line = sourceLocation.line.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue,
558+
let column = sourceLocation.column.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue {
559+
// Hash the entire source location and store as many bits as possible in
560+
// the resulting ID.
561+
let stringValue = "\(fileID):\(line):\(column)"
562+
exitTestID = SHA256.hash(stringValue.utf8).withUnsafeBytes { sha256 in
563+
sha256.loadUnaligned(as: (UInt64, UInt64).self)
564+
}
565+
} else {
566+
// This branch is dead code in production, but is used when we expand a
567+
// macro in our own unit tests because the macro expansion context does
568+
// not have real source location information.
569+
exitTestID.0 = .random(in: 0 ... .max)
570+
exitTestID.1 = .random(in: 0 ... .max)
571+
}
572+
573+
// Return a tuple of integer literals (which is what the runtime __store()
574+
// function is expecting.)
575+
return """
576+
(\(IntegerLiteralExprSyntax(exitTestID.0, radix: .hex)), \(IntegerLiteralExprSyntax(exitTestID.1, radix: .hex)))
577+
"""
578+
}
544579
}
545580

546581
/// A type describing the expansion of the `#expect(exitsWith:)` macro.

Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ extension MacroExpansionContext {
6363
.tokens(viewMode: .fixedUp)
6464
.map(\.textWithoutBackticks)
6565
.joined()
66-
let crcValue = crc32(identifierCharacters.utf8)
67-
let suffix = String(crcValue, radix: 16, uppercase: false)
66+
let hashValue = SHA256.hash(identifierCharacters.utf8).withUnsafeBytes { sha256 in
67+
sha256.loadUnaligned(as: UInt64.self)
68+
}
69+
let suffix = String(hashValue, radix: 16, uppercase: false)
6870

69-
// If the caller did not specify a prefix and the CRC32 value starts with a
71+
// If the caller did not specify a prefix and the hash value starts with a
7072
// digit, include a single-character prefix to ensure that Swift's name
7173
// demangling still works correctly.
7274
var prefix = prefix

Sources/TestingMacros/Support/CRC32.swift

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2014–2025 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
/// The contents of this file were copied more-or-less verbatim from
12+
/// [swift-tools-support-core](https://github.com/swiftlang/swift-tools-support-core/blob/add9e1518ac37a8e52b7612d3eb2f009ae8f6ce8/Sources/TSCBasic/HashAlgorithms.swift).
13+
14+
/// SHA-256 implementation from Secure Hash Algorithm 2 (SHA-2) set of
15+
/// cryptographic hash functions (FIPS PUB 180-2).
16+
enum SHA256 {
17+
/// The length of the output digest (in bits).
18+
private static let _digestLength = 256
19+
20+
/// The size of each blocks (in bits).
21+
private static let _blockBitSize = 512
22+
23+
/// The initial hash value.
24+
private static let _initialHashValue: [UInt32] = [
25+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
26+
]
27+
28+
/// The constants in the algorithm (K).
29+
private static let _konstants: [UInt32] = [
30+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
31+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
32+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
33+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
34+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
35+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
36+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
37+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
38+
]
39+
40+
public static func hash(_ bytes: some Sequence<UInt8>) -> [UInt8] {
41+
var input = Array(bytes)
42+
43+
// Pad the input.
44+
_pad(&input)
45+
46+
// Break the input into N 512-bit blocks.
47+
let messageBlocks = input.blocks(size: _blockBitSize / 8)
48+
49+
/// The hash that is being computed.
50+
var hash = _initialHashValue
51+
52+
// Process each block.
53+
for block in messageBlocks {
54+
_process(block, hash: &hash)
55+
}
56+
57+
// Finally, compute the result.
58+
var result = [UInt8](repeating: 0, count: _digestLength / 8)
59+
for (idx, element) in hash.enumerated() {
60+
let pos = idx * 4
61+
result[pos + 0] = UInt8((element >> 24) & 0xff)
62+
result[pos + 1] = UInt8((element >> 16) & 0xff)
63+
result[pos + 2] = UInt8((element >> 8) & 0xff)
64+
result[pos + 3] = UInt8(element & 0xff)
65+
}
66+
67+
return result
68+
}
69+
70+
/// Process and compute hash from a block.
71+
private static func _process(_ block: ArraySlice<UInt8>, hash: inout [UInt32]) {
72+
73+
// Compute message schedule.
74+
var W = [UInt32](repeating: 0, count: _konstants.count)
75+
for t in 0..<W.count {
76+
switch t {
77+
case 0...15:
78+
let index = block.startIndex.advanced(by: t * 4)
79+
// Put 4 bytes in each message.
80+
W[t] = UInt32(block[index + 0]) << 24
81+
W[t] |= UInt32(block[index + 1]) << 16
82+
W[t] |= UInt32(block[index + 2]) << 8
83+
W[t] |= UInt32(block[index + 3])
84+
default:
85+
let σ1 = W[t-2].rotateRight(by: 17) ^ W[t-2].rotateRight(by: 19) ^ (W[t-2] >> 10)
86+
let σ0 = W[t-15].rotateRight(by: 7) ^ W[t-15].rotateRight(by: 18) ^ (W[t-15] >> 3)
87+
W[t] = σ1 &+ W[t-7] &+ σ0 &+ W[t-16]
88+
}
89+
}
90+
91+
var a = hash[0]
92+
var b = hash[1]
93+
var c = hash[2]
94+
var d = hash[3]
95+
var e = hash[4]
96+
var f = hash[5]
97+
var g = hash[6]
98+
var h = hash[7]
99+
100+
// Run the main algorithm.
101+
for t in 0..<_konstants.count {
102+
let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25)
103+
let ch = (e & f) ^ (~e & g)
104+
let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t]
105+
106+
let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22)
107+
let maj = (a & b) ^ (a & c) ^ (b & c)
108+
let t2 = Σ0 &+ maj
109+
110+
h = g
111+
g = f
112+
f = e
113+
e = d &+ t1
114+
d = c
115+
c = b
116+
b = a
117+
a = t1 &+ t2
118+
}
119+
120+
hash[0] = a &+ hash[0]
121+
hash[1] = b &+ hash[1]
122+
hash[2] = c &+ hash[2]
123+
hash[3] = d &+ hash[3]
124+
hash[4] = e &+ hash[4]
125+
hash[5] = f &+ hash[5]
126+
hash[6] = g &+ hash[6]
127+
hash[7] = h &+ hash[7]
128+
}
129+
130+
/// Pad the given byte array to be a multiple of 512 bits.
131+
private static func _pad(_ input: inout [UInt8]) {
132+
// Find the bit count of input.
133+
let inputBitLength = input.count * 8
134+
135+
// Append the bit 1 at end of input.
136+
input.append(0x80)
137+
138+
// Find the number of bits we need to append.
139+
//
140+
// inputBitLength + 1 + bitsToAppend ≡ 448 mod 512
141+
let mod = inputBitLength % 512
142+
let bitsToAppend = mod < 448 ? 448 - 1 - mod : 512 + 448 - mod - 1
143+
144+
// We already appended first 7 bits with 0x80 above.
145+
input += [UInt8](repeating: 0, count: (bitsToAppend - 7) / 8)
146+
147+
// We need to append 64 bits of input length.
148+
for byte in UInt64(inputBitLength).toByteArray().lazy.reversed() {
149+
input.append(byte)
150+
}
151+
assert((input.count * 8) % 512 == 0, "Expected padded length to be 512.")
152+
}
153+
}
154+
155+
// MARK:- Helpers
156+
157+
extension UInt64 {
158+
/// Converts the 64 bit integer into an array of single byte integers.
159+
fileprivate func toByteArray() -> [UInt8] {
160+
var value = self.littleEndian
161+
return withUnsafeBytes(of: &value, Array.init)
162+
}
163+
}
164+
165+
extension UInt32 {
166+
/// Rotates self by given amount.
167+
fileprivate func rotateRight(by amount: UInt32) -> UInt32 {
168+
return (self >> amount) | (self << (32 - amount))
169+
}
170+
}
171+
172+
extension Array {
173+
/// Breaks the array into the given size.
174+
fileprivate func blocks(size: Int) -> AnyIterator<ArraySlice<Element>> {
175+
var currentIndex = startIndex
176+
return AnyIterator {
177+
if let nextIndex = self.index(currentIndex, offsetBy: size, limitedBy: self.endIndex) {
178+
defer { currentIndex = nextIndex }
179+
return self[currentIndex..<nextIndex]
180+
}
181+
return nil
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)