astrid-runtime/astrid · error · io::Error

layout migration destination is redirected or not a regular…

Error message

layout migration destination is redirected or not a regular file: {}

What it means

verify_receipt_destination_authority validates that the migration receipt's recorded destination is an authoritative, unredirected regular file. It reads symlink_metadata on the destination's physical path and rejects it if the entry is a symlink or not a plain file. This guards against a receipt pointing at a path that an attacker or misconfiguration has redirected (e.g. symlink swap) or that was replaced by a directory or other non-file entry.

Solutions

  1. Remove the symlink at the destination path and replace it with the real regular file the receipt points to.
  2. Re-create or re-issue the layout migration receipt so its LayoutTreeIdentityV1 matches the current real on-disk path.
  3. Inspect the path with `ls -la` / `stat` to see what replaced the regular file and restore it (e.g. from the migration source).

Example fix

// before: destination replaced by symlink
ln -s /mnt/other/volume /path/to/astrid-volume
// after: real regular file at the destination
rm /path/to/astrid-volume
cp /mnt/other/volume /path/to/astrid-volume   # then re-run retirement
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(&dest_path)?;
if md.file_type().is_symlink() || !md.is_file() {
    return Err("receipt destination is not an unredirected regular file");
}

Try / catch

match retire_verified_legacy_source(...) {
    Err(e) if e.to_string().contains("redirected or not a regular file") => restore_real_destination_file(),
    Err(e) => return Err(e),
    Ok(()) => {},
}

Prevention

When it happens

Trigger: Called from verify_receipt_destination_is_live_path (itself called by retire_verified_legacy_source) whenever the path returned by physical_path(destination) is a symlink, directory, FIFO, socket, or any non-regular file when checked via std::fs::symlink_metadata.

Common situations: The Astrid volume path was replaced by a symlink (e.g. to a mount point or tmpfs alias) after the receipt was written; the destination file was deleted and a directory created with the same name; a backup/restore tool replaced the volume file with a symlink.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/a43cf4e078b7a937. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-core/src/dirs_layout_records.rs:183

    if actual != expected {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "layout migration record is not canonical: {}",
                path.display()
            ),
        ));
    }
    Ok(parsed)
}

pub(super) fn verify_receipt_destination_authority(
    destination: &LayoutTreeIdentityV1,
) -> io::Result<()> {
    let path = physical_path(destination)?;
    let metadata = std::fs::symlink_metadata(&path)?;
    if metadata.file_type().is_symlink() || !metadata.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "layout migration destination is redirected or not a regular file: {}",
                path.display()
            ),
        ));
    }
    crate::platform_fs::verify_no_redirects(&path)
}

pub(super) fn verify_receipt_destination_is_live_path(
    destination: &LayoutTreeIdentityV1,
    live_path: &Path,
) -> io::Result<()> {
    let parent = live_path.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "live Astrid volume has no parent",

View on GitHub (pinned to affd8760f4)