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

layout migration destination changed type: {}

Error message

layout migration destination changed type: {}

What it means

After opening the destination with reparse-point-safe flags (FILE_FLAG_OPEN_REPARSE_POINT on Windows), inventory_regular_file re-checks file.metadata() and requires it to still be a regular file. If the opened handle's metadata shows the entry changed type since the initial symlink_metadata check (TOCTOU race) or never was a regular file, this InvalidData error naming the path is raised.

Source

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

    crate::platform_fs::verify_no_redirects(path)?;
    let mut options = OpenOptions::new();
    options.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;

        options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_NONBLOCK);
    }
    #[cfg(windows)]
    {
        use std::os::windows::fs::OpenOptionsExt as _;
        use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;

        options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
    }
    let mut file = options.open(path)?;
    if !file.metadata()?.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "layout migration destination changed type: {}",
                path.display()
            ),
        ));
    }
    let mut hasher = blake3::Hasher::new_derive_key("astrid layout destination inventory v1");
    let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
    let mut bytes = 0_u64;
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        bytes = bytes
            .checked_add(read as u64)
            .ok_or_else(|| io::Error::other("layout destination length overflow"))?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Stop competing processes that touch the destination path, then re-run the migration.
  2. Inspect the path (`stat`, `fsutil reparsepoint query`) and restore it to a plain regular file.
  3. Re-run begin_layout_v2_migration on a stable system so the two checks agree.

Example fix

// before: sync daemon rewrites destination mid-inventory
// after: pause competing writers, then migrate
systemctl --user stop backup-sync  # then re-run begin_layout_v2_migration
Defensive patterns

Strategy: try-catch

Try / catch

match begin_layout_v2_migration(...) {
    Err(e) if e.to_string().contains("changed type") => {
        quiesce_writers(&dest_path);
        retry_with_backoff(begin_layout_v2_migration, 3);
    },
    other => other,
}

Prevention

When it happens

Trigger: Called from begin_layout_v2_migration / LayoutMigrationReceiptV1 when the destination entry is swapped (file -> directory, or a reparse point is resolved) between the initial symlink_metadata check and the opened file's metadata() call.

Common situations: A concurrent process (backup agent, sync tool, migration script) replaced or retyped the destination during inventory; Windows junction/symlink created over the path mid-migration; antivirus quarantine replaced the file.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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