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.
|
/// recovered name.
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||||
pub(crate) fn recovered_name(drawing: &str) -> String {
|
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
|
// A hostile stored name must not smuggle path separators into the tab
|
||||||
// title / download name: keep only the final component of the head.
|
// title / download name — through the head OR the extension ("a.b/c"
|
||||||
let head = head.rsplit(['/', '\\']).next().unwrap_or_default();
|
// keeps "/c" after the last dot): reduce the WHOLE input to its final
|
||||||
// When the split consumed everything (".dxf") fall back to the whole
|
// path component first, then split stem/extension.
|
||||||
// stored name, like `Path::file_stem`'s empty-stem fallback did.
|
let name = drawing.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||||
let mut stem = if head.is_empty() {
|
let (head, ext) = match name.rsplit_once('.') {
|
||||||
drawing
|
Some((head, ext)) if !ext.is_empty() => (head, ext.to_string()),
|
||||||
} else {
|
// No dot (or a trailing one): the whole component is the stem.
|
||||||
head
|
_ => (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.
|
// Idempotent: strip every trailing marker, then append exactly one.
|
||||||
while let Some(stripped) = stem.strip_suffix(RECOVERED_MARKER) {
|
while let Some(stripped) = stem.strip_suffix(RECOVERED_MARKER) {
|
||||||
stem = stripped;
|
stem = stripped;
|
||||||
@@ -311,6 +311,19 @@ mod tests {
|
|||||||
assert!(newer_than_saved(1.0, None));
|
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]
|
#[test]
|
||||||
fn recovered_names_keep_a_parseable_extension() {
|
fn recovered_names_keep_a_parseable_extension() {
|
||||||
assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg");
|
assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg");
|
||||||
|
|||||||
Reference in new issue
Block a user