1#![forbid(unsafe_code)]
4
5use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
6
7pub const XML_SAVE_FORMAT: i32 = 1 << 0;
9pub const XML_SAVE_NO_DECL: i32 = 1 << 1;
10pub const XML_SAVE_NO_EMPTY: i32 = 1 << 2;
11pub const XML_SAVE_NO_XHTML: i32 = 1 << 3;
12pub const XML_SAVE_XHTML: i32 = 1 << 4;
13pub const XML_SAVE_AS_XML: i32 = 1 << 5;
14pub const XML_SAVE_AS_HTML: i32 = 1 << 6;
15pub const XML_SAVE_WSNONSIG: i32 = 1 << 7;
16pub const XML_SAVE_EMPTY: i32 = 1 << 8;
17pub const XML_SAVE_NO_INDENT: i32 = 1 << 9;
18pub const XML_SAVE_INDENT: i32 = 1 << 10;
19
20fn hex_ref(c: u32) -> String {
21 format!("&#x{c:X};")
22}
23
24fn escape_text(s: &str, attr: bool, non_ascii: bool) -> String {
25 escape_as(s, attr, non_ascii, false)
26}
27
28fn escape_as(s: &str, attr: bool, non_ascii: bool, html: bool) -> String {
36 let mut out = String::new();
37 for c in s.chars() {
38 let u = c as u32;
39 if non_ascii && u >= 0x80 {
40 out.push_str(&hex_ref(u));
41 continue;
42 }
43 match c {
44 '&' => out.push_str("&"),
45 '<' => out.push_str("<"),
46 '>' => out.push_str(">"),
47 '"' if attr => out.push_str("""),
48 '\r' => out.push_str(" "),
49 '\t' if attr => out.push_str("	"),
50 '\n' if attr => out.push_str(" "),
51 c if u < 0x20 && c != '\t' && c != '\n' => {
52 if html {
53 out.push(c);
54 } else {
55 out.push_str(&hex_ref(0xfffd));
56 }
57 }
58 c => out.push(c),
59 }
60 }
61 out
62}
63
64const SAVE_AS_HTML: i32 = 1 << 29;
67
68const SAVE_XHTML_EMPTY: i32 = 1 << 28;
70
71const VOID_ELEMENTS: &[&str] = &[
74 "area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img",
75 "input", "isindex", "link", "meta", "param", "source", "track", "wbr",
76];
77
78fn is_void(name: &str) -> bool {
79 VOID_ELEMENTS.iter().any(|v| name.eq_ignore_ascii_case(v))
80}
81
82fn qname(prefix: Option<&str>, local: &str) -> String {
83 match prefix {
84 Some(p) if !p.is_empty() => format!("{p}:{local}"),
85 _ => local.to_string(),
86 }
87}
88
89fn indent_unit() -> String {
90 std::env::var("XMLLINT_INDENT").unwrap_or_else(|_| " ".into())
91}
92
93const MAX_INDENT_CHARS: usize = 60;
98
99fn write_indent(out: &mut String, level: i32) {
100 let unit = indent_unit();
101 if unit.is_empty() || level <= 0 {
102 return;
103 }
104 let want = (level as usize).saturating_mul(unit.chars().count());
105 let mut written = 0;
106 while written < want.min(MAX_INDENT_CHARS) {
107 for c in unit.chars() {
108 if written >= MAX_INDENT_CHARS {
109 break;
110 }
111 out.push(c);
112 written += 1;
113 }
114 }
115}
116
117enum Step {
119 Open(NodeId, i32, bool),
121 Close(NodeId, i32, bool),
123 Indent(i32),
124 Newline,
125}
126
127fn write_node(doc: &XmlDoc, id: NodeId, out: &mut String, opts: i32, level: i32, format: bool) {
134 let mut stack: Vec<Step> = vec![Step::Open(id, level, format)];
135 while let Some(step) = stack.pop() {
136 match step {
137 Step::Indent(l) => write_indent(out, l),
138 Step::Newline => out.push('\n'),
139 Step::Close(id, level, child_format) => {
140 if child_format {
141 write_indent(out, level);
142 }
143 out.push_str("</");
144 out.push_str(&qname(doc.prefix(id), doc.name(id)));
145 out.push('>');
146 }
147 Step::Open(id, level, format) => {
148 write_one(doc, id, out, opts, level, format, &mut stack)
149 }
150 }
151 }
152}
153
154fn write_one(
156 doc: &XmlDoc,
157 id: NodeId,
158 out: &mut String,
159 opts: i32,
160 level: i32,
161 format: bool,
162 stack: &mut Vec<Step>,
163) {
164 match doc.kind(id) {
165 NodeKind::Element => {
166 out.push('<');
167 out.push_str(&qname(doc.prefix(id), doc.name(id)));
168 for (pre, href) in doc.ns_defs(id) {
169 out.push_str(" xmlns");
170 if let Some(p) = pre {
171 out.push(':');
172 out.push_str(p);
173 }
174 let non_ascii = doc.encoding.is_none();
175 out.push_str("=\"");
176 out.push_str(&escape_text(href, true, non_ascii));
177 out.push('"');
178 }
179 for a in doc.attrs(id) {
180 out.push(' ');
181 out.push_str(&qname(doc.prefix(a), doc.name(a)));
182 out.push_str("=\"");
183 out.push_str(&escape_as(
184 doc.content(a),
185 true,
186 doc.encoding.is_none(),
187 (opts & SAVE_AS_HTML) != 0,
188 ));
189 out.push('"');
190 }
191 let html = (opts & SAVE_AS_HTML) != 0;
192 if html && is_void(doc.name(id)) {
193 out.push('>');
198 return;
199 }
200 let has_kids = doc.first_child(id).is_some();
201 if !has_kids {
202 if (opts & XML_SAVE_NO_EMPTY) == 0 && !html {
205 if (opts & SAVE_XHTML_EMPTY) != 0 {
206 out.push(' ');
207 }
208 out.push_str("/>");
209 } else {
210 out.push_str("></");
211 out.push_str(&qname(doc.prefix(id), doc.name(id)));
212 out.push('>');
213 }
214 return;
215 }
216 let mixed = {
217 let mut c = doc.first_child(id);
218 let mut m = false;
219 while let Some(ch) = c {
220 if matches!(doc.kind(ch), NodeKind::Text | NodeKind::CData) {
221 m = true;
222 break;
223 }
224 c = doc.next_sibling(ch);
225 }
226 m
227 };
228 out.push('>');
229 let child_format = format && !mixed;
230 if child_format {
231 out.push('\n');
232 }
233 stack.push(Step::Close(id, level, child_format));
236 let mut c = doc.last_child(id);
237 while let Some(ch) = c {
238 if child_format {
239 stack.push(Step::Newline);
240 }
241 stack.push(Step::Open(ch, level + 1, child_format));
242 if child_format {
243 stack.push(Step::Indent(level + 1));
244 }
245 c = doc.prev_sibling(ch);
246 }
247 }
248 NodeKind::Text => {
249 out.push_str(&escape_as(
250 doc.content(id),
251 false,
252 doc.encoding.is_none(),
253 (opts & SAVE_AS_HTML) != 0,
254 ));
255 }
256 NodeKind::CData => {
257 let content = doc.content(id);
258 if content.is_empty() {
259 out.push_str("<![CDATA[]]>");
260 } else {
261 let bytes = content.as_bytes();
263 let mut start = 0usize;
264 let mut i = 0usize;
265 while i + 2 < bytes.len() {
266 if bytes[i] == b']' && bytes[i + 1] == b']' && bytes[i + 2] == b'>' {
267 out.push_str("<![CDATA[");
268 out.push_str(&content[start..=i + 1]);
269 out.push_str("]]>");
270 start = i + 2;
271 i += 2;
272 }
273 i += 1;
274 }
275 if start < content.len() {
276 out.push_str("<![CDATA[");
277 out.push_str(&content[start..]);
278 out.push_str("]]>");
279 }
280 }
281 }
282 NodeKind::Comment => {
283 let _ = (format, level);
284 out.push_str("<!--");
285 out.push_str(doc.content(id));
286 out.push_str("-->");
287 }
288 NodeKind::Pi => {
289 let _ = (format, level);
290 out.push_str("<?");
291 out.push_str(doc.name(id));
292 if !doc.content(id).is_empty() {
293 out.push(' ');
294 out.push_str(doc.content(id));
295 }
296 out.push_str("?>");
297 }
298 _ => {}
299 }
300}
301
302#[doc(alias = "xmlSaveDoc")]
304pub fn xml_save_doc(doc: &XmlDoc, options: i32) -> Vec<u8> {
305 let mut out = String::new();
306 if doc.kind(rusty_xml_tree::NodeId::DOCUMENT) == NodeKind::HtmlDocument {
311 return save_html_doc(doc, options | SAVE_AS_HTML);
312 }
313 if (options & XML_SAVE_NO_DECL) == 0 {
314 out.push_str("<?xml version=\"");
315 out.push_str(if doc.version.is_empty() {
316 "1.0"
317 } else {
318 &doc.version
319 });
320 out.push('"');
321 if let Some(enc) = &doc.encoding {
322 out.push_str(" encoding=\"");
323 out.push_str(enc);
324 out.push('"');
325 }
326 match doc.standalone {
327 Some(true) => out.push_str(" standalone=\"yes\""),
328 Some(false) => out.push_str(" standalone=\"no\""),
329 None => {}
330 }
331 out.push_str("?>\n");
332 }
333 write_doctype(doc, &mut out);
334 let mut options = options;
335 if is_xhtml(doc) {
336 options |= SAVE_XHTML_EMPTY;
337 }
338 let format = (options & XML_SAVE_FORMAT) != 0;
339 let mut child = doc.first_child(rusty_xml_tree::NodeId::DOCUMENT);
340 while let Some(id) = child {
341 write_node(doc, id, &mut out, options, 0, format);
342 out.push('\n');
343 child = doc.next_sibling(id);
344 }
345 out.into_bytes()
346}
347
348fn write_doctype(doc: &XmlDoc, out: &mut String) {
358 let Some(dtd) = doc.dtd.as_ref() else { return };
359 let Some(name) = dtd.name.as_deref() else { return };
360 out.push_str("<!DOCTYPE ");
361 out.push_str(name);
362 match (dtd.public_id.as_deref(), dtd.system_id.as_deref()) {
363 (Some(p), Some(s)) => {
364 out.push_str(" PUBLIC \"");
365 out.push_str(p);
366 out.push_str("\" \"");
367 out.push_str(s);
368 out.push('"');
369 }
370 (Some(p), None) => {
371 out.push_str(" PUBLIC \"");
372 out.push_str(p);
373 out.push('"');
374 }
375 (None, Some(s)) => {
376 out.push_str(" SYSTEM \"");
377 out.push_str(s);
378 out.push('"');
379 }
380 (None, None) => {}
381 }
382 if let Some(sub) = dtd.int_subset.as_deref() {
383 if !sub.trim().is_empty() {
384 out.push_str(" [");
385 out.push_str(sub);
386 out.push(']');
387 }
388 }
389 out.push_str(">\n");
390}
391
392fn is_xhtml(doc: &XmlDoc) -> bool {
396 let Some(dtd) = doc.dtd.as_ref() else {
397 return false;
398 };
399 let pubid = dtd.public_id.as_deref().unwrap_or("");
400 let sysid = dtd.system_id.as_deref().unwrap_or("");
401 pubid.starts_with("-//W3C//DTD XHTML") || sysid.contains("xhtml1")
402}
403
404#[doc(alias = "xmlDocDumpFormatMemory")]
406pub fn xml_doc_dump_format_memory(doc: &XmlDoc, format: bool) -> Vec<u8> {
407 xml_save_doc(doc, if format { XML_SAVE_FORMAT } else { 0 })
408}
409
410#[doc(alias = "xmlDocDumpMemory")]
412pub fn xml_doc_dump_memory(doc: &XmlDoc) -> Vec<u8> {
413 xml_save_doc(doc, 0)
414}
415
416#[doc(alias = "xmlNodeDump")]
418pub fn xml_node_dump(doc: &XmlDoc, node: NodeId, options: i32) -> Vec<u8> {
419 let mut out = String::new();
420 write_node(doc, node, &mut out, options, 0, (options & XML_SAVE_FORMAT) != 0);
421 out.into_bytes()
422}
423
424#[derive(Clone, Copy, PartialEq, Eq)]
425enum FrameKind {
426 Document,
427 Element,
428}
429
430struct Frame {
431 kind: FrameKind,
432 name: String,
433 prefix: Option<String>,
434 open: bool,
435 has_content: bool,
436}
437
438pub struct XmlTextWriter {
440 buf: String,
441 stack: Vec<Frame>,
442 indent: bool,
443 indent_unit: String,
444 started: bool,
445}
446
447impl Default for XmlTextWriter {
448 fn default() -> Self {
449 Self::xml_new_text_writer_memory()
450 }
451}
452
453impl XmlTextWriter {
454 #[doc(alias = "xmlNewTextWriterMemory")]
456 pub fn xml_new_text_writer_memory() -> Self {
457 Self {
458 buf: String::new(),
459 stack: Vec::new(),
460 indent: false,
461 indent_unit: indent_unit(),
462 started: false,
463 }
464 }
465
466 pub fn set_indent(&mut self, indent: bool) {
467 self.indent = indent;
468 }
469
470 fn close_start_tag(&mut self) {
471 if let Some(f) = self.stack.last_mut() {
472 if f.kind == FrameKind::Element && !f.open {
473 self.buf.push('>');
474 f.open = true;
475 f.has_content = true;
476 }
477 }
478 }
479
480 #[doc(alias = "xmlTextWriterStartDocument")]
482 pub fn start_document(
483 &mut self,
484 version: Option<&str>,
485 encoding: Option<&str>,
486 standalone: Option<&str>,
487 ) -> Result<(), String> {
488 self.buf.push_str("<?xml version=\"");
489 self.buf.push_str(version.unwrap_or("1.0"));
490 self.buf.push('"');
491 if let Some(e) = encoding {
492 self.buf.push_str(" encoding=\"");
493 self.buf.push_str(e);
494 self.buf.push('"');
495 }
496 if let Some(s) = standalone {
497 self.buf.push_str(" standalone=\"");
498 self.buf.push_str(s);
499 self.buf.push('"');
500 }
501 self.buf.push_str("?>\n");
502 self.stack.push(Frame {
503 kind: FrameKind::Document,
504 name: "#document".into(),
505 prefix: None,
506 open: true,
507 has_content: true,
508 });
509 self.started = true;
510 Ok(())
511 }
512
513 #[doc(alias = "xmlTextWriterStartElement")]
515 pub fn start_element(&mut self, name: &str) -> Result<(), String> {
516 self.start_element_ns(None, name, None)
517 }
518
519 #[doc(alias = "xmlTextWriterStartElementNS")]
521 pub fn start_element_ns(
522 &mut self,
523 prefix: Option<&str>,
524 name: &str,
525 ns_uri: Option<&str>,
526 ) -> Result<(), String> {
527 self.close_start_tag();
528 if self.indent && !self.buf.is_empty() && !self.buf.ends_with('\n') {
529 self.buf.push('\n');
530 }
531 if self.indent {
532 let depth = self.stack.iter().filter(|f| f.kind == FrameKind::Element).count();
533 for _ in 0..depth {
534 self.buf.push_str(&self.indent_unit);
535 }
536 }
537 self.buf.push('<');
538 self.buf.push_str(&qname(prefix, name));
539 if let Some(uri) = ns_uri {
540 if let Some(p) = prefix {
541 self.buf.push_str(" xmlns:");
542 self.buf.push_str(p);
543 } else {
544 self.buf.push_str(" xmlns");
545 }
546 self.buf.push_str("=\"");
547 self.buf.push_str(&escape_text(uri, true, false));
548 self.buf.push('"');
549 }
550 self.stack.push(Frame {
551 kind: FrameKind::Element,
552 name: name.to_string(),
553 prefix: prefix.map(str::to_string),
554 open: false,
555 has_content: false,
556 });
557 Ok(())
558 }
559
560 #[doc(alias = "xmlTextWriterWriteAttribute")]
562 pub fn write_attribute(&mut self, name: &str, value: &str) -> Result<(), String> {
563 let top = self.stack.last().ok_or_else(|| "no open element".to_string())?;
564 if top.kind != FrameKind::Element || top.open {
565 return Err("attribute after element content".into());
566 }
567 self.buf.push(' ');
568 self.buf.push_str(name);
569 self.buf.push_str("=\"");
570 self.buf.push_str(&escape_text(value, true, false));
571 self.buf.push('"');
572 Ok(())
573 }
574
575 #[doc(alias = "xmlTextWriterWriteAttributeNS")]
577 pub fn write_attribute_ns(
578 &mut self,
579 prefix: Option<&str>,
580 name: &str,
581 _ns_uri: Option<&str>,
582 value: &str,
583 ) -> Result<(), String> {
584 self.write_attribute(&qname(prefix, name), value)
585 }
586
587 #[doc(alias = "xmlTextWriterWriteString")]
589 pub fn write_string(&mut self, content: &str) -> Result<(), String> {
590 self.close_start_tag();
591 self.buf.push_str(&escape_text(content, false, false));
592 Ok(())
593 }
594
595 #[doc(alias = "xmlTextWriterWriteComment")]
597 pub fn write_comment(&mut self, content: &str) -> Result<(), String> {
598 self.close_start_tag();
599 self.buf.push_str("<!--");
600 self.buf.push_str(content);
601 self.buf.push_str("-->");
602 Ok(())
603 }
604
605 #[doc(alias = "xmlTextWriterWritePI")]
607 pub fn write_pi(&mut self, target: &str, data: Option<&str>) -> Result<(), String> {
608 self.close_start_tag();
609 self.buf.push_str("<?");
610 self.buf.push_str(target);
611 if let Some(d) = data {
612 self.buf.push(' ');
613 self.buf.push_str(d);
614 }
615 self.buf.push_str("?>");
616 Ok(())
617 }
618
619 #[doc(alias = "xmlTextWriterWriteCDATA")]
621 pub fn write_cdata(&mut self, content: &str) -> Result<(), String> {
622 self.close_start_tag();
623 self.buf.push_str("<![CDATA[");
624 self.buf.push_str(content);
625 self.buf.push_str("]]>");
626 Ok(())
627 }
628
629 #[doc(alias = "xmlTextWriterWriteRaw")]
631 pub fn write_raw(&mut self, content: &str) -> Result<(), String> {
632 self.close_start_tag();
633 self.buf.push_str(content);
634 Ok(())
635 }
636
637 #[doc(alias = "xmlTextWriterEndElement")]
639 pub fn end_element(&mut self) -> Result<(), String> {
640 let f = self.stack.pop().ok_or_else(|| "no open element".to_string())?;
641 if f.kind != FrameKind::Element {
642 return Err("end_element on document".into());
643 }
644 if !f.open {
645 self.buf.push_str("/>");
646 } else {
647 if self.indent && f.has_content {
648 }
650 self.buf.push_str("</");
651 self.buf.push_str(&qname(f.prefix.as_deref(), &f.name));
652 self.buf.push('>');
653 }
654 Ok(())
655 }
656
657 #[doc(alias = "xmlTextWriterEndDocument")]
659 pub fn end_document(&mut self) -> Result<(), String> {
660 while self
661 .stack
662 .last()
663 .map(|f| f.kind == FrameKind::Element)
664 .unwrap_or(false)
665 {
666 self.end_element()?;
667 }
668 if !self.buf.ends_with('\n') {
669 self.buf.push('\n');
670 }
671 self.stack.clear();
672 Ok(())
673 }
674
675 pub fn into_bytes(self) -> Vec<u8> {
676 self.buf.into_bytes()
677 }
678
679 pub fn as_str(&self) -> &str {
680 &self.buf
681 }
682}
683
684#[doc(alias = "xmlNewTextWriterMemory")]
686pub fn xml_new_text_writer_memory() -> XmlTextWriter {
687 XmlTextWriter::xml_new_text_writer_memory()
688}
689
690fn save_html_doc(doc: &XmlDoc, options: i32) -> Vec<u8> {
695 let mut out = String::new();
696 if (options & XML_SAVE_NO_DECL) == 0 {
697 out.push_str("<!DOCTYPE ");
698 let dtd = doc.dtd.as_ref();
699 out.push_str(dtd.and_then(|d| d.name.as_deref()).unwrap_or("html"));
700 match (
701 dtd.and_then(|d| d.public_id.as_deref()),
702 dtd.and_then(|d| d.system_id.as_deref()),
703 ) {
704 (Some(p), sys) => {
705 out.push_str(" PUBLIC \"");
706 out.push_str(p);
707 out.push('"');
708 if let Some(s) = sys {
709 out.push_str(" \"");
710 out.push_str(s);
711 out.push('"');
712 }
713 }
714 (None, Some(s)) => {
715 out.push_str(" SYSTEM \"");
716 out.push_str(s);
717 out.push('"');
718 }
719 (None, None) if dtd.is_none() => {
720 out.push_str(
722 " PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \
723 \"http://www.w3.org/TR/REC-html40/loose.dtd\"",
724 );
725 }
726 (None, None) => {}
727 }
728 out.push_str(">\n");
729 }
730 let format = (options & XML_SAVE_FORMAT) != 0;
731 let mut child = doc.first_child(rusty_xml_tree::NodeId::DOCUMENT);
732 while let Some(id) = child {
733 write_node(doc, id, &mut out, options, 0, format);
734 out.push('\n');
735 child = doc.next_sibling(id);
736 }
737 out.into_bytes()
738}