Merge pull request #3890 from dylanmtaylor/fix/audit-engine-mcp-limits

Security: export names, linked-file ids, MCP prompt arguments and image encoders
This commit is contained in:
BFlat
2026-10-24 04:50:38 +07:00
committed by GitHub
5 changed files with 34 additions and 11 deletions
+7 -5
View File
@@ -1366,15 +1366,17 @@ pub(crate) fn unique_file_names<'a>(names: impl IntoIterator<Item = (&'a str, St
/// `names` with `-2`, `-3`… after a name already used (compared without case).
fn distinct_names(names: impl IntoIterator<Item = String>) -> Vec<String> {
let mut taken = std::collections::HashSet::new();
// The next number to try for each base: a name repeated many times doesn't search the numbers
// taken before it.
let mut next: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
names
.into_iter()
.map(|base| {
let mut name = base.clone();
for i in 2.. {
if taken.insert(name.to_lowercase()) {
break;
}
name = format!("{base}-{i}");
while !taken.insert(name.to_lowercase()) {
let n = next.entry(base.to_lowercase()).or_insert(1);
*n += 1;
name = format!("{base}-{n}");
}
name
})
+3 -1
View File
@@ -182,6 +182,8 @@ pub(crate) fn linked_groups(d: &Document) -> Vec<(String, Vec<u64>)> {
fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec<Group> {
let mut out: Vec<Group> = vec![];
let mut index: BTreeMap<(String, String, bool), usize> = BTreeMap::new();
// Looked up for every linked node: a set, not the list (which may repeat an id many times).
let wanted: Option<std::collections::HashSet<NodeId>> = ids.map(|ids| ids.iter().copied().collect());
let mut add = |link: &LinkInfo, key: &str, document: Option<Shown>, id: Option<NodeId>| {
let i = *index.entry((link.path.clone(), key.to_string(), document.is_some())).or_insert_with(|| {
out.push(Group { link: link.clone(), key: key.to_string(), ids: vec![], document });
@@ -193,7 +195,7 @@ fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec<Group> {
};
d.walk(|n| {
if let Some((l, key, document)) = linked(n)
&& ids.is_none_or(|ids| ids.contains(&n.id))
&& wanted.as_ref().is_none_or(|ids| ids.contains(&n.id))
{
add(l, key, document, Some(n.id));
}
+19 -2
View File
@@ -405,7 +405,10 @@ pub fn get(name: &str, args: &Value) -> Result<Value, String> {
let ids: Vec<&str> = PROMPTS.iter().map(|p| p.name).collect();
return Err(format!("unknown prompt `{name}` ({})", ids.join("|")));
};
let mut text = flow(p.text);
let template = flow(p.text);
// (placeholder, value): filled in one pass over the template, so a value that looks like a
// placeholder stays as it is.
let mut values: Vec<(String, String)> = vec![];
for a in p.arguments {
let given = args.get(a.name).filter(|v| !v.is_null()).map(|v| match v {
Value::String(s) => s.clone(),
@@ -416,7 +419,21 @@ pub fn get(name: &str, args: &Value) -> Result<Value, String> {
_ if a.required => return Err(format!("prompt `{name}` needs the `{a_name}` argument", a_name = a.name)),
_ => a.default.to_string(),
};
text = text.replace(&format!("{{{}}}", a.name), &value);
values.push((format!("{{{}}}", a.name), value));
}
let mut text = String::with_capacity(template.len());
let mut rest = template.as_str();
while let Some(c) = rest.chars().next() {
match values.iter().find(|(placeholder, _)| rest.starts_with(placeholder.as_str())) {
Some((placeholder, value)) => {
text.push_str(value);
rest = rest.get(placeholder.len()..).unwrap_or_default();
}
None => {
text.push(c);
rest = rest.get(c.len_utf8()..).unwrap_or_default();
}
}
}
Ok(json!({
"description": p.description,
+3 -1
View File
@@ -197,7 +197,9 @@ pub fn quantize_locked(rgba: &[u8], width: u32, height: u32, o: &PaletteOptions,
}
let weights = o.reduction.weights();
let dither = if exact { Dither::None } else { o.dither };
let mut indices = map(&colors, width as usize, &palette, weights, dither, f32::from(o.dither_amount.min(100)) / 100.0, |i| clear(&px[i]));
// (A width past the pixels there are would size the error rows from a number nothing checked.)
let mut indices =
map(&colors, (width as usize).min(colors.len()), &palette, weights, dither, f32::from(o.dither_amount.min(100)) / 100.0, |i| clear(&px[i]));
// Drop the entries no pixel uses (fixed palettes), keeping the order.
let mut used = [false; 256];
for (i, &ix) in indices.iter().enumerate() {
+2 -2
View File
@@ -34,11 +34,11 @@ pub fn encode(rgba: &[u8], width: u32, height: u32, o: &TgaOptions) -> Result<Ve
return Err(format!("{width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution"));
};
let (w, h) = (usize::from(w16), usize::from(h16));
if w == 0 || h == 0 || w * h * 4 != rgba.len() {
if w == 0 || h == 0 || (w * h).checked_mul(4) != Some(rgba.len()) {
return Err("Targa encoding failed: the pixel buffer doesn't match the image size".into());
}
let px = rgba.as_chunks::<4>().0;
let mut out = Vec::with_capacity(18 + w * h * usize::from(o.depth / 8) + 26);
let mut out = Vec::with_capacity((w * h).saturating_mul(usize::from(o.depth / 8)).saturating_add(18 + 26));
// No image id or colour map, uncompressed true colour, origin (0, 0).
out.extend([0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
out.extend(w16.to_le_bytes());