astrid-runtime/astrid · error

legacy source is not a regular file: {}

Error message

legacy source is not a regular file: {}

What it means

validate_source_path checks a legacy migration source path before it is used. It takes symlink_metadata (which does not follow symlinks) and requires the entry to be a plain regular file; a symlink or any non-file (directory, socket, fifo, device node) fails this check with InvalidData. The library does this so migration digests bind to real file bytes, not to redirectable or non-regular entries.

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:662

            format!("legacy source contains a special entry: {}", path.display()),
        ));
    }
    SourceIdentity::present(
        SourceDigest::from_blake3(hasher.finalize()),
        identity.entries,
        identity.bytes,
    )
    .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}

/// Validate one regular-file source using the same no-follow, private,
/// same-device, and mount checks as recursive snapshots.  Distro lock files
/// keep their component-owned digest format, so they use this structural
/// check instead of the generic tree hash.
pub(super) fn validate_source_path(path: &Path) -> io::Result<()> {
    let metadata = fs::symlink_metadata(path)?;
    if metadata.file_type().is_symlink() || !metadata.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy source is not a regular file: {}", path.display()),
        ));
    }
    astrid_core::platform_fs::verify_no_redirects(path)?;
    validate_private_entry(path, &metadata)?;
    if active_mountpoint(path)? {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy source is an active mount: {}", path.display()),
        ));
    }
    if let Some(parent) = path.parent()
        && let Ok(parent_metadata) = fs::symlink_metadata(parent)
        && device_id(&parent_metadata) != device_id(&metadata)
    {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,

View on GitHub (pinned to affd8760f4)

Solutions

  1. Resolve the path to its final target and pass the real regular file (e.g. fs::canonicalize, then re-check it is a file).
  2. ls -la the path and confirm it is a regular file, not a symlink or directory.
  3. If the target is a directory, use the directory snapshot path (snapshot_owner_controlled_path / snapshot_path_with_access) instead of the file validator.
  4. Recreate or restore the regular file at that path if a symlink or special file was placed there unintentionally.

Example fix

// before
let src = Path::new("/etc/astrid.lock"); // actually a symlink to /usr/share/astrid/astrid.lock
validate_source_path(src)?;

// after
let src = std::fs::canonicalize("/etc/astrid.lock")?;
let md = std::fs::metadata(&src)?;
assert!(md.is_file());
validate_source_path(&src)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_regular_nonsymlink(path: &std::path::Path) -> std::io::Result<bool> {
    let md = std::fs::symlink_metadata(path)?;
    Ok(!md.file_type().is_symlink() && md.is_file())
}
if !is_regular_nonsymlink(&src)? {
    return Err(format!("{} must be a regular file, not a symlink/dir", src.display()));
}

Type guard

fn is_plain_file(path: &std::path::Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|md| !md.file_type().is_symlink() && md.is_file())
        .unwrap_or(false)
}

Try / catch

match validate_source_path(&src) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        let real = std::fs::canonicalize(&src).unwrap_or(src.clone());
        validate_source_path(&real).map_err(|e2| e2)?; // retry against resolved target
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling validate_source_path (or the distro lock-file validation flow that uses it) with a path that is a symlink, a directory, or another special file type.

Common situations: Passing a convenience symlink like ~/.config/foo instead of the real file; pointing at a directory when a lock file path was expected; a special file (fifo/socket) created at the expected path; a distro that replaced a regular lock 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/e9f86ba44b06695a. Report an issue: GitHub.