astrid-runtime/astrid · error

invalid logical destination {}: {error}

Error message

invalid logical destination {}: {error}

What it means

publish_inventory in crates/astrid-kernel/src/principal_home_migration/publish.rs:26 validates each MigrationEntry.destination with FilesystemPath::new before publishing it into the new home. If a destination string is not a valid canonical filesystem path (empty, non-canonical components, rule violations in astrid-storage), it raises this InvalidData error. This guards the write side: unlike error 1510 (legacy source paths), this fires for destinations computed or recorded by the migration plan itself, so it usually indicates a bug in entry construction rather than bad legacy data.

Source

Thrown at crates/astrid-kernel/src/principal_home_migration/publish.rs:26

    ContentName, ContiguousFileIngest, FilesystemEntryKind, FilesystemError, FilesystemPath,
    RuntimePrincipalStore, StateOwner,
};

use super::paths::{conflict_fs, conflict_path, storage_error};
use super::receipts::{EntryKind, MigrationEntry};
use super::{digest_file, ensure_directory, validate_regular_file, verify_file_content};

pub(super) fn publish_inventory(
    store: &RuntimePrincipalStore,
    filesystem: &super::HomeFilesystem,
    uid: astrid_core::identity::PrincipalUid,
    source: &Path,
    entries: impl IntoIterator<Item = MigrationEntry>,
) -> io::Result<()> {
    let mut pending: BTreeMap<String, Vec<MigrationEntry>> = BTreeMap::new();
    for entry in entries {
        let destination = FilesystemPath::new(entry.destination.clone()).map_err(|error| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("invalid logical destination {}: {error}", entry.destination),
            )
        })?;
        match entry.kind {
            EntryKind::Directory => ensure_directory(filesystem, &destination)?,
            EntryKind::File => {
                let parent = destination
                    .as_str()
                    .rsplit_once('/')
                    .map_or_else(String::new, |(parent, _)| parent.to_owned());
                pending.entry(parent).or_default().push(entry);
            },
        }
    }
    for files in pending.values() {
        publish_directory_files(store, filesystem, uid, source, files)?;
    }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the printed destination string; fix the producer so entries are built via destination_name(relative) / canonical path joining rather than raw string concatenation.
  2. Sanitize the legacy relative path (strip '..', collapse separators) before constructing the MigrationEntry, so FilesystemPath::new accepts it.
  3. If entries come from a stored manifest/receipt, regenerate it with the current library version instead of hand-editing.
  4. Re-run the migration after entries validate; the error aborts before any filesystem writes, so no partial publish needs cleanup.

Example fix

// before: raw concatenation yields a non-canonical destination
let destination = format!("home/{legacy_raw}"); // e.g. "home/../etc"
MigrationEntry { destination, .. }

// after: canonicalize the relative part first
let destination = destination_name(&logical_relative(&legacy_path)?);
MigrationEntry { destination, .. }
Defensive patterns

Strategy: validation

Validate before calling

fn entries_valid(entries: &[MigrationEntry]) -> bool {
    entries.iter().all(|e|
        !e.destination.is_empty()
            && astrid_storage::FilesystemPath::new(e.destination.clone()).is_ok()
    )
}
// gate publish_inventory behind this check
if !entries_valid(&entries) { return Err(/* report offending destination */); }

Type guard

fn canonical_destination(e: &MigrationEntry) -> Option<astrid_storage::FilesystemPath> {
    astrid_storage::FilesystemPath::new(e.destination.clone()).ok()
}

Try / catch

match publish_inventory(&fs, &source, entries) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData
        && e.to_string().starts_with("invalid logical destination") => {
        eprintln!("bad MigrationEntry.destination; fix producer and retry: {e}");
        // no files were written: publish aborts before any write
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling publish_inventory (from migrate_legacy_principal_homes / migrate_one_principal) with a MigrationEntry whose destination string fails FilesystemPath::new — e.g. a destination built with '..' segments, an empty string, double slashes, or a name violating storage canonicality rules — instead of passing through destination_name()/canonical joining.

Common situations: Hand-edited or externally generated migration entry manifests; code constructing MigrationEntry by concatenating raw legacy names without destination_name() canonicalization; entries deserialized from receipts written by a different (buggy or older) version whose destination encoding no longer validates.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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