{"record":{"id":"8ac7a29abb5f8bba","repo":"astrid-runtime/astrid","slug":"invalid-logical-destination-error","errorCode":null,"errorMessage":"invalid logical destination {}: {error}","messagePattern":"invalid logical destination (.+?): (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/principal_home_migration/publish.rs","lineNumber":26,"sourceCode":"    ContentName, ContiguousFileIngest, FilesystemEntryKind, FilesystemError, FilesystemPath,\n    RuntimePrincipalStore, StateOwner,\n};\n\nuse super::paths::{conflict_fs, conflict_path, storage_error};\nuse super::receipts::{EntryKind, MigrationEntry};\nuse super::{digest_file, ensure_directory, validate_regular_file, verify_file_content};\n\npub(super) fn publish_inventory(\n    store: &RuntimePrincipalStore,\n    filesystem: &super::HomeFilesystem,\n    uid: astrid_core::identity::PrincipalUid,\n    source: &Path,\n    entries: impl IntoIterator<Item = MigrationEntry>,\n) -> io::Result<()> {\n    let mut pending: BTreeMap<String, Vec<MigrationEntry>> = BTreeMap::new();\n    for entry in entries {\n        let destination = FilesystemPath::new(entry.destination.clone()).map_err(|error| {\n            io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"invalid logical destination {}: {error}\", entry.destination),\n            )\n        })?;\n        match entry.kind {\n            EntryKind::Directory => ensure_directory(filesystem, &destination)?,\n            EntryKind::File => {\n                let parent = destination\n                    .as_str()\n                    .rsplit_once('/')\n                    .map_or_else(String::new, |(parent, _)| parent.to_owned());\n                pending.entry(parent).or_default().push(entry);\n            },\n        }\n    }\n    for files in pending.values() {\n        publish_directory_files(store, filesystem, uid, source, files)?;\n    }","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/principal_home_migration/publish.rs#L8-L44","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: raw concatenation yields a non-canonical destination\nlet destination = format!(\"home/{legacy_raw}\"); // e.g. \"home/../etc\"\nMigrationEntry { destination, .. }\n\n// after: canonicalize the relative part first\nlet destination = destination_name(&logical_relative(&legacy_path)?);\nMigrationEntry { destination, .. }","handlingStrategy":"validation","validationCode":"fn entries_valid(entries: &[MigrationEntry]) -> bool {\n    entries.iter().all(|e|\n        !e.destination.is_empty()\n            && astrid_storage::FilesystemPath::new(e.destination.clone()).is_ok()\n    )\n}\n// gate publish_inventory behind this check\nif !entries_valid(&entries) { return Err(/* report offending destination */); }","typeGuard":"fn canonical_destination(e: &MigrationEntry) -> Option<astrid_storage::FilesystemPath> {\n    astrid_storage::FilesystemPath::new(e.destination.clone()).ok()\n}","tryCatchPattern":"match publish_inventory(&fs, &source, entries) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().starts_with(\"invalid logical destination\") => {\n        eprintln!(\"bad MigrationEntry.destination; fix producer and retry: {e}\");\n        // no files were written: publish aborts before any write\n    }\n    other => other?,\n}","preventionTips":["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."],"tags":["rust","io","path-validation","migration"],"backgroundTag":"invalid-argument-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}