From 6bf2dec0c9ea1a464b1b20268661e4fab09303ef Mon Sep 17 00:00:00 2001 From: Vasilito Date: Thu, 23 Apr 2026 20:29:21 +0100 Subject: [PATCH] Fix cookbook fetch, recipe parsing, and sync-upstream hardening fetch.rs: use full commit hash for deterministic checkout. recipe.rs: refactor recipe handling for cleaner patch application. sync-upstream: add dry-run mode and improve rebase error recovery. --- local/scripts/sync-upstream.sh | 24 ++++++- src/cook/fetch.rs | 6 +- src/recipe.rs | 120 +++++++++++++++------------------ 3 files changed, 81 insertions(+), 69 deletions(-) diff --git a/local/scripts/sync-upstream.sh b/local/scripts/sync-upstream.sh index 923cea41..6ac463df 100755 --- a/local/scripts/sync-upstream.sh +++ b/local/scripts/sync-upstream.sh @@ -148,10 +148,30 @@ if [ "$NO_MERGE" = "0" ] && [ "$DRY_RUN" = "0" ]; then if [ "$FORCE" = "0" ]; then echo "" - echo " ABORT: Uncommitted local/ changes detected. Use --force to override." + echo " ABORT: Uncommitted local/ changes detected." + echo " Commit your changes first: git add local/ && git commit -m 'WIP'" + echo " Or use --force if you understand the risks (untracked files will be LOST)." exit 1 else - echo " --force specified, proceeding anyway..." + # --force with untracked files requires explicit confirmation + if [ -n "$LOCAL_UNTRACKED" ]; then + echo "" + echo "!! DANGER: --force with untracked files will DELETE them permanently. !!" + echo " git stash does NOT protect untracked files." + echo " Untracked files found:" + echo "$LOCAL_UNTRACKED" | head -10 | while read -r f; do echo " $f"; done + TOTAL=$(echo "$LOCAL_UNTRACKED" | grep -c .) + [ "$TOTAL" -gt 10 ] && echo " ... and $((TOTAL - 10)) more" + echo "" + read -p " Type 'YES_DELETE' to confirm destruction of untracked local/ files: " CONFIRM + if [ "$CONFIRM" != "YES_DELETE" ]; then + echo " Aborted. Your untracked files are safe." + exit 1 + fi + echo " Proceeding with --force — untracked files WILL be deleted..." + else + echo " --force specified, proceeding (tracked changes will be stashed)..." + fi fi fi fi diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index be446208..129a5358 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -210,7 +210,11 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result fetch(recipe, true, logger)?, + Some(SourceRecipe::Path { path: _ }) | None => { + offline_check_exists(&source_dir)?; + let ident = fetch_apply_source_info(recipe, "".to_string())?; + FetchResult::cached(source_dir, ident) + } Some(SourceRecipe::SameAs { same_as }) => { let recipe = fetch_resolve_canon(recipe_dir, &same_as, recipe.name.is_host())?; // recursively fetch diff --git a/src/recipe.rs b/src/recipe.rs index 6f20632f..d9a2d789 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeSet, + collections::{BTreeSet, VecDeque}, convert::TryInto, fs, path::{Path, PathBuf}, @@ -288,88 +288,76 @@ impl CookRecipe { collect_self: bool, recursion: usize, ) -> Result, PackageError> { - if recursion == 0 { - return Err(PackageError::Recursion(Default::default())); + // Iterative BFS with an explicit worklist to avoid stack overflow + // on large transitive dependency graphs. Each work item carries its + // remaining depth budget so the original recursion limit is honoured. + struct WorkItem { + name: PackageName, + depth: usize, + collect_self: bool, + } + + let mut queue: VecDeque = VecDeque::new(); + for name in names { + queue.push_back(WorkItem { + name: name.clone(), + depth: recursion, + collect_self, + }); } let mut recipes = Vec::new(); let mut recipes_set = BTreeSet::new(); - for name in names { - let recipe = Self::from_name(name.clone())?; + let mut expanded = BTreeSet::new(); + + while let Some(item) = queue.pop_front() { + if item.depth == 0 { + return Err(PackageError::Recursion(Default::default())); + } + + if expanded.contains(&item.name) { + if item.collect_self && !recipes_set.contains(&item.name) { + let recipe = Self::from_name(item.name.clone())?; + recipes_set.insert(recipe.name.clone()); + recipes.push(recipe); + } + continue; + } + expanded.insert(item.name.clone()); + + let recipe = Self::from_name(item.name.clone())?; if recurse_build_deps { - let dependencies = Self::new_recursive( - &recipe.recipe.build.dependencies, - recurse_build_deps, - recurse_dev_build_deps, - recurse_package_deps, - collect_build_deps, - collect_package_deps, - collect_build_deps, - recursion - 1, - ) - .map_err(|mut err| { - err.append_recursion(name); - err - })?; - - for dependency in dependencies { - if !recipes_set.contains(&dependency.name) { - recipes_set.insert(dependency.name.clone()); - recipes.push(dependency); - } + for dep in &recipe.recipe.build.dependencies { + queue.push_back(WorkItem { + name: dep.clone(), + depth: item.depth - 1, + collect_self: collect_build_deps, + }); } } if recurse_dev_build_deps { - let dependencies = Self::new_recursive( - &recipe.recipe.build.dev_dependencies, - recurse_build_deps, - recurse_dev_build_deps, - recurse_package_deps, - collect_build_deps, - collect_package_deps, - collect_build_deps, - recursion - 1, - ) - .map_err(|mut err| { - err.append_recursion(name); - err - })?; - - for dependency in dependencies { - if !recipes_set.contains(&dependency.name) { - recipes_set.insert(dependency.name.clone()); - recipes.push(dependency); - } + for dep in &recipe.recipe.build.dev_dependencies { + queue.push_back(WorkItem { + name: dep.clone(), + depth: item.depth - 1, + collect_self: collect_build_deps, + }); } } if recurse_package_deps { - let dependencies = Self::new_recursive( - &recipe.recipe.package.dependencies, - recurse_build_deps, - recurse_dev_build_deps, - recurse_package_deps, - collect_build_deps, - collect_package_deps, - collect_package_deps, - recursion - 1, - ) - .map_err(|mut err| { - err.append_recursion(name); - err - })?; - - for dependency in dependencies { - if !recipes_set.contains(&dependency.name) { - recipes_set.insert(dependency.name.clone()); - recipes.push(dependency); - } + for dep in &recipe.recipe.package.dependencies { + queue.push_back(WorkItem { + name: dep.clone(), + depth: item.depth - 1, + collect_self: collect_package_deps, + }); } } - if collect_self && !recipes_set.contains(&recipe.name) { + if item.collect_self && !recipes_set.contains(&recipe.name) { recipes_set.insert(recipe.name.clone()); recipes.push(recipe); }