{"record":{"id":"f8cd75b7c63bb7da","repo":"astrid-runtime/astrid","slug":"legacy-source-contains-redirect","errorCode":null,"errorMessage":"legacy source contains redirect: {}","messagePattern":"legacy source contains redirect: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":708,"sourceCode":"fn snapshot_dir(\n    root: &Path,\n    dir: &Path,\n    device: u64,\n    access: SourceAccess,\n    hasher: &mut blake3::Hasher,\n    identity: &mut SourceInventory,\n) -> io::Result<()> {\n    let mut children = fs::read_dir(dir)\n        .map_err(io::Error::other)?\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(io::Error::other)?;\n    children.sort_by_key(std::fs::DirEntry::file_name);\n    for entry in children {\n        let path = entry.path();\n        let relative = path.strip_prefix(root).map_err(io::Error::other)?;\n        let metadata = fs::symlink_metadata(&path).map_err(io::Error::other)?;\n        if metadata.file_type().is_symlink() {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"legacy source contains redirect: {}\", path.display()),\n            ));\n        }\n        validate_source_entry(&path, &metadata, access)?;\n        if device_id(&metadata) != device {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\n                    \"legacy source crosses a device boundary: {}\",\n                    path.display()\n                ),\n            ));\n        }\n        if active_mountpoint(&path)? {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"legacy source is an active mount: {}\", path.display()),","sourceCodeStart":690,"sourceCodeEnd":726,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L690-L726","documentation":"During recursive directory snapshotting, snapshot_dir examines every child with symlink_metadata (no-follow). Any child that is a symlink is rejected as a \"redirect\" with InvalidData, because a symlink could point anywhere and would make the snapshot digest unrepresentative and the migration unsafe.","triggerScenarios":"Calling snapshot_path_with_access / snapshot_owner_controlled_path on a directory whose tree contains a symlink at any depth (snapshot_dir iterates children recursively).","commonSituations":"Legacy config directories containing convenience symlinks (e.g. current -> v2, rc -> rc.d); shared library or plugin dirs with symlinked versions; user-created aliases inside a state directory.","solutions":["Replace the symlink with a real copy of its target (cp -L --remove-destination), then re-run the snapshot.","Remove the symlink if it is not needed.","Exclude the symlinked entry from the source directory and snapshot a symlink-free tree.","If the symlink is intentional, copy the whole tree to a staging directory dereferencing symlinks and snapshot that."],"exampleFix":"// before\nlegacy_dir/\n  current -> v2/        # symlink: snapshot_dir rejects it\n\n// after\ncp -rL legacy_dir legacy_dir_flat   # dereference symlinks into real files\nrm legacy_dir/current && mv legacy_dir_flat/v2 legacy_dir/current  # or just snapshot legacy_dir_flat","handlingStrategy":"validation","validationCode":"fn tree_has_symlinks(root: &std::path::Path) -> std::io::Result<bool> {\n    for entry in walkdir::WalkDir::new(root).follow_links(false) {\n        let entry = entry?;\n        if std::fs::symlink_metadata(entry.path())?.file_type().is_symlink() {\n            return Ok(true);\n        }\n    }\n    Ok(false)\n}\nassert!(!tree_has_symlinks(&src_dir)?, \"tree contains symlinks\");","typeGuard":"fn symlink_free_tree(root: &std::path::Path) -> bool {\n    !tree_has_symlinks(root).unwrap_or(true)\n}","tryCatchPattern":"match snapshot_owner_controlled_path(&dir) {\n    Err(e) if e.to_string().contains(\"contains redirect\") => {\n        let flat = copy_dereferencing(&dir, \"/tmp/astrid-flat\")?; // cp -rL equivalent\n        snapshot_owner_controlled_path(&flat)\n    }\n    other => other,\n}","preventionTips":["Scan the tree with `find <dir> -type l` before snapshotting.","Replace intentional symlinks with real copies in legacy directories.","Avoid snapshotting directories that hold version-alias symlinks.","Keep symlinks outside the migration source tree."],"tags":["filesystem","symlink","snapshot","rust"],"backgroundTag":"incompatible-source-type","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}