Escape \r in text and \r/\n/\t in attributes during serde serialization - #1001
Escape \r in text and \r/\n/\t in attributes during serde serialization#1001dralley wants to merge 1 commit into
Conversation
4c98756 to
3a3a675
Compare
The serde serializer wrote literal \r into text content and \r, \n, \t into attribute values. These characters are silently modified by XML end-of-line normalization and attribute-value normalization respectively, causing data loss on round-trip. Now escape_list() escapes \r as &tafia#13; in text content at all QuoteLevels, and escapes \r/\n/\t as &tafia#13;/&tafia#10;/&tafia#9; in attribute values. This is consistent with the behavior of libxml2 / lxml. Make note of the fact that CDATA sections are inherently lossy for \r since character references are not permitted inside CDATA. closes tafia#670 closes tafia#990 Assisted-By: Claude Opus 4.6
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1001 +/- ##
==========================================
- Coverage 57.31% 56.02% -1.29%
==========================================
Files 46 47 +1
Lines 18197 18387 +190
==========================================
- Hits 10429 10302 -127
- Misses 7768 8085 +317
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Output from the serde API now matches libxml2 and roundtrips correctly use quick_xml::de::from_str;
use quick_xml::se::{to_string, Serializer};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "value")]
struct TextValue {
#[serde(rename = "$value")]
value: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct Root {
#[serde(rename = "@value")]
value: String,
}
fn main() {
println!("=== TEXT CONTENT ===\n");
// CR in text
let v = TextValue { value: "hello\rworld".into() };
let xml = to_string(&v).unwrap();
println!("CR in text - serialized: {:?}", xml);
let parsed: TextValue = from_str(&xml).unwrap();
println!("CR in text - parsed: {:?}", parsed.value);
println!("CR in text - roundtrip: {}\n", parsed == v);
// CRLF in text
let v = TextValue { value: "hello\r\nworld".into() };
let xml = to_string(&v).unwrap();
println!("CRLF in text - serialized: {:?}", xml);
let parsed: TextValue = from_str(&xml).unwrap();
println!("CRLF in text - parsed: {:?}", parsed.value);
println!("CRLF in text - roundtrip: {}\n", parsed == v);
// LF in text
let v = TextValue { value: "hello\nworld".into() };
let xml = to_string(&v).unwrap();
println!("LF in text - serialized: {:?}", xml);
let parsed: TextValue = from_str(&xml).unwrap();
println!("LF in text - parsed: {:?}", parsed.value);
println!("LF in text - roundtrip: {}\n", parsed == v);
// TAB in text
let v = TextValue { value: "col1\tcol2".into() };
let xml = to_string(&v).unwrap();
println!("TAB in text - serialized: {:?}", xml);
let parsed: TextValue = from_str(&xml).unwrap();
println!("TAB in text - parsed: {:?}", parsed.value);
println!("TAB in text - roundtrip: {}\n", parsed == v);
println!("=== ATTRIBUTE VALUES ===\n");
// CR in attr
let v = Root { value: "new\rline".into() };
let xml = to_string(&v).unwrap();
println!("CR in attr - serialized: {:?}", xml);
let parsed: Root = from_str(&xml).unwrap();
println!("CR in attr - parsed: {:?}", parsed.value);
println!("CR in attr - roundtrip: {}\n", parsed == v);
// CRLF in attr
let v = Root { value: "new\r\nline".into() };
let xml = to_string(&v).unwrap();
println!("CRLF in attr - serialized: {:?}", xml);
let parsed: Root = from_str(&xml).unwrap();
println!("CRLF in attr - parsed: {:?}", parsed.value);
println!("CRLF in attr - roundtrip: {}\n", parsed == v);
// LF in attr
let v = Root { value: "new\nline".into() };
let xml = to_string(&v).unwrap();
println!("LF in attr - serialized: {:?}", xml);
let parsed: Root = from_str(&xml).unwrap();
println!("LF in attr - parsed: {:?}", parsed.value);
println!("LF in attr - roundtrip: {}\n", parsed == v);
// TAB in attr
let v = Root { value: "col1\tcol2".into() };
let xml = to_string(&v).unwrap();
println!("TAB in attr - serialized: {:?}", xml);
let parsed: Root = from_str(&xml).unwrap();
println!("TAB in attr - parsed: {:?}", parsed.value);
println!("TAB in attr - roundtrip: {}\n", parsed == v);
println!("=== CDATA ===\n");
// CR in CDATA
let v = TextValue { value: "hello\rworld".into() };
let mut buffer = String::new();
let mut ser = Serializer::with_root(&mut buffer, Some("value")).unwrap();
ser.text_format(quick_xml::se::TextFormat::CData);
v.serialize(ser).unwrap();
println!("CR in CDATA - serialized: {:?}", buffer);
let parsed: TextValue = from_str(&buffer).unwrap();
println!("CR in CDATA - parsed: {:?}\n", parsed.value);
// CRLF in CDATA
let v = TextValue { value: "hello\r\nworld".into() };
let mut buffer = String::new();
let mut ser = Serializer::with_root(&mut buffer, Some("value")).unwrap();
ser.text_format(quick_xml::se::TextFormat::CData);
v.serialize(ser).unwrap();
println!("CRLF in CDATA - serialized: {:?}", buffer);
let parsed: TextValue = from_str(&buffer).unwrap();
println!("CRLF in CDATA - parsed: {:?}\n", parsed.value);
// LF in CDATA
let v = TextValue { value: "hello\nworld".into() };
let mut buffer = String::new();
let mut ser = Serializer::with_root(&mut buffer, Some("value")).unwrap();
ser.text_format(quick_xml::se::TextFormat::CData);
v.serialize(ser).unwrap();
println!("LF in CDATA - serialized: {:?}", buffer);
let parsed: TextValue = from_str(&buffer).unwrap();
println!("LF in CDATA - parsed: {:?}\n", parsed.value);
// Space in text
let v = TextValue { value: "with spaces".into() };
let xml = to_string(&v).unwrap();
println!("Space in text - serialized: {:?}", xml);
// Space in attr
let v = Root { value: "with spaces".into() };
let xml = to_string(&v).unwrap();
println!("Space in attr - serialized: {:?}", xml);
let parsed: Root = from_str(&xml).unwrap();
println!("Space in attr - parsed: {:?}", parsed.value);
println!("Space in attr - roundtrip: {}", parsed == v);
} |
|
So, this fixes #670 and #990 for the serde API, but it does not yet fix it for the raw For that, the Still working on that part. |
The serde serializer wrote literal \r into text content and \r, \n, \t into attribute values. These characters are silently modified by XML end-of-line normalization and attribute-value normalization respectively, causing data loss on round-trip.
Now escape_list() escapes \r as in text content at all QuoteLevels, and escapes \r/\n/\t as / / in attribute values. This is consistent with the behavior of libxml2 / lxml.
Make note of the fact that CDATA sections are inherently lossy for \r since character references are not permitted inside CDATA.
closes #670
closes #990
Assisted-By: Claude Opus 4.6