diff --git a/Sources/FoundationXML/XMLParser.swift b/Sources/FoundationXML/XMLParser.swift index d89d0ee1f4..62c94679ee 100644 --- a/Sources/FoundationXML/XMLParser.swift +++ b/Sources/FoundationXML/XMLParser.swift @@ -398,9 +398,7 @@ extension XMLParser : @unchecked Sendable { } open class XMLParser : NSObject { private var _handler: _CFXMLInterfaceSAXHandler -#if !os(WASI) internal var _stream: InputStream? -#endif internal var _data: Data? internal var _chunkSize = Int(4096 * 32) // a suitably large number for a decent chunk size @@ -414,9 +412,6 @@ open class XMLParser : NSObject { // initializes the parser with the specified URL. public convenience init?(contentsOf url: URL) { -#if os(WASI) - return nil -#else setupXMLParsing() if url.isFileURL { if let stream = InputStream(url: url) { @@ -434,7 +429,6 @@ open class XMLParser : NSObject { return nil } } -#endif } // create the parser from data @@ -450,7 +444,6 @@ open class XMLParser : NSObject { _CFXMLInterfaceDestroyContext(_parserContext) } -#if !os(WASI) //create a parser that incrementally pulls data from the specified stream and parses it. public init(stream: InputStream) { setupXMLParsing() @@ -458,7 +451,6 @@ open class XMLParser : NSObject { _handler = _CFXMLInterfaceCreateSAXHandler() _parserContext = nil } -#endif open weak var delegate: XMLParserDelegate? @@ -469,33 +461,35 @@ open class XMLParser : NSObject { open var externalEntityResolvingPolicy: ExternalEntityResolvingPolicy = .never open var allowedExternalEntityURLs: Set? - -#if os(WASI) - private static var _currentParser: XMLParser? -#endif + #if os(WASI) + /// The current parser associated with the current thread. (assuming no multi-threading) + /// FIXME: Unify the implementation with the other platforms once we unlock `threadDictionary` + /// or migrate to `FoundationEssentials._ThreadLocal`. + private static nonisolated(unsafe) var _currentParser: XMLParser? = nil + #else + /// The current parser associated with the current thread. + private static var _currentParser: XMLParser? { + get { + return Thread.current.threadDictionary["__CurrentNSXMLParser"] as? XMLParser + } + set { + Thread.current.threadDictionary["__CurrentNSXMLParser"] = newValue + } + } + #endif + + /// The current parser associated with the current thread. internal static func currentParser() -> XMLParser? { -#if os(WASI) return _currentParser -#else - if let current = Thread.current.threadDictionary["__CurrentNSXMLParser"] { - return current as? XMLParser - } else { - return nil - } -#endif } - - internal static func setCurrentParser(_ parser: XMLParser?) { -#if os(WASI) + + /// Execute the given closure with the current parser set to the given parser. + internal static func withCurrentParser(_ parser: XMLParser, _ body: () -> R) -> R { + let oldParser = _currentParser _currentParser = parser -#else - if let p = parser { - Thread.current.threadDictionary["__CurrentNSXMLParser"] = p - } else { - Thread.current.threadDictionary.removeObject(forKey: "__CurrentNSXMLParser") - } -#endif + defer { _currentParser = oldParser } + return body() } internal func _handleParseResult(_ parseResult: Int32) -> Bool { @@ -569,7 +563,6 @@ open class XMLParser : NSObject { return result } -#if !os(WASI) internal func parseFrom(_ stream : InputStream) -> Bool { var result = true @@ -598,37 +591,17 @@ open class XMLParser : NSObject { return result } -#else - internal func parse(from data: Data) -> Bool { - var result = true - var chunkStart = 0 - var chunkEnd = min(_chunkSize, data.count) - while result && chunkStart < chunkEnd { - let chunk = data[chunkStart.. Bool { -#if os(WASI) - return _data.map { parse(from: $0) } ?? false -#else - XMLParser.setCurrentParser(self) - defer { XMLParser.setCurrentParser(nil) } - - if _stream != nil { - return parseFrom(_stream!) - } else if _data != nil { - return parseData(_data!, lastChunkOfData: true) + return Self.withCurrentParser(self) { + if _stream != nil { + return parseFrom(_stream!) + } else if _data != nil { + return parseData(_data!, lastChunkOfData: true) + } + return false } - - return false -#endif } // called by the delegate to stop the parse. The delegate will get an error message sent to it. diff --git a/Tests/Foundation/TestXMLParser.swift b/Tests/Foundation/TestXMLParser.swift index c98741eb38..df3685a82e 100644 --- a/Tests/Foundation/TestXMLParser.swift +++ b/Tests/Foundation/TestXMLParser.swift @@ -198,5 +198,46 @@ class TestXMLParser : XCTestCase { ElementNameChecker("noPrefix").check() ElementNameChecker("myPrefix:myLocalName").check() } - + + func testExternalEntity() throws { + class Delegate: XMLParserDelegateEventStream { + override func parserDidStartDocument(_ parser: XMLParser) { + // Start a child parser, updating `currentParser` to the child parser + // to ensure that `currentParser` won't be reset to `nil`, which would + // ignore any external entity related configuration. + let childParser = XMLParser(data: "".data(using: .utf8)!) + XCTAssertTrue(childParser.parse()) + super.parserDidStartDocument(parser) + } + } + try withTemporaryDirectory { dir, _ in + let greetingPath = dir.appendingPathComponent("greeting.xml") + try Data("".utf8).write(to: greetingPath) + let xml = """ + + + ]> + &greeting; + """ + + let parser = XMLParser(data: xml.data(using: .utf8)!) + // Explicitly disable external entity resolving + parser.externalEntityResolvingPolicy = .never + let delegate = Delegate() + parser.delegate = delegate + // The parse result changes depending on the libxml2 version + // because of the following libxml2 commit (shipped in libxml2 2.9.10): + // https://gitlab.gnome.org/GNOME/libxml2/-/commit/eddfbc38fa7e84ccd480eab3738e40d1b2c83979 + // So we don't check the parse result here. + _ = parser.parse() + XCTAssertEqual(delegate.events, [ + .startDocument, + .didStartElement("doc", nil, nil, [:]), + // Should not have parsed the external entity + .didEndElement("doc", nil, nil), + .endDocument, + ]) + } + } }