Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **`XMLNode.lineNumber`** — new computed property exposing the 1-based source line number
of an element as reported by libxml2 (`xmlGetLineNo`). Returns `nil` when line information
is unavailable (e.g. programmatically created nodes). Resolves #28.

- **Coverage boost tests for internal streaming/encoder paths** — expanded
`XMLCoverageBoostTests` with direct tests for `_XMLTreeEncoder` internals
(date strategy variants, custom-date error wrapping, keyed attribute/text-content
Expand Down
11 changes: 5 additions & 6 deletions Sources/SwiftXMLCoder/LibXML2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import SwiftXMLCoderOwnership6

enum LibXML2 {
static let initializeOnce: Void = {
// libxml2 is fully initialised (including the encoding-handler table)
// by swiftxmlcoder_auto_init_libxml2, a C __attribute__((constructor))
// that runs at library-load time before any thread is created.
// This call is a no-op safeguard for exotic configurations (e.g. pure
// static linking on platforms that do not run constructors).
xmlInitParser()
// Pre-warm the encoding handler table for UTF-8. libxml2's
// xmlGetCharEncodingHandler lazily initialises a global handler
// table that is not thread-safe. Resolving it here (under the
// dispatch_once guarantee of `static let`) prevents a SEGV when
// multiple threads call xmlTextWriterStartDocument concurrently.
swiftxmlcoder_warm_encoding_handler("UTF-8")
}()

static func ensureInitialized() {
Expand Down
10 changes: 10 additions & 0 deletions Sources/SwiftXMLCoder/XMLNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public struct XMLNode {
}
}

/// The 1-based line number of this element in the source document, or `nil` if unavailable.
///
/// libxml2 reports `0` when line information was not recorded during parsing; this property
/// returns `nil` in that case. Line numbers greater than 65535 are supported on platforms
/// where libxml2 stores extended line information.
public var lineNumber: Int? {
let line = xmlGetLineNo(nodePointer)
return line > 0 ? Int(line) : nil
}

/// Returns all direct element children of this node.
public func children() -> [XMLNode] {
var nodes: [XMLNode] = []
Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftXMLCoderCShim/SwiftXMLCoderCShim.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
#include "SwiftXMLCoderCShim.h"

// Initialise libxml2 at library-load time, in a single-threaded context,
// before any Swift or user code runs. This is the only reliable way to
// avoid a SEGV when multiple threads concurrently access libxml2 APIs for
// the first time: under ThreadSanitizer, TSan's pthread interceptors can
// interfere with libxml2's own internal pthread_once guard, leaving the
// encoding-handler table in a NULL state when xmlGetCharEncodingHandler is
// subsequently called from a concurrent thread.
//
// By forcing full initialisation here (including the UTF-8 handler warm-up),
// we guarantee that the global tables are populated before any thread is
// created, making the subsequent calls in LibXML2.ensureInitialized() no-ops.
__attribute__((constructor))
static void swiftxmlcoder_auto_init_libxml2(void) {
xmlInitParser();
// Pre-warm the encoding handler for UTF-8 so that libxml2's lazy table
// population (xmlGetCharEncodingHandler) never runs on a concurrent thread.
xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler("UTF-8");
if (handler != NULL) {
xmlCharEncCloseFunc(handler);
}
}

void swiftxmlcoder_xml_free_xml_char(xmlChar * _Nullable pointer) {
if (pointer != NULL) {
xmlFree(pointer);
Expand Down
32 changes: 32 additions & 0 deletions Tests/SwiftXMLCoderTests/XMLNodeLineNumberTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftXMLCoder
import XCTest

final class XMLNodeLineNumberTests: XCTestCase {
// MARK: - lineNumber

func test_lineNumber_returnsCorrectLineForRootElement() throws {
let xml = "<root/>"
let document = try SwiftXMLCoder.XMLDocument(data: Data(xml.utf8))
let root = try XCTUnwrap(document.rootElement())
XCTAssertEqual(root.lineNumber, 1)
}

func test_lineNumber_returnsCorrectLineForNestedElement() throws {
let xml = "<root>\n <child/>\n</root>"
let document = try SwiftXMLCoder.XMLDocument(data: Data(xml.utf8))
let child = try XCTUnwrap(document.rootElement()?.firstChild(named: "child"))
XCTAssertEqual(child.lineNumber, 2)
}

func test_lineNumber_returnsCorrectLinesForSiblings() throws {
let xml = "<root>\n<a/>\n<b/>\n<c/>\n</root>"
let document = try SwiftXMLCoder.XMLDocument(data: Data(xml.utf8))
let root = try XCTUnwrap(document.rootElement())
let children = root.children()

XCTAssertEqual(children.count, 3)
XCTAssertEqual(children[0].lineNumber, 2)
XCTAssertEqual(children[1].lineNumber, 3)
XCTAssertEqual(children[2].lineNumber, 4)
}
}
Loading