diff --git a/Package.swift b/Package.swift index ba46203..f4e53f1 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ let package = Package( .package( url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), .package( - url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0"), + url: "https://github.com/swiftlang/swift-syntax.git", from: "603.0.0"), .package( url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.6.4"), diff --git a/Sources/BinaryParsing/Parsers/String.swift b/Sources/BinaryParsing/Parsers/String.swift index 28aae71..6897c11 100644 --- a/Sources/BinaryParsing/Parsers/String.swift +++ b/Sources/BinaryParsing/Parsers/String.swift @@ -60,10 +60,18 @@ extension String { internal init(_uncheckedParsingUTF16 input: inout ParserSpan) throws(ParsingError) { + assert(input.count.isMultiple(of: 2)) let stringBytes = input.divide(at: input.endPosition) self = unsafe stringBytes.withUnsafeBytes { buffer in - let utf16Buffer = unsafe buffer.assumingMemoryBound(to: UInt16.self) - return unsafe String(decoding: utf16Buffer, as: UTF16.self) + guard let base = buffer.baseAddress else { return "" } + if base._isAligned(for: UInt16.self) { + let utf16Buffer = unsafe buffer.assumingMemoryBound(to: UInt16.self) + return unsafe String(decoding: utf16Buffer, as: UTF16.self) + } else { + let utf16Buffer = unsafe _UnalignedUnsafeBufferPointer( + _base: base, _count: buffer.count / 2) + return unsafe String(decoding: utf16Buffer, as: UTF16.self) + } } } @@ -104,3 +112,48 @@ extension String { unsafe try self.init(_uncheckedParsingUTF16: &slice) } } + +// MARK: - Unaligned buffer pointer + +extension UnsafeRawPointer { + @_alwaysEmitIntoClient + @safe + func _isAligned(for: T.Type) -> Bool { + Int(bitPattern: self) & (MemoryLayout.alignment - 1) == 0 + } +} + +@unsafe +@usableFromInline +struct _UnalignedUnsafeBufferPointer { + @usableFromInline + typealias Element = T + @usableFromInline + typealias Index = Int + + @usableFromInline + var _base: UnsafeRawPointer + @usableFromInline + @safe var _count: Int + + @_alwaysEmitIntoClient + init(_base: UnsafeRawPointer, _count: Int) { + unsafe self._base = _base + self._count = _count + } +} + +extension _UnalignedUnsafeBufferPointer: @unsafe RandomAccessCollection { + @_alwaysEmitIntoClient + var startIndex: Int { 0 } + @_alwaysEmitIntoClient + var endIndex: Int { _count } + + @_alwaysEmitIntoClient + subscript(position: Int) -> T { + get { + unsafe _base.loadUnaligned( + fromByteOffset: position * MemoryLayout.stride, as: T.self) + } + } +} diff --git a/Tests/BinaryParsingTests/StringParsingTests.swift b/Tests/BinaryParsingTests/StringParsingTests.swift index c771516..7f17ebe 100644 --- a/Tests/BinaryParsingTests/StringParsingTests.swift +++ b/Tests/BinaryParsingTests/StringParsingTests.swift @@ -212,6 +212,20 @@ struct StringParsingTests { } } + @Test + func parseUTF16Unaligned() throws { + // Prepend a single byte so that the UTF-16 code units that follow start + // at an odd offset within the backing buffer, forcing a misaligned load. + let buffer: [UInt8] = [0xAA] + testStringNonASCII.utf16Buffer + + try buffer.withParserSpan { span in + _ = try UInt8(parsing: &span) + let str = try String(parsingUTF16: &span) + #expect(str == testStringNonASCII) + #expect(span.count == 0) + } + } + @Test func testMultipleOperationsOnSameBuffer() throws { let combinedString = "\(testString)\0\(testStringNonASCII)"