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
- Inspect the printed destination string; fix the producer so entries are built via destination_name(relative) / canonical path joining rather than raw string concatenation.
- Sanitize the legacy relative path (strip '..', collapse separators) before constructing the MigrationEntry, so FilesystemPath::new accepts it.
- If entries come from a stored manifest/receipt, regenerate it with the current library version instead of hand-editing.
- 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
- Always derive destinations via destination_name(logical_relative(path)?) — never concatenate raw legacy strings.
- Validate all MigrationEntry.destination values with FilesystemPath::new before invoking publish_inventory.
- Regenerate entry manifests with the current library version instead of hand-editing stored plans.
- Add a unit test asserting every produced destination passes FilesystemPath::new.
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
- legacy path {text:?} is not a canonical filesystem path: {er
- legacy principal-home source {}: {detail}
- legacy audit source has no parent
- WouldBlock
- legacy audit retirement source is outside the default princi
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8ac7a29abb5f8bba.
Report an issue: GitHub.