jdx/mise · error

recovery directory descends through a file or symlink

Error message

recovery directory descends through a file or symlink

What it means

The Dir snapshot validation ensures no file or symlink entry is also an ancestor directory of another entry — otherwise restoring would need to create a file where a directory must exist (or vice versa). This bail means the recorded tree is self-contradictory: some recorded leaf path is claimed to contain other entries.

Source

Thrown at src/system/history/recovery.rs:181

                if relative.as_os_str().is_empty()
                    || !relative
                        .components()
                        .all(|c| matches!(c, Component::Normal(_)))
                    || !seen.insert(relative)
                {
                    bail!("invalid path inside recovery directory");
                }
            }
            for leaf in files
                .iter()
                .map(|f| &f.rel)
                .chain(links.iter().map(|f| &f.rel))
            {
                if seen
                    .iter()
                    .any(|other| *other != leaf && other.starts_with(leaf))
                {
                    bail!("recovery directory descends through a file or symlink");
                }
            }
            for file in files {
                read_blob(state_dir, &file.content)?;
            }
        }
        PathSnapshot::Unrecorded { reason, .. } => bail!("no usable preimage: {reason}"),
        _ => {}
    }
    Ok(())
}

fn remove_leaf(path: &Path) -> Result<()> {
    match std::fs::symlink_metadata(path) {
        Ok(meta) if meta.is_dir() => std::fs::remove_dir(path)?,
        Ok(_) => std::fs::remove_file(path)?,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(error.into()),

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Inspect the pending-operation JSON and resolve the conflicting entry (remove the file entry whose path is also a parent of another entry).
  2. Discard the corrupt record and accept current contents, restoring the tree manually.
  3. If reproducible without manual edits, report a capture bug: snapshot should never record a file and its descendant simultaneously.

Example fix

// before (contradictory snapshot)
[{"rel": "settings", "type": "file"}, {"rel": "settings/app.conf", ...}]

// after (consistent tree)
[{"rel": "settings/app.conf", "type": "file"}]
Defensive patterns

Strategy: validation

Validate before calling

// ensure no file/link rel is an ancestor of another entry
let rels: Vec<_> = files.iter().chain(links.iter()).map(|f| &f.rel).collect();
for leaf in &rels {
    assert!(!rels.iter().any(|o| *o != *leaf && o.starts_with(*leaf)), "tree contradiction at {leaf:?}");
}

Try / catch

match result { Err(e) if e.to_string().contains("descends through a file or symlink") => /* fix or discard the contradictory snapshot record */, other => other? }

Prevention

When it happens

Trigger: validate_snapshot finds a `rel` in files ∪ links such that another seen entry starts_with that path (e.g. both `a` recorded as a file and `a/b` recorded as a file beneath it).

Common situations: Corrupted or hand-edited snapshot JSON where an entry's type flipped between file and directory, or a state where the file type changed on disk between capture and journal write.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/e1520cd875764c34. Report an issue: GitHub.