1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
//! One-pass streaming schema validator implementation.
mod bound_automaton;
mod content;
mod event_handler;
mod identity;
mod lookup;
mod numeric;
mod occurrence;
mod symbols;
mod text;
use std::io::BufRead;
use std::sync::Arc;
use crate::error::{Result, StructuredError, ValidationErrorType};
use crate::event::StreamingParser;
use crate::schema::types::CompiledSchema;
use crate::schema::xsd::constraints::ConstraintValidator;
use super::ValidationMode;
use super::state::ValidationState;
/// Options for controlling which validations are performed.
///
/// By default, all validations are enabled. Disabling specific validations
/// can significantly improve performance for large documents.
#[derive(Debug, Clone, Default)]
#[doc(hidden)]
pub struct ValidationOptions {
/// Skip minOccurs validation (required child element checks).
/// Disabling this can improve performance but may miss missing required elements.
pub skip_min_occurs: bool,
/// Skip maxOccurs validation (element count limit checks).
/// Disabling this can significantly improve performance (~50%) but may miss
/// element count violations.
pub skip_max_occurs: bool,
/// Skip substitution group resolution in occurs validation.
/// Disabling this can improve performance but may cause false positives/negatives
/// for elements that use substitution groups.
pub skip_substitution_groups: bool,
/// Collapse identical errors into one entry with a count, keeping
/// memory bounded on error-dense documents.
pub aggregate_errors: bool,
}
/// One-pass streaming schema validator.
///
/// Validates XML documents against an XSD schema during streaming parsing
/// in a single pass. Best for memory-constrained environments or non-seekable streams.
#[doc(hidden)]
pub struct OnePassSchemaValidator {
pub(crate) schema: Arc<CompiledSchema>,
pub(crate) state: ValidationState,
pub(crate) errors: Vec<StructuredError>,
pub(crate) current_line: Option<usize>,
pub(crate) current_column: Option<usize>,
/// Constraint validator for identity constraints (unique, key, keyref)
pub(crate) constraint_validator: ConstraintValidator,
/// Validation mode (strict or lenient)
pub(crate) mode: ValidationMode,
/// Maximum number of errors to collect (0 = unlimited)
pub(crate) max_errors: usize,
/// Options for controlling which validations are performed
pub(crate) options: ValidationOptions,
/// `xs:ID` values seen in the document (for uniqueness checking)
pub(crate) seen_ids: rustc_hash::FxHashSet<String>,
/// `xs:IDREF` values with their locations, resolved at `finish()`
pub(crate) pending_idrefs: Vec<(String, Option<usize>, Option<usize>)>,
/// In-scope identity constraints being tracked
pub(crate) identity_scopes: Vec<identity::ScopeState>,
/// Memoized facet constraints per named simple type
pub(crate) facet_cache: crate::schema::xsd::facets::FacetCache,
/// Memoized inherited-element lists per complex type name
pub(crate) elements_cache:
rustc_hash::FxHashMap<String, std::sync::Arc<Vec<crate::schema::types::ElementDef>>>,
/// Memoized inherited-element lists per resolved `(namespace, local)` type
/// identity. The namespace-blind `elements_cache` (keyed by the bare
/// `type_ref` string) cannot distinguish same-local-name complex types in
/// different namespaces; this collision-free map keys on the compile-time
/// resolved [`NsName`](crate::schema::types::NsName).
pub(crate) elements_cache_ns: rustc_hash::FxHashMap<
crate::schema::types::NsName,
std::sync::Arc<Vec<crate::schema::types::ElementDef>>,
>,
/// Memoized collected attribute declarations per complex type, keyed by the
/// element's ns-safe type-identity symbol (C7). Keying on the resolved
/// identity rather than the bare `type_ref` string keeps same-local-name
/// types in different namespaces from sharing an entry.
pub(crate) attr_cache:
rustc_hash::FxHashMap<u32, std::sync::Arc<super::attributes::CollectedAttrs>>,
/// Memoized flattened-children resolution keyed by type reference string
/// (S2). Resolving a `type_ref` to its `FlattenedChildren` otherwise
/// allocates a two-`String` `NsName` on every element; this caches the
/// resolved `Arc` so the allocation happens once per distinct type.
pub(crate) type_ref_children: rustc_hash::FxHashMap<
String,
Option<std::sync::Arc<crate::schema::types::FlattenedChildren>>,
>,
/// Memoized inline (parent-content-model) element resolution keyed by
/// `(parent type symbol, child local symbol)` (S3). Resolving a child's
/// declared type from its parent's content model otherwise walks and
/// linearly scans the parent's flattened element list and clones its
/// `type_ref` on every element; this caches the resolved picture so it is
/// computed once per distinct (parent-type, child) pair.
pub(crate) inline_cache:
rustc_hash::FxHashMap<(u32, u32), std::sync::Arc<lookup::InlineResolved>>,
/// Memoized text-content validation plan keyed by type symbol (S5).
/// Deciding how a declared type's text must be checked otherwise costs a
/// `get_type` probe plus a facet-cache probe on every element close; this
/// caches the resolved [`text::TextOp`] once per distinct type.
pub(crate) text_op_cache: rustc_hash::FxHashMap<u32, text::TextOp>,
/// Symbol-bound content-model automatons keyed by the wrapped
/// automaton's `Arc` pointer (S6). Binding interns each position's name
/// set once, so per-child matching becomes `SymbolId` binary searches
/// instead of string-set hash probes.
pub(crate) bound_automatons:
rustc_hash::FxHashMap<usize, std::sync::Arc<bound_automaton::BoundAutomaton>>,
/// Interned error strings (messages repeat heavily on invalid files)
pub(crate) error_strings: rustc_hash::FxHashSet<std::sync::Arc<str>>,
/// (message, node_name) -> index into `errors`, used when
/// `aggregate_errors` is on.
pub(crate) aggregate_index: crate::error::ErrorAggregateIndex,
/// Interned element/attribute names (`SymbolId`s), so per-element
/// qualified names don't hash/allocate on every start tag. Holds both
/// qualified and local names in one namespace; see [`symbols`].
pub(crate) symbols: symbols::SymbolTable,
/// Reusable buffer for building qualified names.
pub(crate) qname_buf: String,
/// Anti-regression work counters (see [`ValidationCounters`]).
pub(crate) counters: super::ValidationCounters,
}
impl OnePassSchemaValidator {
/// Creates a new one-pass validator in strict mode.
pub fn new(schema: Arc<CompiledSchema>) -> Self {
Self {
schema,
state: ValidationState::new(),
errors: Vec::new(),
current_line: None,
current_column: Some(1),
constraint_validator: ConstraintValidator::new(),
mode: ValidationMode::Strict,
max_errors: 0,
options: ValidationOptions::default(),
seen_ids: rustc_hash::FxHashSet::default(),
pending_idrefs: Vec::new(),
identity_scopes: Vec::new(),
facet_cache: Default::default(),
elements_cache: Default::default(),
elements_cache_ns: Default::default(),
attr_cache: Default::default(),
type_ref_children: Default::default(),
inline_cache: Default::default(),
text_op_cache: Default::default(),
bound_automatons: Default::default(),
error_strings: Default::default(),
aggregate_index: Default::default(),
symbols: Default::default(),
qname_buf: String::new(),
counters: super::ValidationCounters::default(),
}
}
/// Sets the validation mode (builder pattern).
pub fn set_mode(mut self, mode: ValidationMode) -> Self {
self.mode = mode;
self
}
/// Sets the maximum number of errors to collect (builder pattern).
///
/// Set to 0 for unlimited errors (default).
/// Collapses identical errors into one entry with a count (builder).
pub fn with_aggregate_errors(mut self) -> Self {
self.options.aggregate_errors = true;
self
}
pub fn with_max_errors(mut self, max: usize) -> Self {
self.max_errors = max;
self
}
/// Validates an XML document from a reader and returns validation errors.
///
/// This is a convenience method that internally creates a `StreamingParser`,
/// runs validation, and returns the collected errors.
///
/// # Example
///
/// ```ignore
/// use std::fs::File;
/// use std::io::BufReader;
/// use std::sync::Arc;
/// use fastxml::schema::validator::OnePassSchemaValidator;
///
/// let file = File::open("document.xml")?;
/// let reader = BufReader::new(file);
///
/// let errors = OnePassSchemaValidator::new(schema)
/// .with_max_errors(100)
/// .validate(reader)?;
/// ```
/// Runs streaming validation, returning collected errors and the
/// accumulated [`ValidationCounters`](super::ValidationCounters). The
/// counters are used by the bench harness as an anti-regression guardrail.
pub(crate) fn validate_capturing<R: BufRead>(
self,
reader: R,
) -> Result<(Vec<StructuredError>, super::ValidationCounters)> {
let mut parser = StreamingParser::new(reader);
parser.add_handler(Box::new(self));
parser.parse()?;
// Extract the validator from the parser to get errors + counters
let handlers = parser.into_handlers();
for handler in handlers {
if let Ok(validator) = handler.as_any().downcast::<OnePassSchemaValidator>() {
let counters = validator.counters;
return Ok((validator.into_errors(), counters));
}
}
// Fallback: return empty errors (should not happen)
Ok((Vec::new(), super::ValidationCounters::default()))
}
/// Returns collected validation errors.
pub fn errors(&self) -> &[StructuredError] {
&self.errors
}
/// Takes ownership of collected errors.
pub fn into_errors(self) -> Vec<StructuredError> {
self.errors
}
pub(crate) fn should_collect_more(&self) -> bool {
self.max_errors == 0 || self.errors.len() < self.max_errors
}
pub(crate) fn add_error(&mut self, error: StructuredError) {
let error = error.interned(&mut self.error_strings);
if self.options.aggregate_errors {
// Identical errors collapse into the first occurrence's entry.
// Messages are interned, so equal content means equal Arcs.
let key = (
std::sync::Arc::clone(&error.message),
error.node_name.clone(),
);
if let Some(&idx) = self.aggregate_index.get(&key) {
self.errors[idx].record_occurrence(error.location.line, error.location.column);
return;
}
if self.should_collect_more() {
self.aggregate_index.insert(key, self.errors.len());
self.errors.push(error);
}
return;
}
if self.should_collect_more() {
self.errors.push(error);
}
}
pub(crate) fn make_error(
&self,
error_type: ValidationErrorType,
message: impl Into<String>,
) -> StructuredError {
let mut error = StructuredError::new(message, error_type);
if let Some(line) = self.current_line {
error = error.with_line(line);
}
if let Some(column) = self.current_column {
error = error.with_column(column);
}
error = error.with_element_path(self.state.element_path());
error
}
}
/// Error-introspection helpers used only by the in-crate engine tests.
#[cfg(test)]
impl OnePassSchemaValidator {
/// Convenience wrapper: runs validation and returns only the errors.
pub fn validate<R: BufRead>(self, reader: R) -> Result<Vec<StructuredError>> {
Ok(self.validate_capturing(reader)?.0)
}
/// Sets the maximum number of errors to collect (setter pattern).
pub fn set_max_errors(&mut self, max: usize) {
self.max_errors = max;
}
/// Returns only errors (excludes warnings).
pub fn errors_only(&self) -> Vec<&StructuredError> {
self.errors.iter().filter(|e| e.is_error()).collect()
}
/// Returns only warnings.
pub fn warnings(&self) -> Vec<&StructuredError> {
self.errors.iter().filter(|e| e.is_warning()).collect()
}
/// Returns true if validation passed without errors (warnings are OK).
pub fn is_valid(&self) -> bool {
!self.errors.iter().any(|e| e.is_error())
}
/// Returns true if there are no errors or warnings.
pub fn is_clean(&self) -> bool {
self.errors.is_empty()
}
/// Returns the error count (excluding warnings).
pub fn error_count(&self) -> usize {
self.errors.iter().filter(|e| e.is_error()).count()
}
/// Returns the warning count.
pub fn warning_count(&self) -> usize {
self.errors.iter().filter(|e| e.is_warning()).count()
}
}
#[cfg(test)]
mod tests;