{"record":{"id":"7d3a4e141f89f0a8","repo":"astrid-runtime/astrid","slug":"legacy-path-text-is-not-a-canonical-filesystem","errorCode":null,"errorMessage":"legacy path {text:?} is not a canonical filesystem path: {error}","messagePattern":"legacy path (.+?) is not a canonical filesystem path: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/principal_home_migration/paths.rs","lineNumber":107,"sourceCode":"    if path.is_absolute()\n        || path\n            .components()\n            .any(|component| !matches!(component, Component::Normal(_)))\n    {\n        return Err(invalid_source(\n            path,\n            \"legacy relative path is not canonical\",\n        ));\n    }\n    let text = path\n        .to_str()\n        .ok_or_else(|| invalid_source(path, \"legacy relative path is not UTF-8\"))?\n        .replace('\\\\', \"/\");\n    if text.len() > MAX_RELATIVE_PATH_BYTES {\n        return Err(invalid_source(path, \"legacy relative path is too long\"));\n    }\n    FilesystemPath::new(text.clone()).map_err(|error| {\n        io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"legacy path {text:?} is not a canonical filesystem path: {error}\"),\n        )\n    })?;\n    Ok(text)\n}\n\npub(super) fn destination_name(relative: &str) -> String {\n    format!(\"home/{relative}\")\n}\n\npub(super) fn is_dedicated_path(path: &Path) -> bool {\n    let components = path\n        .components()\n        .filter_map(|component| match component {\n            Component::Normal(value) => value.to_str(),\n            _ => None,\n        })","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/principal_home_migration/paths.rs#L89-L125","documentation":"Thrown by logical_relative in crates/astrid-kernel/src/principal_home_migration/paths.rs:107 when a legacy principal-home relative path, after passing the basic canonicality checks (relative, no . / .. / root components, UTF-8, length limit), still fails FilesystemPath::new validation in astrid-storage. It wraps the underlying FilesystemError, so the path cannot be represented as a canonical filesystem path (e.g. empty string, trailing/inner path rules, or other storage-layer invariants violated). The migration fails closed rather than publishing a non-canonical path into the new home layout.","triggerScenarios":"Calling migrate_legacy_principal_homes (directly or via walk_directory / retire_one_receipted_source) when a file or directory discovered under a legacy principal home produces a relative path that FilesystemPath::new rejects — e.g. paths with unusual separators left after the backslash normalization, embedded components astrid-storage considers non-canonical, or paths violating storage-layer canonical form rules not caught by the earlier checks.","commonSituations":"Migrating legacy homes whose on-disk trees were created by older or non-conforming tooling: symlinks resolved into odd forms, names with escaped or mixed separators, zero-length segments after normalization, or filesystems that permitted names the canonical storage model disallows.","solutions":["Inspect the path printed in the error and remove/rename the offending entry under the legacy home so the relative path is canonical (plain normal components, forward slashes).","Re-run the migration; if the path should be legal, verify astrid-storage's FilesystemPath::new canonicality rules and adjust the legacy tree to match.","If the entry is not needed, delete it from the legacy source directory before migration.","Ensure no symlinks or mount points inside the legacy home produce non-normal path components; restructure or bind-mount cleanly."],"exampleFix":"// before (legacy tree contains a non-canonical segment)\n/home/legacy-principal/.config/env//prod -> migration aborts\n\n// after (clean the legacy tree first)\nrm -r '/home/legacy-principal/.config/env//prod'\n# or rename so the relative path is canonical, then re-run migration","handlingStrategy":"validation","validationCode":"fn is_canonical_relative(path: &std::path::Path) -> bool {\n    !path.is_absolute()\n        && path.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n        && path.to_str().map(|t| t.replace('\\\\', \"/\").len() <= MAX_RELATIVE_PATH_BYTES).unwrap_or(false)\n}\n// pre-check each discovered legacy path before migration\nif !is_canonical_relative(&legacy_path) { skip_or_repair(&legacy_path); }","typeGuard":"fn is_utf8_canonical(p: &std::path::Path) -> Option<String> {\n    p.to_str().map(|t| t.replace('\\\\', \"/\")).filter(|t|\n        astrid_storage::FilesystemPath::new(t.clone()).is_ok()\n    )\n}","tryCatchPattern":"match migrate_legacy_principal_homes(&home, &fs, &source) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().contains(\"not a canonical filesystem path\") => {\n        eprintln!(\"repair legacy path, then retry: {e}\");\n    }\n    other => other?,\n}","preventionTips":["Keep legacy homes free of symlinks, '.', '..', and non-UTF-8 file names before migrating.","Normalize separators to '/' and validate every relative path with FilesystemPath::new in a dry-run pass first.","Enforce the MAX_RELATIVE_PATH_BYTES limit when creating legacy content, not only at migration time.","Run the migration on a copied snapshot of the legacy home so repairs don't touch live data."],"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"}