fix: harden the bundle lifecycle per code review
The path-escape guard that uninstall applies to recorded paths now also covers update's obsolete-file deletion through a shared check, so a tampered store cannot turn either delete site into an arbitrary file removal. Updates gain a working non-interactive path: --yes now applies to --update-bundle (locally modified files, obsolete files, and modified mcp entries are all kept; everything else refreshes), owned mcp entries whose recorded hash still matches the local entry take the remote side without prompting, and the non-TTY conflict bails name the flag that actually works per surface. An update records its new commit and version only after files and mcp entries land, so an aborted update cannot claim content it never wrote. The store gains a version field and rejects stores from newer builds, the corrupt-store error no longer advises the removal that would forfeit ownership tracking, and duplicate records tracking one source abort a rename instead of overwriting a record. Reinstalling from a source URL reclassifies owned unmodified files as silent refreshes just like updates. git runs with GIT_TERMINAL_PROMPT=0 and a null stdin so private or mistyped URLs fail instead of hanging. File comparison fills buffers fully before comparing, deleting an obsolete file prunes emptied directories, mcp.json backfill uses the fsynced atomic writer, --list-bundles no longer triggers builtin backfill, bundle-name completion logs store errors instead of swallowing them and offers --yes, and REPL .uninstall rejects unknown flags.
This commit is contained in:
+42
-1
@@ -100,14 +100,19 @@ pub(crate) struct ResolvedBundleName {
|
||||
pub(crate) same_name_other_source: Option<String>,
|
||||
}
|
||||
|
||||
const STORE_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct StoreContents {
|
||||
#[serde(default)]
|
||||
version: u32,
|
||||
#[serde(default)]
|
||||
bundles: BTreeMap<String, BundleRecord>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StoreContentsRef<'a> {
|
||||
version: u32,
|
||||
bundles: &'a BTreeMap<String, BundleRecord>,
|
||||
}
|
||||
|
||||
@@ -137,10 +142,19 @@ impl BundleStore {
|
||||
let contents: StoreContents = serde_yaml::from_str(&content).with_context(|| {
|
||||
format!(
|
||||
"failed to parse {}; refusing to treat it as empty. \
|
||||
Fix or remove the file to continue",
|
||||
Restore or repair the file to continue; removing it forfeits \
|
||||
uninstall tracking for every installed bundle",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
if contents.version > STORE_VERSION {
|
||||
bail!(
|
||||
"{} has store version {}, but this coyote build supports up to \
|
||||
{STORE_VERSION}; update coyote or restore a matching store",
|
||||
path.display(),
|
||||
contents.version
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
path,
|
||||
bundles: contents.bundles,
|
||||
@@ -149,6 +163,7 @@ impl BundleStore {
|
||||
|
||||
pub(crate) fn save(&self) -> Result<()> {
|
||||
let content = serde_yaml::to_string(&StoreContentsRef {
|
||||
version: STORE_VERSION,
|
||||
bundles: &self.bundles,
|
||||
})
|
||||
.context("failed to serialize the installed-bundles store")?;
|
||||
@@ -277,6 +292,13 @@ impl BundleStore {
|
||||
if let Some(old_key) = existing_key
|
||||
&& !already_recorded
|
||||
{
|
||||
if self.bundles.contains_key(&resolved.name) {
|
||||
bail!(
|
||||
"records '{old_key}' and '{}' both track source '{url}'; \
|
||||
uninstall one or repair installed-bundles.yaml before continuing",
|
||||
resolved.name
|
||||
);
|
||||
}
|
||||
let record = self
|
||||
.bundles
|
||||
.remove(&old_key)
|
||||
@@ -348,6 +370,25 @@ impl BundleStore {
|
||||
self.save()
|
||||
}
|
||||
|
||||
/// Written only after an update's files and mcp entries land, so an
|
||||
/// aborted update cannot leave the record claiming a commit whose content
|
||||
/// never finished applying.
|
||||
pub(crate) fn set_bundle_versions(
|
||||
&mut self,
|
||||
name: &str,
|
||||
commit: &str,
|
||||
version: Option<String>,
|
||||
) -> Result<()> {
|
||||
self.ensure_bundle_exists(name)?;
|
||||
let record = self
|
||||
.bundles
|
||||
.get_mut(name)
|
||||
.expect("bundle existence checked above");
|
||||
record.commit = commit.to_string();
|
||||
record.version = version;
|
||||
self.save()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_file_record(&mut self, bundle: &str, path: &str) -> Result<()> {
|
||||
self.ensure_bundle_exists(bundle)?;
|
||||
let record = self
|
||||
|
||||
Reference in New Issue
Block a user