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
- Stop competing processes that touch the destination path, then re-run the migration.
- Inspect the path (`stat`, `fsutil reparsepoint query`) and restore it to a plain regular file.
- 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
- Stop backup/sync/AV agents that rewrite the volume before migrating.
- Run migration with exclusive access to the destination path.
- Re-run migration after any suspected concurrent modification — the check is a TOCTOU guard by design.
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
- layout migration destination changed while inventoried: {}
- layout migration destination is redirected or not a regular
- layout migration receipt destination path does not match the
- layout migration source is redirected or not a directory: {}
- layout migration source changed type: {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/87696dd103779f35.
Report an issue: GitHub.