sxyazi/yazi · error · io::Error
InvalidInput
InvalidInput
Error message
invalid trash entry path
What it means
DsStore::join rebuilds a macOS trashed item's original absolute path by joining a relative entry under the put-back location from .DS_Store. The relative path must be relative and free of ".." components; absolute paths or parent-directory traversals are rejected with this InvalidInput error to prevent path traversal when restoring.
Source
Thrown at yazi-fs/src/trash/macos/ds_store.rs:38
let Value::Ustr(value) = record.value else { continue };
if value.is_empty() {
continue;
}
let location = locations.entry_ref(OsStr::new(&record.name)).or_default();
match &record.field.fourcc().bytes() {
b"ptbL" => location.parent = Some(value.into()),
b"ptbN" => location.name = Some(value.into()),
_ => {}
}
}
Ok(locations)
}
pub(super) fn join(&self, rel: &Path) -> io::Result<PathBuf> {
if !rel.is_relative() || rel.has_parent_component() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
}
let parent = self.parent.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location")
})?;
let name = self.name.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name")
})?;
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name"));
}
let top_path = Path::new("/").join(parent).join(name);
Ok(if rel.as_os_str().is_empty() { top_path } else { top_path.join(rel) })
}View on GitHub (pinned to 8ebf930f17)
Solutions
- Pass rel as the item's path relative to the trash directory (e.g. "folder/file.txt"), not an absolute path.
- Strip or reject any ".." components before calling join; recompute the relative path from the actual trash root.
- Ensure the item being restored actually resides inside the macOS trash so the relative computation is valid.
Example fix
// before
store.join(Path::new("/Users/alice/file.txt"))?
// after
let rel = item_path.strip_prefix(trash_dir)?;
store.join(rel)? Defensive patterns
Strategy: validation
Validate before calling
fn rel_is_safe(rel: &std::path::Path) -> bool {
rel.is_relative()
&& !rel.components().any(|c| matches!(c, std::path::Component::ParentDir | std::path::Component::RootDir))
} Try / catch
match ds_store.join(&rel) {
Ok(dest) => restore_to(dest),
Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => eprintln!("refusing non-relative or traversing trash entry path"),
Err(e) => return Err(e.into()),
} Prevention
- Compute rel via Path::strip_prefix(trash_dir) rather than passing user-supplied paths.
- Never pass absolute paths to join; only trash-relative components.
- Reject entries whose computed relative path contains '..'.
- Confirm the item actually lives inside the trash directory before restoring.
When it happens
Trigger: Calling join with an absolute rel path (e.g. "/foo/bar") or one containing ".." components (e.g. "../escape"); rel comes from the trashed item's path relative to ~/.Trash, so only malformed caller input or tampered layout triggers it.
Common situations: Custom restore logic passing full absolute paths instead of Trash-relative ones; items moved between trash folders so the relative computation is wrong; adversarial paths crafted to escape the trash.
Related errors
AI-assisted analysis of sxyazi/yazi@8ebf930f17 (2026-09-02).
Data as JSON: /api/errors/ebe417abf5a2480c.
Report an issue: GitHub.