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

layout migration source changed type: {}

Error message

layout migration source changed type: {}

What it means

While inventorying the migration source, the library opens each regular file with no-follow flags (O_NOFOLLOW / FILE_FLAG_OPEN_REPARSE_POINT) and re-checks the opened file's metadata. If the opened handle is no longer a regular file (e.g. the entry was swapped for a symlink or fifo between the initial readdir stat and the open), it aborts instead of hashing unexpected content. This is a TOCTOU guard protecting the integrity of the migration inventory digest.

Source

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

            hasher.update(&metadata.len().to_le_bytes());
            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(&child_path)?;
            if !file.metadata()?.is_file() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "layout migration source changed type: {}",
                        child_path.display()
                    ),
                ));
            }
            let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
            let mut file_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 inventory byte count overflow"))?;
                file_bytes = file_bytes

View on GitHub (pinned to affd8760f4)

Solutions

  1. Stop the process that is modifying the state directory and re-run the migration when the tree is quiescent.
  2. Check the reported path and restore it to a regular file with the expected content.
  3. Exclude the state directory from live-sync/backup tools, or pause them during migration.
  4. Retry the migration; the error is transient if it was caused by a concurrent swap.

Example fix

// before: rsync/backup daemon rewriting state files during migration
* * * * * rsync -a --delete ~/live/ ~/.local/state/astrid/
// after: pause syncs while migrating
systemctl --user stop backup.timer && astrid migrate && systemctl --user start backup.timer
Defensive patterns

Strategy: retry

Try / catch

match result {
    Err(e) if e.to_string().contains("layout migration source changed type") => {
        // pause writers/sync tools, then retry the migration
        retry_with_backoff(|| run_migration(), 3);
    }
    Err(e) => return Err(e),
    Ok(v) => v,
}

Prevention

When it happens

Trigger: inventory_directory opens a child with O_NOFOLLOW/REPARSE_POINT flags and file.metadata()?.is_file() is false — the entry changed from a regular file to something else (symlink, fifo, socket) between the directory scan and the open.

Common situations: Another process (backup, editor, sync daemon like Dropbox/Nextcloud) rewrites or replaces files mid-migration; a user manually deletes/recreates an entry as a symlink while a migration is running.

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/e9045e26af095b63. Report an issue: GitHub.