//===--- MultiStreamFile.swift - PDB support for Swift --------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 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 the list of Swift project authors // //===----------------------------------------------------------------------===// // // Provides support for reading the multi-stream-file container format on // which PDB is based. // //===----------------------------------------------------------------------===// import Swift #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) internal import Darwin #elseif os(Windows) internal import WinSDK #elseif canImport(Glibc) internal import Glibc #elseif canImport(Musl) internal import Musl #endif internal import BacktracingImpl.OS.Libc internal import BacktracingImpl.ImageFormats.PDB enum MultiStreamFileError: Error { #if os(Windows) case readFailed(error: UInt32) #else case readFailed(errno: CInt) #endif case readOffEndOfStream } class MultiStreamFile { #if os(Windows) typealias FileHandle = HANDLE #else typealias FileHandle = CInt #endif var fileHandle: FileHandle struct StreamInfo { var size: Int var pages: [Int] var valid: Bool { return size != Int(bitPattern: 0xffffffff) } } enum Kind { case v2 case v7 } var kind: Kind var pageSize: Int var freePageMapPage: Int var pageCount: Int var streams: [StreamInfo] var pageCache: [Int:UnsafeMutableRawBufferPointer] convenience init?(path: String) { #if os(Windows) guard let handle: HANDLE = win32_open(path: path) else { return nil } if handle == INVALID_HANDLE_VALUE { return nil } #else let handle: CInt = _swift_open(path, O_RDONLY, 0) if handle == -1 { return nil } #endif self.init(fileHandle: handle) } init?(fileHandle: FileHandle) { self.fileHandle = fileHandle // Read the start of the file to see what kind it is let theKind = withUnsafeTemporaryAllocation(of: CChar.self, capacity: 64) { (buffer: UnsafeMutableBufferPointer) -> Kind? in let count = safe_read(fileHandle, 0, buffer.baseAddress!, buffer.count) if count < 0x1e { return nil } if count >= 0x2c && pdb_is_v2(buffer.baseAddress!) { return .v2 } if pdb_is_v7(buffer.baseAddress!) { return .v7 } return nil } guard let theKind else { return nil } self.kind = theKind self.streams = [] self.pageCache = [:] var streamTable: StreamInfo switch kind { case .v2: let hdr = withUnsafeTemporaryAllocation(of: MSF_HDR.self, capacity: 1) { (buffer: UnsafeMutableBufferPointer) -> MSF_HDR? in let count = safe_read(fileHandle, 0, buffer.baseAddress!, MemoryLayout.size) if count != MemoryLayout.size { return nil } return buffer[0] } guard let hdr else { return nil } pageSize = Int(hdr.cbPg) freePageMapPage = Int(hdr.pnFpm) pageCount = Int(hdr.pnMac) let streamTablePageCount = (Int(hdr.siSt.cb) + pageSize - 1) / pageSize let pages = withUnsafeTemporaryAllocation(of: PN.self, capacity: streamTablePageCount) { (buffer: UnsafeMutableBufferPointer) -> Array? in let byteCount = MemoryLayout.stride * Int(streamTablePageCount) let count = safe_read(fileHandle, MemoryLayout.size, buffer.baseAddress!, byteCount) if count != byteCount { return nil } return buffer.map { Int($0) } } guard let pages else { return nil } streamTable = StreamInfo(size: Int(hdr.siSt.cb), pages: pages) case .v7: let hdr = withUnsafeTemporaryAllocation(of: BIGMSF_HDR.self, capacity: 1) { (buffer: UnsafeMutableBufferPointer) -> BIGMSF_HDR? in let count = safe_read(fileHandle, 0, buffer.baseAddress!, MemoryLayout.size) if count != MemoryLayout.size { return nil } return buffer[0] } guard let hdr else { return nil } pageSize = Int(hdr.cbPg) freePageMapPage = Int(hdr.pnFpm) pageCount = Int(hdr.pnMac) let streamTablePageCount = (Int(hdr.siSt.cb) + pageSize - 1) / pageSize let indirectTableSize = MemoryLayout.stride * streamTablePageCount let indirectPageCount = (indirectTableSize + pageSize - 1) / pageSize // The stream table's page numbers are stored indirectly here; the // array in the header is an array of pages that contain the page // numbers, rather than an array of page numbers for the stream table. let indirectPages = withUnsafeTemporaryAllocation(of: UPN.self, capacity: indirectPageCount) { (buffer: UnsafeMutableBufferPointer) -> Array? in let byteCount = MemoryLayout.stride * Int(indirectPageCount) let count = safe_read(fileHandle, MemoryLayout.size, buffer.baseAddress!, byteCount) if count != byteCount { return nil } return buffer.map { Int($0) } } guard let indirectPages else { return nil } var pages: [Int] = [] streamTable = StreamInfo(size: Int(hdr.siSt.cb), pages: pages) do { var remaining = streamTablePageCount for page in indirectPages { let chunkSize = min(remaining, pageSize / MemoryLayout.stride) try with(page: page) { let pageNumbers = Span(_bytes: $0) for n in 0...stride } case .v7: streamCount = Int( try read(stream: kStreamTable, offset: 0, as: UNSN.self) ) // This is followed by an array of byte counts, one per stream for _ in 0...stride } } for n in 1.. 0 { streams[n].pages = (try read(stream: kStreamTable, offset: offset, as: PN32.self, count: pages, mustRead: true)).map { Int($0) } offset += pages * MemoryLayout.stride } } } catch { return nil } } deinit { for (_, data) in pageCache { data.deallocate() } #if os(Windows) CloseHandle(fileHandle) #else close(fileHandle) #endif } func with( page: Int, body: (RawSpan) throws -> R ) throws -> R { if let data = pageCache[page] { return try body(data.bytes) } let data = UnsafeMutableRawBufferPointer.allocate(byteCount: self.pageSize, alignment: self.pageSize) let count = safe_read(fileHandle, page * self.pageSize, data.baseAddress!, data.count) if count != data.count { #if os(Windows) let err = GetLastError() throw MultiStreamFileError.readFailed(error: UInt32(err)) #else let err = errno throw MultiStreamFileError.readFailed(errno: err) #endif } pageCache[page] = data return try body(data.bytes) } func open(stream: Int) -> Stream? { if stream + 1 >= streams.count || stream < 0 { return nil } return Stream(parent: self, stream: stream) } struct Stream { private var parent: MultiStreamFile private var stream: Int private(set) var offset: Int = 0 internal init(parent: MultiStreamFile, stream: Int) { self.parent = parent self.stream = stream } var size: Int { return parent.sizeOf(stream: stream) } var atEnd: Bool { return offset >= size } mutating func read(as type: T.Type) throws -> T { let result = try parent.read(stream: stream, offset: offset, as: type) offset += MemoryLayout.size return result } mutating func read(as type: T.Type, count: Int) throws -> [T] { let result = try parent.read(stream: stream, offset: offset, as: type, count: count) offset += MemoryLayout.stride * result.count return result } mutating func readString() throws -> String { var bytesRead = 0 let result = try parent.readString(stream: stream, offset: offset, bytesRead: &bytesRead) offset += bytesRead return result } func pread(from offset: Int, as type: T.Type) throws -> T { return try parent.read(stream: stream, offset: offset, as: type) } func pread( from offset: Int, as type: T.Type, count: Int ) throws -> [T] { return try parent.read(stream: stream, offset: offset, as: type, count: count) } func preadString(from offset: Int) throws -> String { var bytesRead = 0 let result = try parent.readString(stream: stream, offset: offset, bytesRead: &bytesRead) return result } enum RelativeTo { case start case current case end } mutating func seek( offset: Int, relativeTo: RelativeTo = .start ) -> Int { var newOffset: Int switch relativeTo { case .start: newOffset = offset case .current: newOffset = self.offset + offset case .end: newOffset = size + offset } if newOffset < 0 { newOffset = 0 } else if newOffset > size { newOffset = size } self.offset = newOffset return newOffset } mutating func align(toMultiple alignment: Int) -> Int { let extra = self.offset % alignment if extra == 0 { return self.offset } var newOffset = self.offset + alignment - extra if newOffset > size { newOffset = size } self.offset = newOffset return newOffset } } func sizeOf(stream: Int) -> Int { return streams[stream + 1].size } func read(stream: Int, offset: Int, as type: T.Type) throws -> T { return try read( stream: stream, offset: offset, as: type, count: 1, mustRead: true )[0] } func read( stream: Int, offset: Int, as type: T.Type, count: Int, mustRead: Bool = false ) throws -> [T] { let streamInfo = streams[stream + 1] var pageNdx = offset / pageSize var offsetInPage = offset - pageNdx * pageSize var theCount = count if offset > streamInfo.size { theCount = 0 } else { let remaining = streamInfo.size - offset if MemoryLayout.stride * count <= remaining { theCount = count } else if mustRead { throw MultiStreamFileError.readOffEndOfStream } else { theCount = remaining / MemoryLayout.stride } } return try withUnsafeTemporaryAllocation(of: T.self, capacity: theCount) { let rawBuffer = UnsafeMutableRawBufferPointer($0) var bytesLeft = MemoryLayout.stride * $0.count var offset = 0 while bytesLeft > 0 { let chunkSize = min(bytesLeft, pageSize - offsetInPage) let page = streamInfo.pages[pageNdx] try with(page: page) { data in for n in 0.. String { var result = "" let streamInfo = streams[stream + 1] var pageNdx = offset / pageSize var offsetInPage = offset - pageNdx * pageSize bytesRead = 0 if offset > streamInfo.size { throw MultiStreamFileError.readOffEndOfStream } while pageNdx < streamInfo.pages.count { let page = streamInfo.pages[pageNdx] var end = offsetInPage try with(page: page) { $0.withUnsafeBytes { data in while end < pageSize && data[end] != 0 { end += 1 } let chunk = String(decoding: data[offsetInPage..= streamInfo.pages.count { throw MultiStreamFileError.readOffEndOfStream } return result } func dump(stream: Int) throws { let streamInfo = streams[stream + 1] var offset = 0 func hex(_ x: T, width: Int) -> String { let asHex = String(x, radix: 16) let length = asHex.count if length < width { let padding = String(repeating: "0", count: width - length) return padding + asHex } return asHex } while offset < streamInfo.size { let bytes = try read(stream: stream, offset: offset, as: UInt8.self, count: 16) var line = "\(hex(offset, width: 8)):" for byte in bytes { line += " \(hex(byte, width: 2))" } line += String(repeating: " ", count: 60 - line.count) for byte in bytes { if byte >= 32 && byte < 0x7f { line.append(Character(UnicodeScalar(byte))) } else { line += "." } } offset += bytes.count print(line) } } } extension MultiStreamFile: CustomStringConvertible { var description: String { return "MultiStreamFile { kind = \(kind), page size = \(pageSize), page count = \(pageCount), streams = \(streams.count - 1) }" } } #if os(Windows) fileprivate func safe_read( _ handle: MultiStreamFile.FileHandle, _ offset: Int, _ buffer: UnsafeMutableRawPointer?, _ count: Int ) -> Int { var ovl = OVERLAPPED() ovl.Offset = DWORD(truncatingIfNeeded: offset) ovl.OffsetHigh = DWORD(truncatingIfNeeded: offset >> 32) var bytesRead = DWORD(0) let bRet = ReadFile(handle, buffer, DWORD(count), &bytesRead, &ovl) if !bRet { return -1 } return Int(bytesRead) } #else fileprivate func safe_read( _ handle: MultiStreamFile.FileHandle, _ offset: Int, _ buffer: UnsafeMutableRawPointer?, _ count: Int ) -> Int { while true { let ret = pread(handle, buffer, count, off_t(offset)) if ret >= 0 || (ret < 0 && errno != EINTR) { return ret } } } #endif #if os(Windows) fileprivate func win32_open(path: String) -> HANDLE? { return path.withCString(encodedAs: UTF16.self) { lpwszPath in CreateFileW(lpwszPath, GENERIC_READ, DWORD(FILE_SHARE_READ), nil, DWORD(OPEN_EXISTING), DWORD(FILE_ATTRIBUTE_NORMAL), nil) } } #endif