1use super::*;
3use buntrail_protocol::{
4 frame::{Frame, TransferVerifier},
5 types::ProofAction,
6};
7use buntrail_services::git::proof_codec::{self, output::OutputReader};
8pub use buntrail_services::git::{
9 proof_codec::output::{EntryGroup, ObservationEvent, OwnedObservation},
10 worktree_proof::WorktreePreparation,
11};
12use std::{
13 io::{self, Read, Write},
14 time::{Duration, Instant},
15};
16
17fn unavailable(message: &str) -> CoreError {
18 CoreError::new(ErrorCode::Unavailable, message)
19}
20fn response(message: Message) -> CoreError {
21 match message {
22 Message::Error { code, safe_message } => w::core_error(&code, &safe_message),
23 _ => w::invalid(),
24 }
25}
26struct Count(u64);
27impl Write for Count {
28 fn write(&mut self, b: &[u8]) -> io::Result<usize> {
29 self.0 = self
30 .0
31 .checked_add(b.len() as u64)
32 .ok_or_else(|| io::Error::other("proof length overflow"))?;
33 Ok(b.len())
34 }
35 fn flush(&mut self) -> io::Result<()> {
36 Ok(())
37 }
38}
39#[derive(Clone, Copy)]
40enum Part {
41 Lookup,
42 Original,
43 Approvals,
44 Aliases,
45}
46impl Part {
47 fn chunk(self, id: String, offset: u64) -> Message {
48 match self {
49 Self::Lookup => Message::ProofLookupChunk {
50 transfer_id: id,
51 offset,
52 },
53 Self::Original => Message::ProofOriginalChunk {
54 transfer_id: id,
55 offset,
56 },
57 Self::Approvals => Message::ProofApprovalsChunk {
58 transfer_id: id,
59 offset,
60 },
61 Self::Aliases => Message::ProofAliasesChunk {
62 transfer_id: id,
63 offset,
64 },
65 }
66 }
67 fn end(self, id: String, sha256: String) -> Message {
68 match self {
69 Self::Lookup => Message::ProofLookupEnd {
70 transfer_id: id,
71 sha256,
72 },
73 Self::Original => Message::ProofOriginalEnd {
74 transfer_id: id,
75 sha256,
76 },
77 Self::Approvals => Message::ProofApprovalsEnd {
78 transfer_id: id,
79 sha256,
80 },
81 Self::Aliases => Message::ProofAliasesEnd {
82 transfer_id: id,
83 sha256,
84 },
85 }
86 }
87}
88fn next(
89 remote: &RemoteServices,
90 request: &mut transport::Request,
91 deadline: Instant,
92 cancellable: bool,
93) -> Result<Frame, CoreError> {
94 let mut cx = Context::from_waker(std::task::Waker::noop());
95 loop {
96 if let Poll::Ready(frame) = request.poll(&mut cx) {
97 let frame = frame?;
98 if !matches!(frame.header.message, Message::ProofOutputBegin { .. })
99 && frame.header.stream_id != request.header.stream_id
100 {
101 return Err(w::invalid());
102 }
103 return Ok(frame);
104 }
105 if cancellable {
106 remote.check_cancellation()?;
107 }
108 if Instant::now() >= deadline {
109 return Err(CoreError::new(
110 ErrorCode::Unknown,
111 "proof transfer absolute deadline exceeded; inspect original operation without replay",
112 ));
113 }
114 std::thread::park_timeout(Duration::from_millis(2));
115 }
116}
117struct Upload<'a> {
118 remote: &'a RemoteServices,
119 request: &'a mut transport::Request,
120 part: Part,
121 deadline: Instant,
122 bytes: Vec<u8>,
123 offset: u64,
124 sequence: u64,
125 hash: sha2::Sha256,
126}
127impl<'a> Upload<'a> {
128 fn new(
129 remote: &'a RemoteServices,
130 request: &'a mut transport::Request,
131 part: Part,
132 deadline: Instant,
133 ) -> Self {
134 Self {
135 remote,
136 request,
137 part,
138 deadline,
139 bytes: Vec::with_capacity(65536),
140 offset: 0,
141 sequence: 0,
142 hash: sha2::Sha256::new(),
143 }
144 }
145 fn send(&self, message: Message, payload: Vec<u8>) -> Result<(), CoreError> {
146 if matches!(self.part, Part::Lookup) {
147 self.request.send_control(message, payload)
148 } else {
149 self.request.send(message, payload)
150 }
151 }
152 fn drain(&mut self) -> Result<(), CoreError> {
153 if self.bytes.is_empty() {
154 return Ok(());
155 }
156 if !matches!(self.part, Part::Lookup) {
157 self.remote.check_cancellation()?;
158 }
159 let id = self
160 .request
161 .header
162 .stream_id
163 .clone()
164 .ok_or_else(w::invalid)?;
165 self.request.header.sequence = Some(self.sequence);
166 let payload = std::mem::replace(&mut self.bytes, Vec::with_capacity(65536));
167 let length = payload.len();
168 self.hash.update(&payload);
169 self.send(self.part.chunk(id.clone(), self.offset), payload)?;
170 let frame = next(
171 self.remote,
172 self.request,
173 self.deadline,
174 !matches!(self.part, Part::Lookup),
175 )?;
176 match frame.header.message {
177 Message::Credit { bytes }
178 if bytes as usize == length && frame.header.stream_id.as_ref() == Some(&id) => {}
179 m => return Err(response(m)),
180 }
181 self.offset += length as u64;
182 self.sequence = self.sequence.checked_add(1).ok_or_else(w::invalid)?;
183 Ok(())
184 }
185 fn finish(mut self, length: u64) -> Result<(), CoreError> {
186 self.drain()?;
187 if self.offset != length {
188 return Err(w::invalid());
189 }
190 self.request.header.sequence = Some(self.sequence);
191 self.send(
192 self.part.end(
193 self.request
194 .header
195 .stream_id
196 .clone()
197 .ok_or_else(w::invalid)?,
198 w::hex(&self.hash.clone().finalize()),
199 ),
200 Vec::new(),
201 )?;
202 self.request.header.sequence = None;
203 Ok(())
204 }
205}
206impl Write for Upload<'_> {
207 fn write(&mut self, mut b: &[u8]) -> io::Result<usize> {
208 let total = b.len();
209 while !b.is_empty() {
210 let n = b.len().min(65536 - self.bytes.len());
211 self.bytes.extend_from_slice(&b[..n]);
212 b = &b[n..];
213 if self.bytes.len() == 65536 {
214 self.drain().map_err(io::Error::other)?;
215 }
216 }
217 Ok(total)
218 }
219 fn flush(&mut self) -> io::Result<()> {
220 self.drain().map_err(io::Error::other)
221 }
222}
223fn approvals<'a>(
224 out: &mut impl Write,
225 remote: &RemoteServices,
226 values: impl Iterator<Item = &'a GitSaveApproval>,
227) -> Result<(), CoreError> {
228 for a in values {
229 if a.actor.client != remote.lease.client || a.actor.runtime != remote.lease.runtime {
230 return Err(CoreError::new(
231 ErrorCode::TargetChanged,
232 "save approval client or runtime changed",
233 ));
234 }
235 for b in [
236 a.operation.0.as_slice(),
237 a.request_digest.as_slice(),
238 a.actor.client.0.as_slice(),
239 a.actor.workspace.0.as_slice(),
240 a.actor.lease.0.as_slice(),
241 ] {
242 out.write_all(b).map_err(io_error)?;
243 }
244 }
245 Ok(())
246}
247struct Download<'a> {
248 remote: &'a RemoteServices,
249 request: &'a mut transport::Request,
250 id: String,
251 verifier: TransferVerifier,
252 sequence: u64,
253 buffer: std::io::Cursor<Vec<u8>>,
254 seal: Option<[u8; 32]>,
255 deadline: Instant,
256}
257impl Read for Download<'_> {
258 fn read(&mut self, b: &mut [u8]) -> io::Result<usize> {
259 if b.is_empty() {
260 return Ok(0);
261 }
262 loop {
263 let n = self.buffer.read(b)?;
264 if n != 0 {
265 return Ok(n);
266 }
267 if !self.buffer.get_ref().is_empty() {
268 self.request
269 .credit(&self.id, self.buffer.get_ref().len())
270 .map_err(io::Error::other)?;
271 self.buffer = std::io::Cursor::new(Vec::new());
272 }
273 if self.seal.is_some() {
274 return Ok(0);
275 }
276 let frame =
277 next(self.remote, self.request, self.deadline, true).map_err(io::Error::other)?;
278 if frame.header.stream_id.as_ref() != Some(&self.id)
279 || frame.header.sequence != Some(self.sequence)
280 {
281 return Err(io::Error::other("output owner or sequence mismatch"));
282 }
283 self.sequence = self
284 .sequence
285 .checked_add(1)
286 .ok_or_else(|| io::Error::other("output sequence overflow"))?;
287 match frame.header.message {
288 Message::ProofOutputChunk {
289 transfer_id,
290 offset,
291 } => {
292 self.verifier
293 .chunk(&transfer_id, offset, &frame.payload)
294 .map_err(io::Error::other)?;
295 self.buffer = std::io::Cursor::new(frame.payload);
296 }
297 Message::ProofOutputEnd {
298 transfer_id,
299 sha256,
300 } => {
301 let digest = w::digest(&sha256).map_err(io::Error::other)?;
302 self.verifier
303 .end(&transfer_id, digest)
304 .map_err(io::Error::other)?;
305 self.seal = Some(digest);
306 }
307 m => return Err(io::Error::other(response(m))),
308 }
309 }
310 }
311}
312impl RemoteServices {
313 fn proof_capability(&self) -> Result<(), CoreError> {
314 if !self
315 .negotiated_capabilities()
316 .iter()
317 .any(|c| c == "git.worktree_proof")
318 {
319 return Err(unavailable(
320 "active helper lacks git.worktree_proof; preserve its existing sessions",
321 ));
322 }
323 Ok(())
324 }
325 pub fn observe_owned<'a>(
328 &self,
329 worktree: WorktreeIdentity,
330 aliases: impl Iterator<Item = &'a FileRef> + Clone,
331 mut on_event: impl FnMut(ObservationEvent) -> Result<(), CoreError>,
332 ) -> Result<OwnedObservation, CoreError> {
333 self.proof_capability()?;
334 self.check(&worktree.target)?;
335 if !self.transport.owns_registration(&self.lease) {
336 return Err(unavailable("exact observation registration changed"));
337 }
338 self.check_cancellation()?;
339 let mut count = Count(0);
340 proof_codec::write_aliases(&mut count, &self.target, aliases.clone())?;
341 let id = w::uuid(buntrail_services::connections::local::new_id()?);
342 let deadline = Instant::now() + Duration::from_secs(120);
343 let mut request = self.transport.start(
344 Message::ProofObserveBegin {
345 transfer_id: id.clone(),
346 total_length: count.0,
347 lease_id: w::uuid(self.lease.lease.0),
348 worktree: w::Worktree::from_core(&worktree)?,
349 },
350 None,
351 Some(id),
352 )?;
353 match next(self, &mut request, deadline, true)?.header.message {
354 Message::Ack {} => {}
355 m => return Err(response(m)),
356 }
357 let mut upload = Upload::new(self, &mut request, Part::Aliases, deadline);
358 proof_codec::write_aliases(&mut upload, &self.target, aliases)?;
359 upload.finish(count.0)?;
360 let begin = next(self, &mut request, deadline, true)?;
361 let Message::ProofOutputBegin {
362 transfer_id,
363 total_length,
364 } = begin.header.message
365 else {
366 return Err(response(begin.header.message));
367 };
368 if begin.header.stream_id.as_ref() != Some(&transfer_id) {
369 return Err(w::invalid());
370 }
371 request.header.stream_id = Some(transfer_id.clone());
372 let mut download = Download {
373 remote: self,
374 request: &mut request,
375 id: transfer_id.clone(),
376 verifier: TransferVerifier::new(transfer_id, total_length).map_err(|_| w::invalid())?,
377 sequence: 0,
378 buffer: std::io::Cursor::new(Vec::new()),
379 seal: None,
380 deadline,
381 };
382 let mut output = OutputReader::new(&mut download, total_length, &self.target)?;
383 let observation = output.observation_id();
384 let result = (|| {
385 while let Some(event) = output.next_event()? {
386 on_event(event)?;
387 }
388 let digest = output.wire_digest();
389 let summary = output.finish()?;
390 Ok((summary, digest))
391 })();
392 let result = result.and_then(|(summary, digest)| {
393 if digest.is_none() || digest != download.seal {
394 Err(w::invalid())
395 } else {
396 Ok(summary)
397 }
398 });
399 drop(download);
400 if result.is_ok() {
401 request.finish();
402 }
403 drop(request);
404 if result.is_err() {
405 let _ = self.release_observation(observation);
406 }
407 result
408 }
409 fn lookup_prefix(
410 &self,
411 original: &GitMutationRequest,
412 action: ProofAction,
413 ) -> Result<(transport::Request, Message, u64), CoreError> {
414 self.check(&original.worktree.target)?;
415 let mut count = Count(0);
416 proof_codec::write_original(&mut count, original, &self.target)?;
417 let id = w::uuid(buntrail_services::connections::local::new_id()?);
418 let deadline = Instant::now() + Duration::from_secs(120);
419 let mut request = self.transport.start(
420 Message::ProofLookupBegin {
421 transfer_id: id.clone(),
422 total_length: count.0,
423 action,
424 lease_id: w::uuid(self.lease.lease.0),
425 },
426 Some(original.operation),
427 Some(id),
428 )?;
429 match next(self, &mut request, deadline, false)?.header.message {
430 Message::ProofLookupReady { credit: 65536 } => {}
431 m => return Err(response(m)),
432 }
433 let mut upload = Upload::new(self, &mut request, Part::Lookup, deadline);
434 proof_codec::write_original(&mut upload, original, &self.target)?;
435 upload.finish(count.0)?;
436 let ready = next(self, &mut request, deadline, false)?;
437 Ok((request, ready.header.message, count.0))
438 }
439
440 pub fn existing_original(
442 &self,
443 original: &GitMutationRequest,
444 ) -> Result<Option<OperationReceipt>, CoreError> {
445 if !self
446 .negotiated_capabilities()
447 .iter()
448 .any(|c| c == "git.original_lookup")
449 {
450 return Err(unavailable(
451 "Active helper cannot stream original lookup; preserve the existing supervisor",
452 ));
453 }
454 let (mut request, message, _) = self.lookup_prefix(original, ProofAction::Lookup)?;
455 request.finish();
456 match message {
457 Message::ProofMissing {} => Ok(None),
458 Message::ProofExisting { receipt } => {
459 let receipt = receipt.core(&self.target)?;
460 if receipt.id != original.operation {
461 return Err(w::invalid());
462 }
463 Ok(Some(receipt))
464 }
465 other => Err(response(other)),
466 }
467 }
468 fn proof_request<'a>(
469 &self,
470 original: &GitMutationRequest,
471 action: ProofAction,
472 proof_id: [u8; 16],
473 alias_digest: [u8; 32],
474 values: impl Iterator<Item = &'a GitSaveApproval> + Clone,
475 ) -> Result<Message, CoreError> {
476 self.proof_capability()?;
477 let (mut request, ready, original_length) = self.lookup_prefix(original, action)?;
478 let (original_transfer_id, approvals_transfer_id) = match ready {
479 message @ Message::ProofExisting { .. } => {
480 request.finish();
481 return Ok(message);
482 }
483 Message::ProofReady {
484 original_transfer_id,
485 approvals_transfer_id,
486 } => (original_transfer_id, approvals_transfer_id),
487 m => return Err(response(m)),
488 };
489 self.check_cancellation()?;
490 if !self.transport.owns_registration(&self.lease) {
491 return Err(unavailable("exact proof registration changed"));
492 }
493 let deadline = Instant::now() + Duration::from_secs(120);
494 request.header.stream_id = Some(original_transfer_id);
495 let mut upload = Upload::new(self, &mut request, Part::Original, deadline);
496 proof_codec::write_original(&mut upload, original, &self.target)?;
497 upload.finish(original_length)?;
498 match next(self, &mut request, deadline, true)?.header.message {
499 Message::Ack {} => {}
500 m => return Err(response(m)),
501 }
502 let mut count = Count(0);
503 approvals(&mut count, self, values.clone())?;
504 request.header.stream_id = Some(approvals_transfer_id.clone());
505 request.send(
506 Message::ProofApprovalsBegin {
507 transfer_id: approvals_transfer_id,
508 total_length: count.0,
509 proof_id: w::uuid(proof_id),
510 alias_digest: w::hex(&alias_digest),
511 },
512 Vec::new(),
513 )?;
514 match next(self, &mut request, deadline, true)?.header.message {
515 Message::Ack {} => {}
516 m => return Err(response(m)),
517 }
518 let mut upload = Upload::new(self, &mut request, Part::Approvals, deadline);
519 approvals(&mut upload, self, values)?;
520 upload.finish(count.0)?;
521 let result = next(self, &mut request, deadline, false)?.header.message;
522 request.finish();
523 Ok(result)
524 }
525 pub fn prepare_worktree<'a>(
527 &self,
528 original: &GitMutationRequest,
529 observation: [u8; 16],
530 alias_digest: [u8; 32],
531 approvals: impl Iterator<Item = &'a GitSaveApproval> + Clone,
532 ) -> Result<WorktreePreparation, CoreError> {
533 match self.proof_request(
534 original,
535 ProofAction::Prepare,
536 observation,
537 alias_digest,
538 approvals,
539 )? {
540 Message::ProofPrepared { plan_id } => {
541 Ok(WorktreePreparation::Prepared(w::id(&plan_id)?))
542 }
543 Message::ProofExisting { receipt } => {
544 let r = receipt.core(&self.target)?;
545 if r.id != original.operation {
546 return Err(w::invalid());
547 }
548 Ok(WorktreePreparation::Existing(Box::new(r)))
549 }
550 m => Err(response(m)),
551 }
552 }
553 pub fn execute_worktree<'a>(
554 &self,
555 original: GitMutationRequest,
556 plan: [u8; 16],
557 alias_digest: [u8; 32],
558 approvals: impl Iterator<Item = &'a GitSaveApproval> + Clone,
559 ) -> Result<OperationReceipt, CoreError> {
560 match self.proof_request(
561 &original,
562 ProofAction::Execute,
563 plan,
564 alias_digest,
565 approvals,
566 )? {
567 Message::ProofExisting { receipt } => {
568 let r = receipt.core(&self.target)?;
569 if r.id != original.operation {
570 return Err(w::invalid());
571 }
572 Ok(r)
573 }
574 m => Err(response(m)),
575 }
576 }
577 fn proof_cleanup(&self, message: Message) -> Result<(), CoreError> {
578 self.proof_capability()?;
579 let mut request = self.transport.start(message, None, None)?;
580 let result = request.next()?.header.message;
581 request.finish();
582 match result {
583 Message::Ack {} => Ok(()),
584 m => Err(response(m)),
585 }
586 }
587 pub fn cancel_worktree_plan(&self, plan: [u8; 16]) -> Result<(), CoreError> {
588 self.proof_cleanup(Message::ProofCancel {
589 lease_id: w::uuid(self.lease.lease.0),
590 plan_id: w::uuid(plan),
591 })
592 }
593 pub fn release_observation(&self, observation: [u8; 16]) -> Result<(), CoreError> {
594 self.proof_cleanup(Message::ProofRelease {
595 lease_id: w::uuid(self.lease.lease.0),
596 observation_id: w::uuid(observation),
597 })
598 }
599}