Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/cyber-core/src/binary/draft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ impl<B: ImmutableBase> Draft<B> {
Ok(format!("{:x}", hash.finalize()))
}

fn ranges(&self) -> Vec<(u64, u64, u64, u64)> {
pub(super) fn ranges(&self) -> Vec<(u64, u64, u64, u64)> {
let (mut original, mut after, mut start) = (0, 0, 0);
let mut ranges = Vec::new();
for piece in &self.pieces {
Expand Down Expand Up @@ -452,7 +452,12 @@ impl<B: ImmutableBase> Draft<B> {
proof.update(&proposed_sha256);
let (mut original_bytes, mut proposed_bytes, mut count) = (0, 0, 0);
let mut ranges = Vec::new();
for (before, removed, after, inserted) in self.ranges() {
let candidates = if proposed_sha256 == self.base.sha256() {
vec![]
} else {
self.ranges()
};
for (before, removed, after, inserted) in candidates {
let before_sha256 = self.range_hash(true, before, removed, &cancelled)?;
let after_sha256 = self.range_hash(false, after, inserted, &cancelled)?;
if removed == inserted && before_sha256 == after_sha256 {
Expand Down
88 changes: 88 additions & 0 deletions crates/cyber-core/src/binary/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use std::path::PathBuf;
mod catalog;
#[path = "editor/restore.rs"]
mod restore;
#[path = "editor/versions.rs"]
mod versions;
pub use catalog::{StoredDocument, StoredDocuments};
pub use versions::VersionView;

const MAX_OPERATIONS: usize = 4096;
const EFFECTS: &str = "Permissions, executable bits, owner/group, supported extended attributes and ACL are preserved. Native replacement changes file identity and updates modification time. Save checkpoints draft undo/redo; Restore Last Save uses the retained preimage. External checksums, signatures and irreversible observer effects are not repaired. Uncooperative writers can leave an unknown outcome; evidence is retained.";
Expand Down Expand Up @@ -66,6 +69,8 @@ struct LastSave {
preimage: BaseRef,
installed_sha256: String,
operation: String,
#[serde(default)]
evidence: Option<versions::Evidence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
Expand All @@ -76,6 +81,8 @@ struct Intent {
new_base: BaseRef,
after_draft: DraftState,
stage: String,
#[serde(default)]
evidence: Option<versions::Evidence>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -428,6 +435,7 @@ impl Editor {
return Ok(SavePreparation::Existing(prior));
}
self.available()?;
self.publish_last_version()?;
let current = self.review(0, 128, &cancelled)?;
if current.fingerprint != review {
return Err(Error::Changed);
Expand Down Expand Up @@ -489,13 +497,22 @@ impl Editor {
reason: None,
notification_pending: false,
};
let evidence = versions::Evidence::prepared(
self,
&operation,
&current.draft,
after.clone(),
&after_draft,
self.draft.ranges(),
)?;
let intent = Intent {
operation,
prior: target,
after,
new_base: BaseRef::of(after_draft.base()),
after_draft: after_draft.snapshot(),
stage: stage_name,
evidence: Some(evidence),
};
Ok(SavePreparation::Ready(Box::new(PreparedSave {
intent,
Expand Down Expand Up @@ -538,6 +555,19 @@ impl Editor {
&mut self,
prepared: PreparedSave,
cancelled: impl Fn() -> bool,
) -> Result<Operation, Error> {
self.commit_save_checked(prepared, cancelled, || Ok(()))
}
/// The host rechecks trusted current read/edit/Mode and ancestor authority
/// after content verification and capacity reservation, before native intent.
/// The check must not prompt, invoke hooks/models, or release mutation owners.
/// A refusal has no native effect and leaves the operation eligible for an
/// explicit newly authorized attempt. It does not manufacture a save receipt.
pub fn commit_save_checked(
&mut self,
prepared: PreparedSave,
cancelled: impl Fn() -> bool,
authorize: impl FnOnce() -> Result<(), Error>,
) -> Result<Operation, Error> {
if let Some(prior) = self.prior(
&prepared.intent.operation.id,
Expand Down Expand Up @@ -609,6 +639,10 @@ impl Editor {
.0
.saturating_sub(prepared.intent.after.length.0),
)?;
authorize()?;
if cancelled() {
return Err(Error::Cancelled);
}
self.record.pending = Some(prepared.intent);
persist(&mut self.store, &self.record)?;
let intent = self.record.pending.as_ref().ok_or(Error::Invalid)?;
Expand Down Expand Up @@ -686,6 +720,11 @@ impl Editor {
operation.file_effect = FileEffect::Changed;
operation.reason = None;
operation.notification_pending = true;
let mut evidence = intent.evidence.clone();
if let Some(evidence) = &mut evidence {
evidence.target = target.clone();
evidence.operation = operation.clone();
}
let record = Record {
version: 1,
owner: self.record.owner.clone(),
Expand All @@ -704,6 +743,7 @@ impl Editor {
preimage: self.record.base.clone(),
installed_sha256: intent.new_base.sha256.clone(),
operation: operation.id.clone(),
evidence,
}),
};
(record, operation)
Expand Down Expand Up @@ -782,6 +822,54 @@ mod tests {
}
}
#[test]
fn final_save_authority_refusal_preserves_source_draft_and_receipt_absence() {
let (_root, mut editor, path, _owner) = setup();
patch(&mut editor, "patch_one");
let revision = editor.draft().revision();
let prepared = ready(&mut editor, "save_checked");
let called = std::cell::Cell::new(false);
let result = editor.commit_save_checked(
*prepared,
|| false,
|| {
called.set(true);
Err(Error::Changed)
},
);
assert!(called.get());
assert!(matches!(result, Err(Error::Changed)));
assert_eq!(std::fs::read(&path).unwrap(), b"original");
assert_eq!(editor.draft().revision(), revision);
assert_eq!(editor.draft().read(0, 4).unwrap(), [0, 255, 128, 27]);
assert!(!editor.unresolved());
assert!(editor.lookup_operation("save_checked").unwrap().is_none());
// A stricter check can also observe Stop while checking authority.
let cancelled = std::cell::Cell::new(false);
let prepared = ready(&mut editor, "save_checked");
assert!(matches!(
editor.commit_save_checked(
*prepared,
|| cancelled.get(),
|| {
cancelled.set(true);
Ok(())
}
),
Err(Error::Cancelled)
));
assert_eq!(std::fs::read(&path).unwrap(), b"original");
assert!(!editor.unresolved());
let prepared = ready(&mut editor, "save_checked");
assert_eq!(
editor
.commit_save_checked(*prepared, || false, || Ok(()))
.unwrap()
.outcome,
Outcome::Acknowledged
);
assert_eq!(std::fs::read(path).unwrap(), [0, 255, 128, 27]);
}
#[test]
fn prepared_editors_keep_native_source_and_directory_pins_after_the_live_view_is_dropped() {
use std::os::unix::fs::MetadataExt;
let (_root, editor, _path, owner) = setup();
Expand Down
20 changes: 20 additions & 0 deletions crates/cyber-core/src/binary/editor/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl Editor {
return Ok(SavePreparation::Existing(prior));
}
self.available()?;
self.publish_last_version()?;
let current = self.restore_review(&cancelled)?;
if current.fingerprint != review {
return Err(Error::Changed);
Expand Down Expand Up @@ -130,13 +131,32 @@ impl Editor {
reason: None,
notification_pending: false,
};
let ranges = if current.draft.range_count == 0 {
vec![]
} else {
vec![(
0,
current.draft.base_length.0,
0,
current.draft.proposed_length.0,
)]
};
let evidence = versions::Evidence::prepared(
self,
&operation,
&current.draft,
after.clone(),
&after_draft,
ranges,
)?;
let intent = Intent {
operation,
prior: target,
after,
new_base: BaseRef::of(after_draft.base()),
after_draft: after_draft.snapshot(),
stage: stage_name,
evidence: Some(evidence),
};
Ok(SavePreparation::Ready(Box::new(PreparedSave {
intent,
Expand Down
Loading
Loading