Skip to content

Commit 604aa72

Browse files
committed
A simple xml parser.
1 parent 695298b commit 604aa72

20 files changed

Lines changed: 2178 additions & 21 deletions

.github/workflows/CI.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
pull_request:
9+
10+
jobs:
11+
linux:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- uses: messense/maturin-action@v1
16+
with:
17+
manylinux: auto
18+
command: build
19+
args: --release --sdist -o dist --find-interpreter
20+
- name: Upload wheels
21+
uses: actions/upload-artifact@v2
22+
with:
23+
name: wheels
24+
path: dist
25+
26+
windows:
27+
runs-on: windows-latest
28+
steps:
29+
- uses: actions/checkout@v3
30+
- uses: messense/maturin-action@v1
31+
with:
32+
command: build
33+
args: --release -o dist --find-interpreter
34+
- name: Upload wheels
35+
uses: actions/upload-artifact@v2
36+
with:
37+
name: wheels
38+
path: dist
39+
40+
macos:
41+
runs-on: macos-latest
42+
steps:
43+
- uses: actions/checkout@v3
44+
- uses: messense/maturin-action@v1
45+
with:
46+
command: build
47+
args: --release -o dist --universal2 --find-interpreter
48+
- name: Upload wheels
49+
uses: actions/upload-artifact@v2
50+
with:
51+
name: wheels
52+
path: dist
53+
54+
release:
55+
name: Release
56+
runs-on: ubuntu-latest
57+
if: "startsWith(github.ref, 'refs/tags/')"
58+
needs: [ macos, windows, linux ]
59+
steps:
60+
- uses: actions/download-artifact@v2
61+
with:
62+
name: wheels
63+
- name: Publish to PyPI
64+
uses: messense/maturin-action@v1
65+
env:
66+
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
67+
with:
68+
command: upload
69+
args: --skip-existing *

