security(ui): bound export folder elision work (#2902)

* security(ui): bound export folder elision work

* export mode: keep location_link's doc comment on the function

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Earl Co
2026-10-23 10:52:45 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 441e80b7a6
commit d3ec344691
+54 -6
View File
@@ -401,6 +401,23 @@ fn draft_text(ui: &mut egui::Ui, reg: &mut Reg, id: &str, stored: &str, hint: &s
(r.changed() && !v.trim().is_empty()).then_some(v)
}
/// Longest folder tail the link lays out or shows on hover.
const MAX_LOCATION_DISPLAY_BYTES: usize = 4096;
/// Keep the tail of a UTF-8 folder name without copying or splitting a codepoint. Project item
/// names can enter the generated export path, so UI display work must not scale with an arbitrary
/// imported name.
fn location_tail(s: &str) -> (&str, bool) {
if s.len() <= MAX_LOCATION_DISPLAY_BYTES {
return (s, false);
}
let mut start = s.len() - MAX_LOCATION_DISPLAY_BYTES;
while !s.is_char_boundary(start) {
start += 1;
}
(&s[start..], true)
}
/// The output folder as a link (folder icon, accent colour, underlined); the caller opens the save
/// dialog on a click. A path too long for the row loses its start, so the folder's name stays.
fn location_link(ui: &mut egui::Ui, t: &Tokens, folder: &str) -> egui::Response {
@@ -408,18 +425,34 @@ fn location_link(ui: &mut egui::Ui, t: &Tokens, folder: &str) -> egui::Response
let col = if resp.hovered() { t.accent_hover } else { t.accent };
crate::icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 8.0, r.center().y), vec2(14.0, 14.0)), crate::icons::Icon::Folder, col);
let room = (r.width() - 22.0).max(0.0);
let (folder, truncated) = location_tail(folder);
let mut galley = ui.painter().layout_no_wrap(folder.to_string(), Tokens::ui(12.5), col);
for (i, _) in folder.char_indices().skip(1) {
if galley.size().x <= room {
break;
}
galley = ui.painter().layout_no_wrap(format!("…{}", folder.get(i..).unwrap_or("")), Tokens::ui(12.5), col);
if galley.size().x > room {
let starts: Vec<usize> = folder.char_indices().map(|(i, _)| i).collect();
let suffix = if starts.len() > 1 {
let mut lo = 1usize;
let mut hi = starts.len() - 1;
while lo < hi {
let mid = (lo + hi) / 2;
let candidate = format!("…{}", &folder[starts[mid]..]);
if ui.painter().layout_no_wrap(candidate, Tokens::ui(12.5), col).size().x <= room {
hi = mid;
} else {
lo = mid + 1;
}
}
format!("…{}", &folder[starts[lo]..])
} else {
"…".into()
};
galley = ui.painter().layout_no_wrap(suffix, Tokens::ui(12.5), col);
}
let at = pos2(r.min.x + 20.0, r.center().y - galley.size().y / 2.0);
let underline = at.y + galley.size().y;
ui.painter().hline(at.x..=at.x + galley.size().x, underline, egui::Stroke::new(1.0, col));
ui.painter().galley(at, galley, col);
resp.on_hover_cursor(egui::CursorIcon::PointingHand).on_hover_text(folder)
let hover = if truncated { format!("…{folder}") } else { folder.to_string() };
resp.on_hover_cursor(egui::CursorIcon::PointingHand).on_hover_text(hover)
}
const RATES: [(&str, FrameRate); 11] = [
@@ -1504,3 +1537,18 @@ fn list_row(ui: &mut egui::Ui, t: &Tokens, selected: bool, text: &str, width: f3
ui.painter().text(pos2(r.min.x + 6.0, r.center().y), Align2::LEFT_CENTER, text, Tokens::ui(12.5), if selected { Color32::WHITE } else { t.text });
resp
}
#[cfg(test)]
mod location_tests {
use super::*;
#[test]
fn location_tail_is_bounded_and_keeps_utf8_intact() {
let input = format!("/{}終", "é".repeat(MAX_LOCATION_DISPLAY_BYTES));
let (tail, truncated) = location_tail(&input);
assert!(truncated);
assert!(tail.len() <= MAX_LOCATION_DISPLAY_BYTES);
assert!(tail.ends_with('終'));
assert!(std::str::from_utf8(tail.as_bytes()).is_ok());
}
}