mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-09 20:09:38 -06:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd562c016c | ||
|
|
f16a8a93ed | ||
|
|
d0bf9d5934 | ||
|
|
a990af3942 | ||
|
|
1b4dad9082 |
@@ -16,12 +16,6 @@ const INVALID_REQUEST: i64 = -32600;
|
||||
const METHOD_NOT_FOUND: i64 = -32601;
|
||||
const INVALID_PARAMS: i64 = -32602;
|
||||
const INTERNAL_ERROR: i64 = -32603;
|
||||
|
||||
/// Entries one batch (a JSON-RPC batch, or the `batch` tool's commands) may hold.
|
||||
pub(crate) const MAX_BATCH_ITEMS: usize = 1000;
|
||||
/// Bytes of results one batch may gather before the rest of it is refused: each result can copy
|
||||
/// much of the document (an inspection holds every note), so a batch holds at most this much.
|
||||
pub(crate) const MAX_BATCH_BYTES: usize = 64 << 20;
|
||||
const RESOURCE_NOT_FOUND: i64 = -32002;
|
||||
/// Keep request frames in step with the connected backend's reply limit.
|
||||
const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
|
||||
@@ -99,28 +93,8 @@ impl Server {
|
||||
// Batches were removed in 2025-06-18; still answer older clients sensibly.
|
||||
if batch.is_empty() {
|
||||
Some(error(Value::Null, INVALID_REQUEST, "empty batch"))
|
||||
} else if batch.len() > MAX_BATCH_ITEMS {
|
||||
Some(error(Value::Null, INVALID_REQUEST, format!("a batch holds at most {MAX_BATCH_ITEMS} messages")))
|
||||
} else {
|
||||
let mut used = 0usize;
|
||||
let mut replies: Vec<Value> = Vec::new();
|
||||
for m in batch {
|
||||
if used > MAX_BATCH_BYTES {
|
||||
// Not run: answer each remaining request with a short error.
|
||||
if let Some(id) = m.get("id").filter(|id| id.is_string() || id.is_number()) {
|
||||
replies.push(error(id.clone(), INTERNAL_ERROR, "the batch's replies passed 64 MiB; not run"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = self.handle(m) {
|
||||
used = used.saturating_add(r.to_string().len());
|
||||
replies.push(if used > MAX_BATCH_BYTES {
|
||||
error(r.get("id").cloned().unwrap_or(Value::Null), INTERNAL_ERROR, "the batch's replies passed 64 MiB; reply dropped")
|
||||
} else {
|
||||
r
|
||||
});
|
||||
}
|
||||
}
|
||||
let replies: Vec<Value> = batch.into_iter().filter_map(|m| self.handle(m)).collect();
|
||||
(!replies.is_empty()).then_some(Value::Array(replies))
|
||||
}
|
||||
}
|
||||
|
||||
+9
-21
@@ -357,30 +357,18 @@ pub fn call_tool(b: &mut dyn Backend, name: &str, a: &Value) -> ToolResult {
|
||||
wrap(exec(b, c, a.get("params").cloned().unwrap_or(json!({}))))
|
||||
}
|
||||
"batch" => {
|
||||
use crate::server::{MAX_BATCH_BYTES, MAX_BATCH_ITEMS};
|
||||
let keep = a.get("keepGoing").and_then(Value::as_bool).unwrap_or(false);
|
||||
let commands = a.get("commands").and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default();
|
||||
if commands.len() > MAX_BATCH_ITEMS {
|
||||
return ToolResult::error(format!("a batch holds at most {MAX_BATCH_ITEMS} commands"));
|
||||
}
|
||||
let mut out = vec![];
|
||||
let mut used = 0usize;
|
||||
for c in commands {
|
||||
for c in a.get("commands").and_then(Value::as_array).cloned().unwrap_or_default() {
|
||||
let id = c.get("command").and_then(Value::as_str).unwrap_or("");
|
||||
let (entry, failed) = match exec(b, id, c.get("params").cloned().unwrap_or(json!({}))) {
|
||||
Ok(v) => (json!({"command": id, "ok": true, "result": v}), false),
|
||||
Err(e) => (json!({"command": id, "ok": false, "error": e}), true),
|
||||
};
|
||||
// What the reply carries: the entry as text, escaped again inside the JSON-RPC reply.
|
||||
// Failures count too: with keepGoing, a remote backend's large errors add up.
|
||||
used = used.saturating_add(json!(entry.to_string()).to_string().len());
|
||||
if used > MAX_BATCH_BYTES {
|
||||
out.push(json!({"command": id, "ok": false, "error": "the batch's results passed 64 MiB; stopped here"}));
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
}
|
||||
out.push(entry);
|
||||
if failed && !keep {
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
match exec(b, id, c.get("params").cloned().unwrap_or(json!({}))) {
|
||||
Ok(v) => out.push(json!({"command": id, "ok": true, "result": v})),
|
||||
Err(e) => {
|
||||
out.push(json!({"command": id, "ok": false, "error": e}));
|
||||
if !keep {
|
||||
return ToolResult { content: vec![json!({"type": "text", "text": Value::Array(out).to_string()})], is_error: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolResult::json(&Value::Array(out))
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//! A batch (JSON-RPC or the `batch` tool) is bounded in entries and in the bytes of results it
|
||||
//! gathers, so a short request cannot copy a large document into memory hundreds of times.
|
||||
|
||||
use deckcraft_mcp::{Headless, Server};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn server_with_notes(bytes: usize) -> Server {
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let line = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "run_command", "arguments": {"command": "slide.notes", "params": {"text": "n".repeat(bytes)}}}});
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&line.to_string()).unwrap()).unwrap();
|
||||
assert_ne!(reply["result"]["isError"], true, "{reply}");
|
||||
server
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_rpc_batches_are_bounded_in_entries_and_reply_bytes() {
|
||||
let mut server = server_with_notes(1 << 20);
|
||||
let ping = |i: usize| json!({"jsonrpc": "2.0", "id": i, "method": "ping"});
|
||||
let too_many: Vec<Value> = (0..1001).map(ping).collect();
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&Value::Array(too_many).to_string()).unwrap()).unwrap();
|
||||
assert_eq!(reply["error"]["code"], -32600, "{reply}");
|
||||
// 100 inspections of a slide with 1 MB of notes: about 64 are answered, the rest refused.
|
||||
let inspect = |i: usize| json!({"jsonrpc": "2.0", "id": i, "method": "tools/call", "params": {"name": "inspect_slide", "arguments": {}}});
|
||||
let line = Value::Array((0..100).map(inspect).collect()).to_string();
|
||||
let reply = server.handle_line(&line).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
let replies: Vec<Value> = serde_json::from_str(&reply).unwrap();
|
||||
assert_eq!(replies.len(), 100, "every request is answered");
|
||||
assert!(replies.last().unwrap()["error"]["message"].as_str().unwrap().contains("64 MiB"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_is_bounded_in_commands_and_result_bytes() {
|
||||
let mut server = server_with_notes(1 << 20);
|
||||
let call = |server: &mut Server, commands: Vec<Value>| {
|
||||
let line = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply: Value = serde_json::from_str(&server.handle_line(&line.to_string()).unwrap()).unwrap();
|
||||
(reply["result"]["isError"] == true, reply["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string())
|
||||
};
|
||||
let (err, text) = call(&mut server, vec![json!({"command": "document.inspect"}); 1001]);
|
||||
assert!(err && text.contains("at most 1000"), "{text}");
|
||||
let (err, text) = call(&mut server, vec![json!({"command": "slide.inspect"}); 100]);
|
||||
assert!(err && text.contains("64 MiB"), "{}", &text[text.len().saturating_sub(200)..]);
|
||||
assert!(text.len() < (64 << 20) + (4 << 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_charges_the_escaped_reply() {
|
||||
// Quotes escape twice on the way out (`\\\"`), so 30 results of ~2 MiB serialize to ~120 MiB.
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let notes = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "run_command", "arguments": {"command": "slide.notes", "params": {"text": "\"".repeat(1 << 20)}}}});
|
||||
server.handle_line(¬es.to_string()).unwrap();
|
||||
let commands = vec![json!({"command": "slide.inspect"}); 30];
|
||||
let line = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply = server.handle_line(&line.to_string()).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
assert!(reply.contains("64 MiB"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_batch_tool_charges_failures_too() {
|
||||
// Each unknown command's error echoes its ~1 MiB id; with keepGoing none of them is a result.
|
||||
let mut server = Server::new(Box::new(Headless::with_document()));
|
||||
let id = "x".repeat(1 << 20);
|
||||
let commands = vec![json!({"command": id}); 70];
|
||||
let line = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "batch", "arguments": {"commands": commands, "keepGoing": true}}});
|
||||
let reply = server.handle_line(&line.to_string()).unwrap();
|
||||
assert!(reply.len() < (64 << 20) + (4 << 20), "{} bytes", reply.len());
|
||||
assert!(reply.contains("64 MiB"));
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md).
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod automation_roots;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod batch_budget;
|
||||
|
||||
+159
-24
@@ -100,8 +100,7 @@ impl Part {
|
||||
}
|
||||
/// Like [`Part::keep`], also declaring prefixes that only an enclosing wrapper binds.
|
||||
pub fn keep_inner(&self, el: &El) -> String {
|
||||
let ns: Vec<(String, String)> = self.ns.iter().chain(&self.inner_ns).cloned().collect();
|
||||
keep_xml(el, &ns)
|
||||
keep_with(el, &[&self.ns, &self.inner_ns])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,36 +135,69 @@ fn inner_decls(root: &El, ns: &[(String, String)]) -> Vec<(String, String)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Serialize `el`, declaring every prefix it uses that the source root declared.
|
||||
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
|
||||
let mut used = HashSet::new();
|
||||
fn rec(e: &El, used: &mut HashSet<String>, depth: usize) {
|
||||
/// Longest namespace name copied onto a kept fragment or a resolved `mc` branch. Real ones are
|
||||
/// URIs of well under 100 bytes; a longer one is not copied, so one declaration cannot be
|
||||
/// multiplied by every fragment that uses its prefix.
|
||||
const MAX_NS_VALUE: usize = 2048;
|
||||
|
||||
/// Prefixes `el` and its descendants use in element and attribute names, and those listed in
|
||||
/// `mc:Ignorable` or `mc:MustUnderstand` (which must stay declared for the markup-compatibility rules to hold),
|
||||
/// and those starting a QName-like attribute value.
|
||||
fn used_prefixes(el: &El) -> HashSet<&str> {
|
||||
fn rec<'a>(e: &'a El, used: &mut HashSet<&'a str>, depth: usize) {
|
||||
if depth > crate::xml::MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let Some((p, _)) = e.name.split_once(':') {
|
||||
used.insert(p.to_string());
|
||||
used.insert(p);
|
||||
}
|
||||
for (k, _) in &e.attrs {
|
||||
if let Some((p, _)) = k.split_once(':')
|
||||
for (k, v) in &e.attrs {
|
||||
if let Some((p, local)) = k.split_once(':')
|
||||
&& p != "xmlns"
|
||||
&& p != "xml"
|
||||
{
|
||||
used.insert(p.to_string());
|
||||
used.insert(p);
|
||||
if local == "Ignorable" || local == "MustUnderstand" {
|
||||
used.extend(v.split_whitespace());
|
||||
}
|
||||
}
|
||||
// QName-valued attributes (`xsi:type="q:Item"`) need their prefix declared too.
|
||||
if k != "xmlns" && !k.starts_with("xmlns:") {
|
||||
for t in v.split_whitespace() {
|
||||
if let Some((p, _)) = t.split_once(':')
|
||||
&& !p.is_empty()
|
||||
{
|
||||
used.insert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in e.elements() {
|
||||
rec(c, used, depth + 1);
|
||||
}
|
||||
}
|
||||
let mut used = HashSet::new();
|
||||
rec(el, &mut used, 0);
|
||||
let mut el = el.clone();
|
||||
for (k, v) in ns {
|
||||
used
|
||||
}
|
||||
|
||||
/// Serialize `el`, declaring every prefix it uses that the source root declared.
|
||||
pub fn keep_xml(el: &El, ns: &[(String, String)]) -> String {
|
||||
keep_with(el, &[ns])
|
||||
}
|
||||
|
||||
fn keep_with(el: &El, decls: &[&[(String, String)]]) -> String {
|
||||
let used = used_prefixes(el);
|
||||
let mut have: HashSet<&str> = el.attrs.iter().map(|(a, _)| a.as_str()).collect();
|
||||
let mut extra = Vec::new();
|
||||
for (k, v) in decls.iter().flat_map(|d| d.iter()) {
|
||||
let p = k.strip_prefix("xmlns:").unwrap_or("");
|
||||
if !p.is_empty() && used.contains(p) && !el.attrs.iter().any(|(a, _)| a == k) {
|
||||
el.attrs.push((k.clone(), v.clone()));
|
||||
if !p.is_empty() && used.contains(p) && v.len() <= MAX_NS_VALUE && have.insert(k) {
|
||||
extra.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
let mut el = el.clone();
|
||||
el.attrs.extend(extra);
|
||||
el.to_xml()
|
||||
}
|
||||
|
||||
@@ -314,10 +346,18 @@ fn kept_parts(pkg: &Package, pres_path: &str) -> Vec<deckcraft_model::KeptPart>
|
||||
// The part's own relationships (customXml item → itemProps) and their targets.
|
||||
let rp = opc::rels_path(&name);
|
||||
if let Some(rels_xml) = text(&rp) {
|
||||
let mut capped = false;
|
||||
for sub in pkg.rels(&name).items.iter().filter(|s| !s.external && keepable(&s.target)) {
|
||||
if let Some(x) = text(&sub.target)
|
||||
&& !out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target))
|
||||
{
|
||||
// Checked before the part is read: repeated relationships to one large part
|
||||
// must not copy it again each time.
|
||||
if out.iter().any(|k| k.name.eq_ignore_ascii_case(&sub.target)) {
|
||||
continue;
|
||||
}
|
||||
if out.len() >= 400 {
|
||||
capped = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = text(&sub.target) {
|
||||
out.push(KeptPart {
|
||||
content_type: pkg.content_type(&sub.target).unwrap_or_default(),
|
||||
name: sub.target.clone(),
|
||||
@@ -326,7 +366,9 @@ fn kept_parts(pkg: &Package, pres_path: &str) -> Vec<deckcraft_model::KeptPart>
|
||||
});
|
||||
}
|
||||
}
|
||||
out.push(KeptPart { name: rp, xml: rels_xml, ..Default::default() });
|
||||
if !capped {
|
||||
out.push(KeptPart { name: rp, xml: rels_xml, ..Default::default() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,15 +405,39 @@ fn resolve_mc_rels(el: &mut El, depth: usize, rels: Option<&Rels>) {
|
||||
});
|
||||
let branch = choice.or_else(|| ac.child("Fallback"));
|
||||
if let Some(b) = branch {
|
||||
// The branch's namespace declarations (the first usable one of each name,
|
||||
// in order), indexed by prefix once: each child then looks up the prefixes
|
||||
// it uses instead of scanning every declaration.
|
||||
let mut decls: Vec<(&String, &String)> = Vec::new();
|
||||
// An oversized declaration still shadows an outer one of its name: its
|
||||
// content is left unbound rather than bound to the outer namespace.
|
||||
let mut by_prefix: HashMap<&str, Option<usize>> = HashMap::new();
|
||||
for (k, v) in b.attrs.iter().chain(ac.attrs.iter()) {
|
||||
let prefix = if k == "xmlns" { Some("") } else { k.strip_prefix("xmlns:") };
|
||||
if let Some(p) = prefix
|
||||
&& !by_prefix.contains_key(p)
|
||||
{
|
||||
by_prefix.insert(p, (v.len() <= MAX_NS_VALUE).then_some(decls.len()));
|
||||
if v.len() <= MAX_NS_VALUE {
|
||||
decls.push((k, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
for n in &b.children {
|
||||
if let Node::El(e) = n {
|
||||
// Carry the namespace declarations from the branch that its content
|
||||
// uses (all of them per child made children x declarations copies).
|
||||
let used = used_prefixes(e);
|
||||
let have: HashSet<&str> = e.attrs.iter().map(|(a, _)| a.as_str()).collect();
|
||||
let mut wanted: Vec<usize> = std::iter::once("")
|
||||
.chain(used.iter().copied())
|
||||
.filter_map(|p| by_prefix.get(p).copied().flatten())
|
||||
.filter(|&i| !have.contains(decls[i].0.as_str()))
|
||||
.collect();
|
||||
wanted.sort_unstable();
|
||||
wanted.dedup();
|
||||
let mut e = e.clone();
|
||||
// Carry namespace declarations from the branch onto its content.
|
||||
for (k, v) in b.attrs.iter().chain(ac.attrs.iter()) {
|
||||
if k.starts_with("xmlns") && !e.attrs.iter().any(|(a, _)| a == k) {
|
||||
e.attrs.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
e.attrs.extend(wanted.into_iter().map(|i| (decls[i].0.clone(), decls[i].1.clone())));
|
||||
out.push(Node::El(e));
|
||||
}
|
||||
}
|
||||
@@ -1414,3 +1480,72 @@ fn header_footer(p: &Presentation) -> HeaderFooter {
|
||||
}
|
||||
hf
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mc_branches_carry_only_the_declarations_their_content_uses() {
|
||||
// 2,000 declarations on the branch and 2,000 children: each child gets the one it uses
|
||||
// (and the default), not all 2,000.
|
||||
let decls: String = (0..2000).map(|i| format!(r#" xmlns:n{i}="urn:n{i}""#)).collect();
|
||||
let kids: String = (0..2000).map(|i| format!("<n{i}:x/>")).collect();
|
||||
let x = format!(
|
||||
r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns="urn:d"{decls}>{kids}</mc:Fallback></mc:AlternateContent></r>"#
|
||||
);
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kids: Vec<&El> = d.root.elements().collect();
|
||||
assert_eq!(kids.len(), 2000);
|
||||
assert_eq!(kids[7].attrs, vec![("xmlns".to_string(), "urn:d".to_string()), ("xmlns:n7".to_string(), "urn:n7".to_string())]);
|
||||
}
|
||||
|
||||
/// The branch's own declaration of a prefix wins over the `AlternateContent`'s; an oversized
|
||||
/// one still shadows it (its content stays unbound); a child's own declaration is left alone.
|
||||
#[test]
|
||||
fn mc_branch_declarations_keep_their_precedence() {
|
||||
let long = "u".repeat(MAX_NS_VALUE + 1);
|
||||
let x = format!(
|
||||
r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent xmlns:p="urn:ac" xmlns:q="urn:q-short"><mc:Fallback xmlns:p="urn:b" xmlns:q="{long}"><p:x/><q:y/><p:z xmlns:p="urn:own"/></mc:Fallback></mc:AlternateContent></r>"#
|
||||
);
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kids: Vec<&El> = d.root.elements().collect();
|
||||
let attrs = |i: usize| kids[i].attrs.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>();
|
||||
assert_eq!(attrs(0), ["xmlns:p=urn:b"]);
|
||||
assert!(attrs(1).is_empty(), "{:?}", attrs(1));
|
||||
assert_eq!(attrs(2), ["xmlns:p=urn:own"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mc_branches_keep_prefixes_named_by_must_understand() {
|
||||
let x = r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns:q="urn:q"><a mc:MustUnderstand="q"/></mc:Fallback></mc:AlternateContent></r>"#;
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kid = d.root.elements().next().unwrap();
|
||||
assert!(kid.attrs.iter().any(|(k, v)| k == "xmlns:q" && v == "urn:q"), "{:?}", kid.attrs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mc_branches_keep_prefixes_used_in_qname_values() {
|
||||
let x = r#"<r xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:AlternateContent><mc:Fallback xmlns:q="urn:q" xmlns:z="urn:z"><n xmlns:xsi="urn:xsi" xsi:type="q:Item"/></mc:Fallback></mc:AlternateContent></r>"#;
|
||||
let mut d = crate::xml::parse(x.as_bytes()).unwrap();
|
||||
resolve_mc(&mut d.root, 0);
|
||||
let kid = d.root.elements().next().unwrap();
|
||||
assert!(kid.attrs.iter().any(|(k, v)| k == "xmlns:q" && v == "urn:q"), "{:?}", kid.attrs);
|
||||
assert!(!kid.attrs.iter().any(|(k, _)| k == "xmlns:z"), "unused declarations stay out: {:?}", kid.attrs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kept_fragments_skip_oversized_namespace_names() {
|
||||
let big = "u".repeat(MAX_NS_VALUE + 1);
|
||||
let ns = vec![("xmlns:a".to_string(), "urn:a".to_string()), ("xmlns:b".to_string(), big)];
|
||||
let d = crate::xml::parse(br#"<a:x xmlns:a="urn:a" xmlns:b="urn:b"><b:y/></a:x>"#).unwrap();
|
||||
let mut el = d.root.clone();
|
||||
el.attrs.clear();
|
||||
let out = keep_xml(&el, &ns);
|
||||
assert!(out.contains(r#"xmlns:a="urn:a""#), "{out}");
|
||||
assert!(!out.contains("uuuu"), "the oversized declaration is not copied");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user