.gitignore

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,75 @@
1-
# Generated by Cargo
2-
# will have compiled files and executables
3-
debug/
4-
target/
5-
6-
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7-
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8-
Cargo.lock
9-
10-
# These are backup files generated by rustfmt
11-
**/*.rs.bk
12-
13-
# MSVC Windows builds of rustc generate these, which store debugging information
14-
*.pdb
15-
16-
# RustRover
17-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19-
# and can be added to the global gitignore or merged into this file. For a more nuclear
20-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
21-
#.idea/
1+
/target
2+
3+
# Byte-compiled / optimized / DLL files
4+
__pycache__/
5+
.pytest_cache/
6+
*.py[cod]
7+
8+
# C extensions
9+
*.so
10+
11+
# Distribution / packaging
12+
.Python
13+
.venv/
14+
env/
15+
bin/
16+
build/
17+
develop-eggs/
18+
dist/
19+
eggs/
20+
lib/
21+
lib64/
22+
parts/
23+
sdist/
24+
var/
25+
include/
26+
man/
27+
venv/
28+
*.egg-info/
29+
.installed.cfg
30+
*.egg
31+
32+
# Installer logs
33+
pip-log.txt
34+
pip-delete-this-directory.txt
35+
pip-selfcheck.json
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.cache
42+
nosetests.xml
43+
coverage.xml
44+
45+
# Translations
46+
*.mo
47+
48+
# Mr Developer
49+
.mr.developer.cfg
50+
.project
51+
.pydevproject
52+
53+
# Rope
54+
.ropeproject
55+
56+
# Django stuff:
57+
*.log
58+
*.pot
59+
60+
.DS_Store
61+
62+
# Sphinx documentation
63+
docs/_build/
64+
65+
# PyCharm
66+
.idea/
67+
68+
# VSCode
69+
.vscode/
70+
71+
# Pyenv
72+
.python-version
73+
74+
# Benchmark data cache
75+
benchmark_data/

CI.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
pull_request:
9+
10+
jobs:
11+
linux:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- uses: messense/maturin-action@v1
16+
with:
17+
manylinux: auto
18+
command: build
19+
args: --release --sdist -o dist --find-interpreter
20+
- name: Upload wheels
21+
uses: actions/upload-artifact@v2
22+
with:
23+
name: wheels
24+
path: dist
25+
26+
windows:
27+
runs-on: windows-latest
28+
steps:
29+
- uses: actions/checkout@v3
30+
- uses: messense/maturin-action@v1
31+
with:
32+
command: build
33+
args: --release -o dist --find-interpreter
34+
- name: Upload wheels
35+
uses: actions/upload-artifact@v2
36+
with:
37+
name: wheels
38+
path: dist
39+
40+
macos:
41+
runs-on: macos-latest
42+
steps:
43+
- uses: actions/checkout@v3
44+
- uses: messense/maturin-action@v1
45+
with:
46+
command: build
47+
args: --release -o dist --universal2 --find-interpreter
48+
- name: Upload wheels
49+
uses: actions/upload-artifact@v2
50+
with:
51+
name: wheels
52+
path: dist
53+
54+
release:
55+
name: Release
56+
runs-on: ubuntu-latest
57+
if: "startsWith(github.ref, 'refs/tags/')"
58+
needs: [ macos, windows, linux ]
59+
steps:
60+
- uses: actions/download-artifact@v2
61+
with:
62+
name: wheels
63+
- name: Publish to PyPI
64+
uses: messense/maturin-action@v1
65+
env:
66+
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
67+
with:
68+
command: upload
69+
args: --skip-existing *

CLAUDE.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# CLAUDE.md - AI Context for xml_iterator
2+
3+
## Project Overview
4+
5+
Fast XML parser with streaming iterator interface, built in Rust with Python bindings. Designed for protection against infinite depth XML attacks through streaming processing.
6+
7+
## Architecture
8+
9+
```
10+
xml_iterator/
11+
├── src/lib.rs # Rust core: XMLIterator + Python bindings
12+
├── xml_iterator/core.py # Python utilities: xml_to_dict, get_edge_counts
13+
├── tests/ # Comprehensive pytest suite
14+
└── benchmark*.py # Performance testing vs xmltodict
15+
```
16+
17+
## Core Components
18+
19+
### Rust Implementation (`src/lib.rs`)
20+
- **XMLIterator**: Streaming XML parser using quick-xml
21+
- **Events**: `start`, `end`, `text`, `empty` (self-closing tags)
22+
- **Python bindings**: PyO3 integration
23+
- **Protection**: No depth limits - user controls via early termination
24+
25+
### Python API
26+
- **`iter_xml(path)`**: Stream events `(count, event, value)`
27+
- **`xml_to_dict(path)`**: Convert to dictionary (xmltodict compatible)
28+
- **`get_edge_counts(path)`**: Count tag hierarchies
29+
30+
## Key Features
31+
32+
**100% xmltodict compatibility** - identical results on all test cases
33+
**Streaming performance** - 734x faster with early termination
34+
**Memory efficient** - constant memory usage regardless of file size
35+
**Real-world tested** - handles 300MB+ ESMA FIRDS XML files
36+
**Error handling** - graceful fallbacks for malformed XML
37+
38+
## Performance Characteristics
39+
40+
| Scenario | xml_iterator | xmltodict | Speedup |
41+
|----------|-------------|-----------|---------|
42+
| Small files (500 items) | 0.020s | 0.024s | 1.2x |
43+
| Large files (5000 items) | 0.231s | 0.251s | 1.1x |
44+
| Early termination | 0.001s | N/A | 734x |
45+
46+
## Development Workflow
47+
48+
```bash
49+
# Build and install
50+
make develop
51+
52+
# Run tests
53+
make test # All tests
54+
make test-fast # Skip slow tests
55+
56+
# Run benchmarks
57+
make benchmark # Synthetic data vs xmltodict
58+
make benchmark-real # Real ESMA FIRDS data (downloads 17MB)
59+
60+
# Test specific components
61+
pytest tests/test_basic.py # Core functionality
62+
pytest tests/test_xmltodict.py # Compatibility
63+
pytest tests/test_performance.py # Regression tests
64+
```
65+
66+
## Project Status
67+
68+
- **Complete**: Core functionality, xmltodict compatibility, test suite
69+
- **Tested**: Synthetic data, real-world XML files, edge cases
70+
- **Benchmarked**: Performance proven vs xmltodict
71+
- **Production ready**: Error handling, memory efficiency
72+
73+
## Files of Interest
74+
75+
- **`src/lib.rs`**: Main Rust implementation
76+
- **`xml_iterator/core.py`**: Python utilities and xml_to_dict
77+
- **`tests/test_xmltodict.py`**: Compatibility verification
78+
- **`benchmark_real_world.py`**: Real-world performance testing
79+
- **`benchmark.py`**: Synthetic benchmarks
80+
81+
## Known Limitations
82+
83+
- **Attributes ignored**: Only processes tag structure and text content
84+
- **Single file input**: No streaming from network/pipes (file paths only)
85+
- **Python-only bindings**: No other language bindings yet
86+
87+
## Infinite Depth Protection Strategy
88+
89+
The "infinite depth protection" is achieved through **streaming design**:
90+
- Events yielded immediately, no waiting for document completion
91+
- User controls termination via event counting or custom heuristics
92+
- Constant memory usage regardless of XML nesting depth
93+
- Examples: `n_max` parameter, early break in iteration
94+
95+
This is more flexible than hard depth limits since protection logic is use-case dependent.
96+
97+
## Dependencies
98+
99+
- **Rust**: quick-xml, pyo3, encoding_rs_io
100+
- **Python**: Standard library only (tests require pytest, xmltodict)
101+
- **Build**: maturin for Python extension compilation
102+
103+
## Testing Philosophy
104+
105+
- **Exact compatibility**: 100% identical results vs xmltodict
106+
- **Real-world data**: ESMA FIRDS regulatory XML files
107+
- **Performance regression**: Ensure no slowdowns
108+
- **Error resilience**: Graceful handling of malformed XML

Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "xml_iterator"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
[lib]
8+
name = "xml_iterator"
9+
crate-type = ["cdylib"]
10+
11+
[dependencies]
12+
encoding_rs = "0.8.31"
13+
encoding_rs_io = "0.1.7"
14+
pyo3 = { version = "0.17.1", features = ["extension-module"] }
15+
quick-xml = "0.26.0"

LICENSE.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

0 commit comments

Comments
 (0)