forked from 1b-agent/opencadstudio-upstream
fix: recovered_name must not smuggle path separators through the extension
recovered_name reduced only the head to its final path component, so a
stored name like "a.b/c" came back as "a (recovered).b/c". The whole
input is now sanitized first, and the empty-stem fallback uses the
sanitized component, which also restores idempotence for
separator-headed names ("/.dwg" used to alternate across crash cycles).
Ported from 9262d63f (#1717); its integer-precision fix landed with the
d719fb77 port, and the standalone tests/fuzz_round.rs suite (copies of
the functions rather than the code) stays out.
(cherry picked from commit 9262d63f)
This commit is contained in:
1 parent
1c0963b68d
commit
fb674fc45e
1 file changed
+26
-13
+26
-13
@@ -101,21 +101,21 @@ const RECOVERED_MARKER: &str = " (recovered)";
|
||||
/// recovered name.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||
pub(crate) fn recovered_name(drawing: &str) -> String {
|
||||
let (head, ext) = match drawing.rsplit_once('.') {
|
||||
Some((head, ext)) if !ext.is_empty() => (head, ext.to_string()),
|
||||
// No dot (or a trailing one): the whole name is the stem.
|
||||
_ => (drawing, "dxf".to_string()),
|
||||
};
|
||||
// A hostile stored name must not smuggle path separators into the tab
|
||||
// title / download name: keep only the final component of the head.
|
||||
let head = head.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||
// When the split consumed everything (".dxf") fall back to the whole
|
||||
// stored name, like `Path::file_stem`'s empty-stem fallback did.
|
||||
let mut stem = if head.is_empty() {
|
||||
drawing
|
||||
} else {
|
||||
head
|
||||
// title / download name — through the head OR the extension ("a.b/c"
|
||||
// keeps "/c" after the last dot): reduce the WHOLE input to its final
|
||||
// path component first, then split stem/extension.
|
||||
let name = drawing.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||
let (head, ext) = match name.rsplit_once('.') {
|
||||
Some((head, ext)) if !ext.is_empty() => (head, ext.to_string()),
|
||||
// No dot (or a trailing one): the whole component is the stem.
|
||||
_ => (name, "dxf".to_string()),
|
||||
};
|
||||
// An empty stem (".dxf" — a DXF name under the worker's rsplit
|
||||
// convention) keeps the whole component as the stem so the extension
|
||||
// does not flip, and the result stays idempotent because the marker
|
||||
// lives in the stem.
|
||||
let mut stem = if head.is_empty() { name } else { head };
|
||||
// Idempotent: strip every trailing marker, then append exactly one.
|
||||
while let Some(stripped) = stem.strip_suffix(RECOVERED_MARKER) {
|
||||
stem = stripped;
|
||||
@@ -311,6 +311,19 @@ mod tests {
|
||||
assert!(newer_than_saved(1.0, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_names_never_smuggle_separators_or_drift() {
|
||||
// Fuzz round: the extension could smuggle a path ("a.b/c") and the
|
||||
// empty-stem fallback reinstated the unsanitized input, breaking
|
||||
// idempotence across crash cycles.
|
||||
let once = recovered_name("a.b/c");
|
||||
assert_eq!(once, "c (recovered).dxf", "final path component, no separator survives");
|
||||
assert_eq!(recovered_name(&once), once, "idempotent");
|
||||
let dotfile = recovered_name("/.dwg");
|
||||
assert_eq!(dotfile, ".dwg (recovered).dwg");
|
||||
assert_eq!(recovered_name(&dotfile), dotfile, "idempotent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_names_keep_a_parseable_extension() {
|
||||
assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg");
|
||||
|
||||
Reference in new issue
Block a user