@crossbind/port-expat 2.0.0-beta.60 → 2.0.0-beta.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -10
- package/mergeConfig.mjs +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# @crossbind/port-expat
|
|
2
|
-
**Precompiled Expat
|
|
2
|
+
**Precompiled Expat XML parser library built with crossbind for seamless integration in JavaScript, WebAssembly and React Native projects.**
|
|
3
3
|
|
|
4
4
|
<a href="https://www.npmjs.com/package/@crossbind/port-expat">
|
|
5
5
|
<img alt="NPM version" src="https://img.shields.io/npm/v/@crossbind/port-expat?style=for-the-badge" />
|
|
6
6
|
</a>
|
|
7
7
|
<a href="https://github.com/libexpat/libexpat">
|
|
8
|
-
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Funpkg.com%2F%40crossbind%
|
|
8
|
+
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Funpkg.com%2F%40crossbind%2Fport-expat%2Fpackage.json&query=%24.nativeVersion&style=for-the-badge&label=Expat" />
|
|
9
9
|
</a>
|
|
10
10
|
<a href="https://github.com/libexpat/libexpat/blob/master/COPYING">
|
|
11
|
-
<img alt="License" src="https://img.shields.io/npm/l/%40crossbind%
|
|
11
|
+
<img alt="License" src="https://img.shields.io/npm/l/%40crossbind%2Fport-expat?style=for-the-badge" />
|
|
12
12
|
</a>
|
|
13
13
|
|
|
14
14
|
> Use it together with **[crossbind](https://crossbind.dev)** — the toolchain for using C++ libraries from JavaScript, TypeScript, WebAssembly, Node.js and React Native. Learn more at **[crossbind.dev](https://crossbind.dev)**.
|
|
15
15
|
|
|
16
|
+
## See it run
|
|
17
|
+
Three apps on **[crossbind.dev/ports/expat](https://crossbind.dev/ports/expat/#apps)** run this package in your browser, on the same generated data every time:
|
|
18
|
+
|
|
19
|
+
- **XML firehose.** Parses an OpenStreetMap-style document that is generated as it goes: up to 1.07 GB and 28.8 million elements, 1 MiB at a time, inside the Web Worker the module runs in. The module's memory stays the same size from start to finish. An XML file you pick streams through the same way.
|
|
20
|
+
- **Attack lab.** A 784-byte billion-laughs document that would expand to 3 GB stops at line 14, column 6, after 2.6 MB of text. A quadratic blowup, an external entity pointing at `file:///etc/passwd` (reported to a handler, never loaded) and 100,000 levels of nesting run the same way, with the limits adjustable.
|
|
21
|
+
- **GPX reader.** A namespace-aware parse finds Garmin's heart-rate extension whether a file spells it `gpxtpx:` or `ns3:`; the reader draws the track and exports GeoJSON. The generated sample ride comes out at 64.043 km, 9,113 points, 803.3 m of climb and an average heart rate of 126.7 bpm.
|
|
22
|
+
|
|
23
|
+
Their C++ wrappers, and the self-check the site build runs against results computed independently of this package, are in [`landing/demos/lib-expat`](https://github.com/crossbind/crossbind/tree/main/landing/demos/lib-expat).
|
|
24
|
+
|
|
16
25
|
## Integration
|
|
17
26
|
Install the main package together with the platform builds:
|
|
18
27
|
|
|
@@ -39,18 +48,154 @@ export default {
|
|
|
39
48
|
};
|
|
40
49
|
```
|
|
41
50
|
|
|
51
|
+
A native Node.js addon links the build of its platform: `crossbind build -p darwin`, `-p linux` or `-p win32` takes `@crossbind/port-expat-darwin`, `-linux` or `-win32`. Install it and import its `crossbind.config.js` the same way.
|
|
52
|
+
|
|
42
53
|
## Usage
|
|
43
|
-
|
|
54
|
+
crossbind binds your C++ headers to JavaScript, so the usual pattern is a small wrapper around the library. This one turns XML into plain objects with Expat's element and character data handlers, and reports errors with their line and column. Put it in your project's native folder (`src/native/` by default):
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
```cpp
|
|
57
|
+
// src/native/xml_tree.h
|
|
58
|
+
#pragma once
|
|
59
|
+
|
|
60
|
+
#include <expat.h>
|
|
61
|
+
|
|
62
|
+
#include <stdexcept>
|
|
63
|
+
#include <string>
|
|
64
|
+
#include <vector>
|
|
65
|
+
|
|
66
|
+
// XML in, JSON out. Each element becomes {"name","attributes","children","text"}: `children` holds
|
|
67
|
+
// its child elements and `text` its own character data, left out when it is only whitespace.
|
|
68
|
+
// Malformed XML throws Expat's message with the line and column where parsing stopped.
|
|
69
|
+
class XmlTree {
|
|
70
|
+
public:
|
|
71
|
+
static std::string version() {
|
|
72
|
+
const XML_Expat_Version v = XML_ExpatVersionInfo();
|
|
73
|
+
return std::to_string(v.major) + "." + std::to_string(v.minor) + "." + std::to_string(v.micro);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static std::string parse(const std::string& xml) {
|
|
77
|
+
Builder builder;
|
|
78
|
+
XML_Parser parser = XML_ParserCreate(nullptr);
|
|
79
|
+
if (!parser) throw std::runtime_error("out of memory");
|
|
80
|
+
XML_SetUserData(parser, &builder);
|
|
81
|
+
XML_SetElementHandler(parser, onStart, onEnd);
|
|
82
|
+
XML_SetCharacterDataHandler(parser, onText);
|
|
83
|
+
const bool ok = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), XML_TRUE) == XML_STATUS_OK;
|
|
84
|
+
const std::string error = ok ? "" : std::string(XML_ErrorString(XML_GetErrorCode(parser))) + " at line " +
|
|
85
|
+
std::to_string(XML_GetCurrentLineNumber(parser)) + ", column " +
|
|
86
|
+
std::to_string(XML_GetCurrentColumnNumber(parser));
|
|
87
|
+
XML_ParserFree(parser);
|
|
88
|
+
if (!ok) throw std::runtime_error(error);
|
|
89
|
+
return builder.json;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private:
|
|
93
|
+
struct Builder {
|
|
94
|
+
std::string json;
|
|
95
|
+
std::vector<std::string> text; // character data of each open element
|
|
96
|
+
std::vector<bool> hasChildren;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Expat delivers UTF-8; JSON only needs quotes, backslashes and control characters escaped.
|
|
100
|
+
static std::string quote(const std::string& value) {
|
|
101
|
+
std::string out = "\"";
|
|
102
|
+
for (const char c : value) {
|
|
103
|
+
if (c == '"' || c == '\\') {
|
|
104
|
+
out += '\\';
|
|
105
|
+
out += c;
|
|
106
|
+
} else if (c == '\n') {
|
|
107
|
+
out += "\\n";
|
|
108
|
+
} else if (c == '\t') {
|
|
109
|
+
out += "\\t";
|
|
110
|
+
} else if (c == '\r') {
|
|
111
|
+
out += "\\r";
|
|
112
|
+
} else {
|
|
113
|
+
out += c;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out + "\"";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static void XMLCALL onStart(void* data, const XML_Char* name, const XML_Char** attributes) {
|
|
120
|
+
Builder& builder = *static_cast<Builder*>(data);
|
|
121
|
+
if (!builder.hasChildren.empty()) {
|
|
122
|
+
if (builder.hasChildren.back()) builder.json += ",";
|
|
123
|
+
builder.hasChildren.back() = true;
|
|
124
|
+
}
|
|
125
|
+
builder.json += "{\"name\":" + quote(name) + ",\"attributes\":{";
|
|
126
|
+
for (int i = 0; attributes[i]; i += 2) builder.json += (i ? "," : "") + quote(attributes[i]) + ":" + quote(attributes[i + 1]);
|
|
127
|
+
builder.json += "},\"children\":[";
|
|
128
|
+
builder.text.emplace_back();
|
|
129
|
+
builder.hasChildren.push_back(false);
|
|
130
|
+
}
|
|
48
131
|
|
|
49
|
-
|
|
50
|
-
|
|
132
|
+
static void XMLCALL onText(void* data, const XML_Char* text, int length) {
|
|
133
|
+
static_cast<Builder*>(data)->text.back().append(text, static_cast<size_t>(length));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static void XMLCALL onEnd(void* data, const XML_Char*) {
|
|
137
|
+
Builder& builder = *static_cast<Builder*>(data);
|
|
138
|
+
const std::string& text = builder.text.back();
|
|
139
|
+
builder.json += "]";
|
|
140
|
+
if (text.find_first_not_of(" \t\r\n") != std::string::npos) builder.json += ",\"text\":" + quote(text);
|
|
141
|
+
builder.json += "}";
|
|
142
|
+
builder.text.pop_back();
|
|
143
|
+
builder.hasChildren.pop_back();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then call it from JavaScript:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { initNative, XmlTree } from './native/xml_tree.h';
|
|
152
|
+
|
|
153
|
+
await initNative();
|
|
154
|
+
console.log('Expat', await XmlTree.version()); // Expat 2.8.5
|
|
155
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
156
|
+
<catalog>
|
|
157
|
+
<book id="1" lang="en"><title>Dune</title><price currency="EUR">9.99</price></book>
|
|
158
|
+
<book id="2" lang="fr"><title>L'Étranger</title><price currency="EUR">7.50</price></book>
|
|
159
|
+
<book id="3" lang="en"><title>Pride & Prejudice</title><price currency="GBP">5.25</price></book>
|
|
160
|
+
</catalog>`;
|
|
161
|
+
const catalog = JSON.parse(await XmlTree.parse(xml));
|
|
162
|
+
for (const book of catalog.children) {
|
|
163
|
+
const [title, price] = book.children;
|
|
164
|
+
console.log(book.attributes.id, book.attributes.lang, title.text, price.text, price.attributes.currency);
|
|
165
|
+
}
|
|
166
|
+
// 1 en Dune 9.99 EUR
|
|
167
|
+
// 2 fr L'Étranger 7.50 EUR
|
|
168
|
+
// 3 en Pride & Prejudice 5.25 GBP
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await XmlTree.parse('<catalog>\n <book id="4"><title>Emma</book>\n</catalog>');
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.log(error.cppMessage ?? error.message); // mismatched tag at line 2, column 30
|
|
51
174
|
}
|
|
52
175
|
```
|
|
53
176
|
|
|
177
|
+
- Expat is a stream parser and builds no tree of its own: it calls your handlers as it reads, and the handlers decide what to keep. Text can arrive in several calls for one run of characters, which is why the wrapper appends it.
|
|
178
|
+
- Expat reports UTF-8, and its columns count from 0.
|
|
179
|
+
- On WebAssembly a C++ exception reaches JavaScript with `error.message` prefixed by its type (`std::runtime_error: ...`); `error.cppMessage` holds Expat's text alone. On React Native, `error.message` is the text itself.
|
|
180
|
+
- For large documents, pass a path instead of a string and let Expat read the file in pieces: the streaming example below does that.
|
|
181
|
+
|
|
182
|
+
### More examples
|
|
183
|
+
Each one runs in your browser on [crossbind.dev/ports/expat](https://crossbind.dev/ports/expat/#usage), next to the code shown there:
|
|
184
|
+
|
|
185
|
+
- [Stream a file through Expat](https://crossbind.dev/ports/expat/#02-stream): `XML_GetBuffer` and `XML_ParseBuffer`, 64 KiB at a time.
|
|
186
|
+
- [Read namespaced XML whatever the prefixes](https://crossbind.dev/ports/expat/#03-namespaces): `XML_ParserCreateNS` and `XML_SetStartNamespaceDeclHandler`.
|
|
187
|
+
- [Expand entities without a billion laughs](https://crossbind.dev/ports/expat/#04-limits): `XML_SetBillionLaughsAttackProtectionMaximumAmplification` and `XML_SetBillionLaughsAttackProtectionActivationThreshold`.
|
|
188
|
+
|
|
189
|
+
Setup and differences per platform: [WebAssembly](https://crossbind.dev/ports/expat/wasm/) · [Android](https://crossbind.dev/ports/expat/android/) · [iOS](https://crossbind.dev/ports/expat/ios/) · [macOS](https://crossbind.dev/ports/expat/darwin/) · [Linux](https://crossbind.dev/ports/expat/linux/) · [Windows](https://crossbind.dev/ports/expat/win32/) · [WASI](https://crossbind.dev/ports/expat/wasi/), which also has a command-line program built with `crossbind build -p wasi`.
|
|
190
|
+
|
|
191
|
+
## What this build includes
|
|
192
|
+
- Expat 2.8.5 as a static library, built with upstream's CMake defaults: DTD and general entity support (`XML_DTD`, `XML_GE`), namespaces (`XML_NS`) and 1,024 bytes of context (`XML_CONTEXT_BYTES`). The API speaks UTF-8.
|
|
193
|
+
- Upstream's protections at their defaults. Entity expansion may reach at most 100 times the input once 8 MiB has been processed. Allocations may reach at most 100 times the input once 64 MiB has been allocated.
|
|
194
|
+
- `expat.h` declares the setters for those limits (`XML_SetBillionLaughsAttackProtection*`, `XML_SetAllocTracker*`) only when `XML_DTD` is defined or `XML_GE` is 1, and it does not include the build's `expat_config.h`. Define `XML_GE` as 1 before including it, as the attack-limit example does; the library has them compiled in.
|
|
195
|
+
- The built-in encodings: UTF-8, UTF-16, ISO-8859-1 and US-ASCII. A document declaring any other, such as `windows-1252` or `Shift_JIS`, fails with `unknown encoding` unless you set `XML_SetUnknownEncodingHandler`.
|
|
196
|
+
- No `XML_LARGE_SIZE`. On `wasm32` (WebAssembly and WASI), `XML_GetCurrentByteIndex` and `XML_GetCurrentByteCount` are 32-bit and cannot report positions past 2 GiB, so count bytes yourself; parsing itself goes on, and a 2.43 GB stream parsed in full in a test of this build. On 64-bit Android and iOS they are 64-bit.
|
|
197
|
+
- Upstream's README warns that Expat has unfixed security issues ([libexpat#1160](https://github.com/libexpat/libexpat/issues/1160)). Keep the limits on for untrusted input and update with upstream releases.
|
|
198
|
+
|
|
54
199
|
## Supported platforms
|
|
55
200
|
This is the main package; the precompiled binaries are shipped per platform:
|
|
56
201
|
|
|
@@ -59,8 +204,13 @@ This is the main package; the precompiled binaries are shipped per platform:
|
|
|
59
204
|
| WebAssembly | [`@crossbind/port-expat-wasm`](https://www.npmjs.com/package/@crossbind/port-expat-wasm) | `wasm32` — single-threaded & multi-threaded |
|
|
60
205
|
| Android | [`@crossbind/port-expat-android`](https://www.npmjs.com/package/@crossbind/port-expat-android) | `arm64-v8a` (64-bit ARM), `x86_64` (emulator) |
|
|
61
206
|
| iOS | [`@crossbind/port-expat-ios`](https://www.npmjs.com/package/@crossbind/port-expat-ios) | device (`arm64`), simulator (`arm64`) |
|
|
207
|
+
| macOS | [`@crossbind/port-expat-darwin`](https://www.npmjs.com/package/@crossbind/port-expat-darwin) | `arm64` (Apple silicon), `x64` (Intel) — native Node.js addons |
|
|
208
|
+
| Linux | [`@crossbind/port-expat-linux`](https://www.npmjs.com/package/@crossbind/port-expat-linux) | `x64`, `arm64` — glibc 2.28 or later, native Node.js addons |
|
|
209
|
+
| Windows | [`@crossbind/port-expat-win32`](https://www.npmjs.com/package/@crossbind/port-expat-win32) | `x64`, `arm64` — Windows 10 or later, native Node.js addons |
|
|
210
|
+
| WASI library | [`@crossbind/port-expat-wasi`](https://www.npmjs.com/package/@crossbind/port-expat-wasi) | `wasm32-wasip3` — single-threaded |
|
|
211
|
+
| WASI command | [`@crossbind/port-expat-bin-wasi`](https://www.npmjs.com/package/@crossbind/port-expat-bin-wasi) | the upstream `xmlwf` well-formedness checker as an `xmlwf-wasi` command (wasmtime 47+) |
|
|
62
212
|
|
|
63
213
|
## License
|
|
64
214
|
This project includes the precompiled Expat library, which is distributed under the [MIT License](https://github.com/libexpat/libexpat/blob/master/COPYING).
|
|
65
215
|
|
|
66
|
-
Expat Homepage: [https://github.
|
|
216
|
+
Expat Homepage: [https://libexpat.github.io/](https://libexpat.github.io/)
|
package/mergeConfig.mjs
CHANGED
|
@@ -7,6 +7,9 @@ export default (newConfig = {}) => ({
|
|
|
7
7
|
export: {
|
|
8
8
|
type: 'cmake',
|
|
9
9
|
bundle: false,
|
|
10
|
+
// expat.h declares its limit setters (XML_SetBillionLaughsAttackProtection*,
|
|
11
|
+
// XML_SetAllocTracker*) only when XML_DTD or XML_GE is set, which expat_config.h does.
|
|
12
|
+
headerPrelude: ['expat_config.h'],
|
|
10
13
|
...(newConfig.export || {}),
|
|
11
14
|
},
|
|
12
15
|
paths: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossbind/port-expat",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.62",
|
|
4
4
|
"nativeVersion": "2.8.5",
|
|
5
5
|
"description": "This package provides the precompiled Expat XML parser library, built using crossbind, for seamless integration into JavaScript, WebAssembly, and React Native projects. It enables fast and efficient parsing of XML data, making it ideal for applications requiring robust XML handling across platforms. Perfect for web, server-side, and mobile development environments.",
|
|
6
6
|
"homepage": "https://github.com/crossbind/crossbind/tree/main/ports/expat#readme",
|