Skip to content

Replace CRC-32 with SHA-256 in our macro target #1047

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

Merged
merged 3 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ private import _TestingInternals
#endif
public struct ExitTest: Sendable, ~Copyable {
/// A type whose instances uniquely identify instances of ``ExitTest``.
///
/// An instance of this type uniquely identifies an exit test within the
/// context of the current test target. You can get an exit test's unique
/// identifier from its ``id`` property.
///
/// The encoded form of an instance of this type is subject to change over
/// time. Instances of this type are only guaranteed to be decodable by the
/// same version of the testing library that encoded them.
@_spi(ForToolsIntegrationOnly)
public struct ID: Sendable, Equatable, Codable {
/// An underlying UUID (stored as two `UInt64` values to avoid relying on
/// `UUID` from Foundation or any platform-specific interfaces.)
/// Storage for the underlying bits of the ID.
///
/// - Note: On Apple platforms, we deploy to OS versions that do not include
/// support for `UInt128`, so we use two `UInt64`s for storage instead.
private var _lo: UInt64
private var _hi: UInt64

Expand Down
2 changes: 1 addition & 1 deletion Sources/TestingMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ target_sources(TestingMacros PRIVATE
Support/AvailabilityGuards.swift
Support/CommentParsing.swift
Support/ConditionArgumentParsing.swift
Support/CRC32.swift
Support/DiagnosticMessage.swift
Support/DiagnosticMessage+Diagnosing.swift
Support/SHA256.swift
Support/SourceCodeCapturing.swift
Support/SourceLocationGeneration.swift
Support/TestContentGeneration.swift
Expand Down
51 changes: 43 additions & 8 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

import SwiftParser
public import SwiftSyntax
import SwiftSyntaxBuilder
public import SwiftSyntaxMacros
Expand Down Expand Up @@ -446,9 +447,8 @@ extension ExitTestConditionMacro {
context.diagnose(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro))
}

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

var decls = [DeclSyntax]()

Expand Down Expand Up @@ -494,7 +494,7 @@ extension ExitTestConditionMacro {
enum \(enumName) {
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint, _ in
Testing.ExitTest.__store(
\(exitTestIDExpr),
\(idExpr),
\(bodyThunkName),
into: outValue,
asTypeAt: type,
Expand Down Expand Up @@ -525,10 +525,7 @@ extension ExitTestConditionMacro {
// Insert the exit test's ID as the first argument. Note that this will
// invalidate all indices into `arguments`!
arguments.insert(
Argument(
label: "identifiedBy",
expression: exitTestIDExpr
),
Argument(label: "identifiedBy", expression: idExpr),
at: arguments.startIndex
)

Expand All @@ -541,6 +538,44 @@ extension ExitTestConditionMacro {

return try Base.expansion(of: macro, primaryExpression: bodyArgumentExpr, in: context)
}

/// Make an expression representing an exit test ID that can be passed to the
/// `ExitTest.__store()` function at runtime.
///
/// - Parameters:
/// - macro: The exit test macro being inspected.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: An expression representing the exit test's unique ID.
private static func _makeExitTestIDExpr(
for macro: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
let exitTestID: (UInt64, UInt64)
if let sourceLocation = context.location(of: macro, at: .afterLeadingTrivia, filePathMode: .fileID),
let fileID = sourceLocation.file.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
let line = sourceLocation.line.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue,
let column = sourceLocation.column.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue {
// Hash the entire source location and store as many bits as possible in
// the resulting ID.
let stringValue = "\(fileID):\(line):\(column)"
exitTestID = SHA256.hash(stringValue.utf8).withUnsafeBytes { sha256 in
sha256.loadUnaligned(as: (UInt64, UInt64).self)
}
} else {
// This branch is dead code in production, but is used when we expand a
// macro in our own unit tests because the macro expansion context does
// not have real source location information.
exitTestID.0 = .random(in: 0 ... .max)
exitTestID.1 = .random(in: 0 ... .max)
}

// Return a tuple of integer literals (which is what the runtime __store()
// function is expecting.)
return """
(\(IntegerLiteralExprSyntax(exitTestID.0, radix: .hex)), \(IntegerLiteralExprSyntax(exitTestID.1, radix: .hex)))
"""
}
}

/// A type describing the expansion of the `#expect(exitsWith:)` macro.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ extension MacroExpansionContext {
.tokens(viewMode: .fixedUp)
.map(\.textWithoutBackticks)
.joined()
let crcValue = crc32(identifierCharacters.utf8)
let suffix = String(crcValue, radix: 16, uppercase: false)
let hashValue = SHA256.hash(identifierCharacters.utf8).withUnsafeBytes { sha256 in
sha256.loadUnaligned(as: UInt64.self)
}
let suffix = String(hashValue, radix: 16, uppercase: false)

// If the caller did not specify a prefix and the CRC32 value starts with a
// If the caller did not specify a prefix and the hash value starts with a
// digit, include a single-character prefix to ensure that Swift's name
// demangling still works correctly.
var prefix = prefix
Expand Down
74 changes: 0 additions & 74 deletions Sources/TestingMacros/Support/CRC32.swift

This file was deleted.

184 changes: 184 additions & 0 deletions Sources/TestingMacros/Support/SHA256.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014–2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

/// The contents of this file were copied more-or-less verbatim from
/// [swift-tools-support-core](https://github.com/swiftlang/swift-tools-support-core/blob/add9e1518ac37a8e52b7612d3eb2f009ae8f6ce8/Sources/TSCBasic/HashAlgorithms.swift).

/// SHA-256 implementation from Secure Hash Algorithm 2 (SHA-2) set of
/// cryptographic hash functions (FIPS PUB 180-2).
enum SHA256 {
/// The length of the output digest (in bits).
private static let _digestLength = 256

/// The size of each blocks (in bits).
private static let _blockBitSize = 512

/// The initial hash value.
private static let _initialHashValue: [UInt32] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
]

/// The constants in the algorithm (K).
private static let _konstants: [UInt32] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
]

public static func hash(_ bytes: some Sequence<UInt8>) -> [UInt8] {
var input = Array(bytes)

// Pad the input.
_pad(&input)

// Break the input into N 512-bit blocks.
let messageBlocks = input.blocks(size: _blockBitSize / 8)

/// The hash that is being computed.
var hash = _initialHashValue

// Process each block.
for block in messageBlocks {
_process(block, hash: &hash)
}

// Finally, compute the result.
var result = [UInt8](repeating: 0, count: _digestLength / 8)
for (idx, element) in hash.enumerated() {
let pos = idx * 4
result[pos + 0] = UInt8((element >> 24) & 0xff)
result[pos + 1] = UInt8((element >> 16) & 0xff)
result[pos + 2] = UInt8((element >> 8) & 0xff)
result[pos + 3] = UInt8(element & 0xff)
}

return result
}

/// Process and compute hash from a block.
private static func _process(_ block: ArraySlice<UInt8>, hash: inout [UInt32]) {

// Compute message schedule.
var W = [UInt32](repeating: 0, count: _konstants.count)
for t in 0..<W.count {
switch t {
case 0...15:
let index = block.startIndex.advanced(by: t * 4)
// Put 4 bytes in each message.
W[t] = UInt32(block[index + 0]) << 24
W[t] |= UInt32(block[index + 1]) << 16
W[t] |= UInt32(block[index + 2]) << 8
W[t] |= UInt32(block[index + 3])
default:
let σ1 = W[t-2].rotateRight(by: 17) ^ W[t-2].rotateRight(by: 19) ^ (W[t-2] >> 10)
let σ0 = W[t-15].rotateRight(by: 7) ^ W[t-15].rotateRight(by: 18) ^ (W[t-15] >> 3)
W[t] = σ1 &+ W[t-7] &+ σ0 &+ W[t-16]
}
}

var a = hash[0]
var b = hash[1]
var c = hash[2]
var d = hash[3]
var e = hash[4]
var f = hash[5]
var g = hash[6]
var h = hash[7]

// Run the main algorithm.
for t in 0..<_konstants.count {
let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25)
let ch = (e & f) ^ (~e & g)
let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t]

let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22)
let maj = (a & b) ^ (a & c) ^ (b & c)
let t2 = Σ0 &+ maj

h = g
g = f
f = e
e = d &+ t1
d = c
c = b
b = a
a = t1 &+ t2
}

hash[0] = a &+ hash[0]
hash[1] = b &+ hash[1]
hash[2] = c &+ hash[2]
hash[3] = d &+ hash[3]
hash[4] = e &+ hash[4]
hash[5] = f &+ hash[5]
hash[6] = g &+ hash[6]
hash[7] = h &+ hash[7]
}

/// Pad the given byte array to be a multiple of 512 bits.
private static func _pad(_ input: inout [UInt8]) {
// Find the bit count of input.
let inputBitLength = input.count * 8

// Append the bit 1 at end of input.
input.append(0x80)

// Find the number of bits we need to append.
//
// inputBitLength + 1 + bitsToAppend ≡ 448 mod 512
let mod = inputBitLength % 512
let bitsToAppend = mod < 448 ? 448 - 1 - mod : 512 + 448 - mod - 1

// We already appended first 7 bits with 0x80 above.
input += [UInt8](repeating: 0, count: (bitsToAppend - 7) / 8)

// We need to append 64 bits of input length.
for byte in UInt64(inputBitLength).toByteArray().lazy.reversed() {
input.append(byte)
}
assert((input.count * 8) % 512 == 0, "Expected padded length to be 512.")
}
}

// MARK:- Helpers

extension UInt64 {
/// Converts the 64 bit integer into an array of single byte integers.
fileprivate func toByteArray() -> [UInt8] {
var value = self.littleEndian
return withUnsafeBytes(of: &value, Array.init)
}
}

extension UInt32 {
/// Rotates self by given amount.
fileprivate func rotateRight(by amount: UInt32) -> UInt32 {
return (self >> amount) | (self << (32 - amount))
}
}

extension Array {
/// Breaks the array into the given size.
fileprivate func blocks(size: Int) -> AnyIterator<ArraySlice<Element>> {
var currentIndex = startIndex
return AnyIterator {
if let nextIndex = self.index(currentIndex, offsetBy: size, limitedBy: self.endIndex) {
defer { currentIndex = nextIndex }
return self[currentIndex..<nextIndex]
}
return nil
}
}
}