Skip to content

Commit f26f11f

Browse files
committed
Add JSONMessageStreamingParser to help parse swiftc & xcbuild
1 parent 69a2664 commit f26f11f

File tree

3 files changed

+451
-3
lines changed

3 files changed

+451
-3
lines changed

Sources/TSCUtility/CMakeLists.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ add_library(TSCUtility
1313
BuildFlags.swift
1414
CollectionExtensions.swift
1515
Diagnostics.swift
16+
dlopen.swift
1617
Downloader.swift
17-
FSWatch.swift
1818
FloatingPointExtensions.swift
19+
FSWatch.swift
1920
Git.swift
2021
IndexStore.swift
2122
InterruptHandler.swift
23+
JSONMessageStreamingParser.swift
24+
misc.swift
2225
OSLog.swift
2326
PkgConfig.swift
2427
Platform.swift
@@ -30,8 +33,7 @@ add_library(TSCUtility
3033
Verbosity.swift
3134
Version.swift
3235
Versioning.swift
33-
dlopen.swift
34-
misc.swift)
36+
)
3537
target_link_libraries(TSCUtility PUBLIC
3638
TSCBasic)
3739
# NOTE(compnerd) workaround for CMake not setting up include flags yet
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/// Protocol for the parser delegate to get notified of parsing events.
14+
public protocol JSONMessageStreamingParserDelegate: class {
15+
16+
/// A decodable type representing the JSON messages being parsed.
17+
associatedtype Message: Decodable
18+
19+
/// Called for each message parsed.
20+
func jsonMessageStreamingParser(_ parser: JSONMessageStreamingParser<Self>, didParse message: Message)
21+
22+
/// Called when parsing raw text instead of message size.
23+
func jsonMessageStreamingParser(_ parser: JSONMessageStreamingParser<Self>, didParseRawText text: String)
24+
25+
/// Called on an un-expected parsing error. No more events will be received after that.
26+
func jsonMessageStreamingParser(_ parser: JSONMessageStreamingParser<Self>, didFailWith error: Error)
27+
}
28+
29+
/// Streaming parser for JSON messages seperated by integers to represent size of message. Used by the Swift compiler
30+
/// and XCBuild to share progess information: https://github.com/apple/swift/blob/master/docs/DriverParseableOutput.rst.
31+
public final class JSONMessageStreamingParser<Delegate: JSONMessageStreamingParserDelegate> {
32+
33+
/// The object representing the JSON message being parsed.
34+
public typealias Message = Delegate.Message
35+
36+
/// State of the parser state machine.
37+
private enum State {
38+
case parsingMessageSize
39+
case parsingMessage(size: Int)
40+
case parsingNewlineAfterMessage
41+
case failed
42+
}
43+
44+
/// Delegate to notify of parsing events.
45+
public weak var delegate: Delegate?
46+
47+
/// Buffer containing the bytes until a full message can be parsed.
48+
private var buffer: [UInt8] = []
49+
50+
/// The parser's state machine current state.
51+
private var state: State = .parsingMessageSize
52+
53+
/// The JSON decoder to parse messages.
54+
private let decoder: JSONDecoder
55+
56+
/// Initializes the parser.
57+
/// - Parameters:
58+
/// - delegate: The `JSONMessageStreamingParserDelegate` that will receive parsing event callbacks.
59+
/// - decoder: The `JSONDecoder` to use for decoding JSON messages.
60+
public init(delegate: Delegate, decoder: JSONDecoder = JSONDecoder())
61+
{
62+
self.delegate = delegate
63+
self.decoder = decoder
64+
}
65+
66+
/// Parse the next bytes of the stream.
67+
/// - Note: If a parsing error is encountered, the delegate will be notified and the parser won't accept any further
68+
/// input.
69+
public func parse<C>(bytes: C) where C: Collection, C.Element == UInt8 {
70+
if case .failed = state { return }
71+
72+
do {
73+
try parseImpl(bytes: bytes)
74+
} catch {
75+
state = .failed
76+
delegate?.jsonMessageStreamingParser(self, didFailWith: error)
77+
}
78+
}
79+
}
80+
81+
private extension JSONMessageStreamingParser {
82+
83+
/// Error corresponding to invalid Swift compiler output.
84+
struct ParsingError: LocalizedError {
85+
86+
/// Text describing the specific reason for the parsing failure.
87+
let reason: String
88+
89+
/// The underlying error, if there is one.
90+
let underlyingError: Error?
91+
92+
var errorDescription: String? {
93+
if let error = underlyingError {
94+
return "\(reason): \(error)"
95+
} else {
96+
return reason
97+
}
98+
}
99+
}
100+
101+
/// Throwing implementation of the parse function.
102+
func parseImpl<C>(bytes: C) throws where C: Collection, C.Element == UInt8 {
103+
switch state {
104+
case .parsingMessageSize:
105+
if let newlineIndex = bytes.firstIndex(of: newline) {
106+
buffer.append(contentsOf: bytes[..<newlineIndex])
107+
try parseMessageSize()
108+
109+
let nextIndex = bytes.index(after: newlineIndex)
110+
try parseImpl(bytes: bytes[nextIndex...])
111+
} else {
112+
buffer.append(contentsOf: bytes)
113+
}
114+
case .parsingMessage(size: let size):
115+
let remainingBytes = size - buffer.count
116+
if remainingBytes <= bytes.count {
117+
buffer.append(contentsOf: bytes.prefix(remainingBytes))
118+
119+
let message = try parseMessage()
120+
delegate?.jsonMessageStreamingParser(self, didParse: message)
121+
122+
try parseImpl(bytes: bytes.dropFirst(remainingBytes))
123+
} else {
124+
buffer.append(contentsOf: bytes)
125+
}
126+
case .parsingNewlineAfterMessage:
127+
if let firstByte = bytes.first {
128+
precondition(firstByte == newline)
129+
state = .parsingMessageSize
130+
try parseImpl(bytes: bytes.dropFirst())
131+
}
132+
case .failed:
133+
return
134+
}
135+
}
136+
137+
/// Parse the next message size from the buffer and update the state machine.
138+
func parseMessageSize() throws {
139+
guard let string = String(bytes: buffer, encoding: .utf8) else {
140+
throw ParsingError(reason: "invalid UTF8 bytes", underlyingError: nil)
141+
}
142+
143+
guard let messageSize = Int(string) else {
144+
delegate?.jsonMessageStreamingParser(self, didParseRawText: string)
145+
buffer.removeAll()
146+
return
147+
}
148+
149+
buffer.removeAll()
150+
state = .parsingMessage(size: messageSize)
151+
}
152+
153+
/// Parse the message in the buffer and update the state machine.
154+
func parseMessage() throws -> Message {
155+
let data = Data(buffer)
156+
buffer.removeAll()
157+
state = .parsingNewlineAfterMessage
158+
159+
do {
160+
return try decoder.decode(Message.self, from: data)
161+
} catch {
162+
throw ParsingError(reason: "unexpected JSON message", underlyingError: error)
163+
}
164+
}
165+
}
166+
167+
private let newline = UInt8(ascii: "\n")

0 commit comments

Comments
 (0)