1+ //===--- BacktraceJSONFormatter.swift -------------------------*- swift -*-===//
2+ //
3+ // This source file is part of the Swift.org open source project
4+ //
5+ // Copyright (c) 2025 Apple Inc. and the Swift project authors
6+ // Licensed under Apache License v2.0 with Runtime Library Exception
7+ //
8+ // See https://swift.org/LICENSE.txt for license information
9+ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+ //
11+ //===----------------------------------------------------------------------===//
12+ //
13+ // Provides functionality to format JSON backtraces.
14+ //
15+ //===----------------------------------------------------------------------===//
16+
17+ import Swift
18+
19+ @_spi ( Formatting)
20+ public struct BacktraceJSONFormatterOptions : OptionSet {
21+ public let rawValue : Int
22+
23+ public init ( rawValue: Int ) {
24+ self . rawValue = rawValue
25+ }
26+
27+ public static let allRegisters = BacktraceJSONFormatterOptions ( rawValue: 1 <<0 )
28+ public static let demangle = BacktraceJSONFormatterOptions ( rawValue: 1 <<1 )
29+ public static let sanitize = BacktraceJSONFormatterOptions ( rawValue: 1 <<2 )
30+ public static let mentionedImages = BacktraceJSONFormatterOptions ( rawValue: 1 <<3 )
31+ public static let allThreads = BacktraceJSONFormatterOptions ( rawValue: 1 <<4 )
32+ public static let images = BacktraceJSONFormatterOptions ( rawValue: 1 <<5 )
33+ }
34+
35+ internal extension BacktraceJSONFormatterOptions {
36+ // this is not the form we want but will do for now
37+ // we are going to turn this into an OptionSet or similar
38+ var showAllRegisters : Bool { contains ( . allRegisters) }
39+ var shouldDemangle : Bool { contains ( . demangle) }
40+ var shouldSanitize : Bool { contains ( . sanitize) }
41+ var showMentionedImages : Bool { contains ( . mentionedImages) }
42+ var showAllThreads : Bool { contains ( . allThreads) }
43+ var showImages : Bool { contains ( . images) }
44+ }
45+
46+ @_spi ( Formatting)
47+ public protocol BacktraceJSONWriter {
48+ func write( _ string: String , flush: Bool )
49+ func writeln( _ string: String , flush: Bool )
50+ }
51+
52+ @_spi ( Formatting)
53+ public struct BacktraceJSONFormatter <
54+ Address: FixedWidthInteger ,
55+ Writer: BacktraceJSONWriter > {
56+
57+ var writer : Writer
58+ var options : BacktraceJSONFormatterOptions
59+
60+ typealias Log = CrashLog < Address >
61+
62+ var crashLog : Log
63+ var imageMap : ImageMap ?
64+
65+ var mentionedImages : Set < Int > = [ ]
66+
67+ @_spi ( Formatting)
68+ public init (
69+ crashLog: CrashLog < Address > ,
70+ writer: Writer ,
71+ options: BacktraceJSONFormatterOptions )
72+ {
73+ self . crashLog = crashLog
74+ self . writer = writer
75+ self . options = options
76+ self . imageMap = crashLog. imageMap ( )
77+ }
78+ }
79+
80+ internal extension BacktraceJSONFormatter {
81+ func write( _ string: String , flush: Bool ) {
82+ writer. write ( string, flush: flush)
83+ }
84+ func writeln( _ string: String , flush: Bool ) {
85+ writer. writeln ( string, flush: flush)
86+ }
87+
88+ func getDescription( ) -> String ? {
89+ crashLog. description
90+ }
91+
92+ func getArchitecture( ) -> String ? {
93+ crashLog. architecture
94+ }
95+
96+ func getPlatform( ) -> String ? {
97+ crashLog. platform
98+ }
99+
100+ func getFaultAddress( ) -> String ? {
101+ crashLog. faultAddress
102+ }
103+ }
104+
105+ @_spi ( Formatting)
106+ public extension BacktraceJSONFormatter {
107+ mutating func writeCrashLog( now: String ) {
108+ writePreamble ( now: now)
109+ writeThreads ( )
110+ writeCapturedMemory ( )
111+ writeImages ( )
112+ writeFooter ( )
113+ }
114+ }
115+
116+ @_spi ( Formatting)
117+ public extension BacktraceJSONFormatter {
118+ func writePreamble( now: String ) {
119+ guard let description = getDescription ( ) ,
120+ let faultAddress = getFaultAddress ( ) ,
121+ let platform = getPlatform ( ) ,
122+ let architecture = getArchitecture ( ) else { return }
123+
124+ write ( """
125+ { \
126+ " timestamp " : " \( now) " , \
127+ " kind " : " crashReport " , \
128+ " description " : " \( escapeJSON ( description) ) " , \
129+ " faultAddress " : " \( faultAddress) " , \
130+ " platform " : " \( escapeJSON ( platform) ) " , \
131+ " architecture " : " \( escapeJSON ( architecture) ) "
132+ """ ,
133+ flush: false )
134+ }
135+
136+ // this updates the mentionedImages and omittedImages properties
137+ // it's done here rather than in capture for efficiency, as at that
138+ // point we are not sure if we are limiting to mentioned images or not
139+ mutating func writeThreads( ) {
140+
141+ write ( #", "threads": [ "# , flush: false )
142+
143+ let threads = crashLog. threads. filter { options. showAllThreads || $0. crashed }
144+
145+ var first = true
146+ for thread in threads {
147+ if first {
148+ first = false
149+ } else {
150+ write ( " , " , flush: false )
151+ }
152+
153+ writeThread ( thread: thread)
154+ }
155+
156+ write ( " ] " , flush: false )
157+
158+ if !options. showAllThreads && crashLog. threads. count > 1 {
159+ write ( " , \" omittedThreads \" : \( crashLog. threads. count - 1 ) " , flush: false )
160+ }
161+
162+ // if omittedImages is nil, try to calculate it.
163+ if let images = crashLog. images,
164+ options. showMentionedImages,
165+ self . crashLog. omittedImages == nil {
166+
167+ self . crashLog. omittedImages = images. count - mentionedImages. count
168+
169+ }
170+ }
171+
172+ func writeCapturedMemory( ) {
173+ // suppress writing captured memory if we should sanitize
174+ if !options. shouldSanitize, let capturedMemory = crashLog. capturedMemory {
175+ write ( #", "capturedMemory": {"# , flush: false )
176+ var first = true
177+ for (address, bytes) in capturedMemory. sorted ( by: < ) {
178+ if first {
179+ first = false
180+ } else {
181+ write ( " , " , flush: false )
182+ }
183+ write ( " \" \( address) \" : \" \( bytes) \" " , flush: false )
184+ }
185+ write ( " } " , flush: false )
186+ }
187+ }
188+
189+ // note: this updates the crash log with the ommitted image count
190+ func writeImages( ) {
191+ if options. showImages, let images = crashLog. images {
192+ var imagesToWrite = images
193+
194+ if options. showMentionedImages {
195+ let mentioned =
196+ images. enumerated ( )
197+ . filter { mentionedImages. contains ( $0. 0 ) }
198+ . map { $0. 1 }
199+
200+ imagesToWrite = mentioned
201+
202+ write ( " , \" omittedImages \" : \( self . crashLog. omittedImages ?? 0 ) " ,
203+ flush: false )
204+ }
205+
206+ write ( " , \" images \" : [ " , flush: false )
207+ var first = true
208+ for image in imagesToWrite {
209+ writeImage ( image, first: first)
210+ if first {
211+ first = false
212+ }
213+ }
214+ write ( " ] " , flush: false )
215+ }
216+ }
217+
218+ func writeFooter( ) {
219+ write ( " , \" backtraceTime \" : \( crashLog. backtraceTime) " , flush: false )
220+
221+ write ( " } " , flush: false )
222+ }
223+ }
224+
225+ internal extension BacktraceJSONFormatter {
226+ func writeThreadRegisters( thread: Log . Thread ) {
227+ guard let registers = thread. registers else { return }
228+
229+ var first = true
230+ let registerOrder = HostContext . registerDumpOrder
231+ for registerName in registerOrder {
232+ if let value = registers [ registerName] {
233+ if first {
234+ first = false
235+ } else {
236+ write ( " , " , flush: false )
237+ }
238+
239+ write ( " \" \( registerName) \" : \" \( value) \" " , flush: false )
240+ }
241+ }
242+ }
243+
244+ func imageByAddress( _ address: Address ) -> Int ? {
245+ guard let address = Backtrace . Address ( address) else { return nil }
246+ return imageMap? . indexOfImage ( at: address)
247+ }
248+
249+ // note: this updates the mentioned images
250+ mutating func writeThread(
251+ thread: Log . Thread ) {
252+
253+ write ( " { " , flush: false )
254+
255+ if let name = thread. name, !name. isEmpty {
256+ write ( " \" name \" : \" \( escapeJSON ( name) ) \" , " , flush: false )
257+ }
258+
259+ let isCrashingThread = thread. crashed
260+
261+ if isCrashingThread {
262+ write ( #""crashed": true, "# , flush: false )
263+ }
264+
265+ if options. showAllRegisters || isCrashingThread {
266+ write ( #""registers": {"# , flush: false )
267+ writeThreadRegisters ( thread: thread)
268+ write ( " }, " , flush: false )
269+ }
270+
271+ write ( #""frames": ["# , flush: false )
272+ var first = true
273+ for frame in thread. frames {
274+ if first {
275+ first = false
276+ } else {
277+ write ( " , " , flush: false )
278+ }
279+
280+ write ( " { " , flush: false )
281+
282+ write ( frame. jsonBody, flush: false )
283+
284+ if frame. inlined {
285+ write ( #", "inlined": true"# , flush: false )
286+ }
287+ if frame. isSwiftRuntimeFailure {
288+ write ( #", "runtimeFailure": true"# , flush: false )
289+ }
290+ if frame. isSwiftThunk {
291+ write ( #", "thunk": true"# , flush: false )
292+ }
293+ if frame. isSystem {
294+ write ( #", "system": true"# , flush: false )
295+ }
296+
297+ if let symbol = frame. symbol {
298+ write ( """
299+ , " symbol " : " \( escapeJSON ( symbol) ) " \
300+ , " offset " : \( frame. offset ?? 0 )
301+ """ , flush: false )
302+
303+ if options. shouldDemangle, let demangledName = frame. demangledName {
304+ let formattedOffset : String
305+ if ( frame. offset ?? 0 ) > 0 {
306+ formattedOffset = " + \( frame. offset!) "
307+ } else if ( frame. offset ?? 0 ) < 0 {
308+ formattedOffset = " - \( frame. offset!) "
309+ } else {
310+ formattedOffset = " "
311+ }
312+
313+ write ( """
314+ , " description " : \" \( escapeJSON ( demangledName) ) \( formattedOffset) \"
315+ """ , flush: false )
316+ }
317+
318+ if let image = frame. image {
319+ write ( " , \" image \" : \" \( image) \" " , flush: false )
320+ }
321+
322+ if var sourceLocation = frame. sourceLocation {
323+ if options. shouldSanitize {
324+ sourceLocation. file = sanitizePath ( sourceLocation. file)
325+ }
326+ write ( #", "sourceLocation": { "# , flush: false )
327+
328+ write ( """
329+ " file " : " \( escapeJSON ( sourceLocation. file) ) " , \
330+ " line " : \( sourceLocation. line) , \
331+ " column " : \( sourceLocation. column)
332+ """ , flush: false )
333+
334+ write ( " } " , flush: false )
335+ }
336+ }
337+ write ( " } " , flush: false )
338+ }
339+
340+ write ( " ] " , flush: false )
341+
342+ write ( " } " , flush: false )
343+
344+ if options. showMentionedImages {
345+ for frame in thread. frames {
346+ if let imageName = frame. image,
347+ let imageIndex = crashLog. images?
348+ . firstIndex ( where: { $0. name == imageName } ) {
349+ mentionedImages. insert ( imageIndex)
350+ } else if let addressString = frame. address,
351+ let address = Log . addressFromString ( addressString) ,
352+ let imageIndex = imageByAddress ( address) {
353+ mentionedImages. insert ( imageIndex)
354+ }
355+ }
356+ }
357+ }
358+
359+ func writeImage( _ image: Log . Image , first: Bool ) {
360+ if !first {
361+ write ( " , " , flush: false )
362+ }
363+
364+ write ( " { " , flush: false )
365+
366+ if let name = image. name {
367+ write ( " \" name \" : \" \( escapeJSON ( name) ) \" , " , flush: false )
368+ }
369+
370+ if let buildId = image. buildId {
371+ write ( " \" buildId \" : \" \( buildId) \" , " , flush: false )
372+ }
373+
374+ if var path = image. path {
375+ if options. shouldSanitize {
376+ path = sanitizePath ( path)
377+ }
378+ write ( " \" path \" : \" \( path) \" , " , flush: false )
379+ }
380+
381+ write ( """
382+ " baseAddress " : " \( image. baseAddress) " , \
383+ " endOfText " : " \( image. endOfText) "
384+ """ , flush: false )
385+
386+ write ( " } " , flush: false )
387+ }
388+ }
0 commit comments