1use crate::kernel::{
4 FileKind as EngineFileKind, FileMetadata, FilePayload, LogicalName, MetadataField,
5 NameEncoding, TreeEntry,
6};
7use crate::model::{FilesystemProfile as EngineProfile, Lifecycle, VolumeConfig};
8use crate::wire::{filesystem::v2 as wire, harness::v1 as harness};
9use crate::{
10 ApplyOptions, AsyncAuthorityStore, AsyncObjectStore, ByteRange, CancellationToken, Digest,
11 DurableCommit, ForkOptions, Fs, Generation, GenerationId, IdempotencyKey, JoinHistory,
12 JoinOutcome, MergeConflict, ObjectId, ObjectKind, Sequence, Transaction, TransactionCommit,
13 TransactionConflict, TransactionConflictRegion, TransactionDependencyUse, TransactionRebase,
14 TransactionSparseSeek, Workspace, WorkspaceDelete, WorkspaceError, WorkspaceExtentKind,
15 WorkspaceMetadata, WorkspaceRebase,
16};
17use bytes::Bytes;
18use futures::{StreamExt, stream};
19use prost::Message;
20use std::pin::Pin;
21use std::sync::Arc;
22use tonic::{Request, Response, Status};
23
24type WireStream<T> = Pin<Box<dyn futures::Stream<Item = Result<T, Status>> + Send + 'static>>;
25
26enum JoinTargetHead<A, O> {
27 Expected {
28 head: crate::Head,
29 existing_operation: bool,
30 },
31 Stale(Generation<A, O>),
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct FilesystemWireLimits {
37 pub maximum_request_bytes: u64,
39 pub maximum_response_bytes: u64,
41 pub maximum_transaction_mutations: u32,
43 pub maximum_page_items: u32,
45 pub maximum_credential_seconds: u64,
47}
48
49impl Default for FilesystemWireLimits {
50 fn default() -> Self {
51 Self {
52 maximum_request_bytes: 16 * 1024 * 1024,
53 maximum_response_bytes: 16 * 1024 * 1024,
54 maximum_transaction_mutations: 2_048,
55 maximum_page_items: 1_024,
56 maximum_credential_seconds: 3_600,
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum CredentialKind {
64 Mount,
66 S3,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct CredentialGrantRequest {
73 pub kind: CredentialKind,
75 pub workspace_id: crate::WorkspaceId,
77 pub workspace_name: crate::WorkspaceName,
79 pub generation_id: GenerationId,
81 pub writable: bool,
83 pub expires_after_seconds: u64,
85 pub idempotency_key: IdempotencyKey,
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91pub enum CredentialGrant {
92 Bearer {
94 endpoint: String,
96 token: String,
98 expires_at_unix_seconds: i64,
100 },
101 S3 {
103 endpoint: String,
105 bucket: String,
107 region: String,
109 access_key_id: String,
111 secret_access_key: String,
113 session_token: String,
115 expires_at_unix_seconds: i64,
117 },
118}
119
120#[tonic::async_trait]
122pub trait FilesystemCredentialIssuer: Send + Sync + 'static {
123 fn mount_enabled(&self) -> bool;
125
126 fn s3_enabled(&self) -> bool;
128
129 async fn issue(&self, request: CredentialGrantRequest) -> Result<CredentialGrant, Status>;
131}
132
133#[derive(Clone, Debug, Eq, PartialEq)]
135pub struct HostedSourceScope {
136 pub workspace_id: crate::WorkspaceId,
138 pub workspace_name: crate::WorkspaceName,
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct HostedSourceOperation {
145 pub scope: HostedSourceScope,
147 pub idempotency_key: IdempotencyKey,
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub enum HostedSourceInvalidation {
154 InitialSnapshotRequired,
156 QueueOverflow,
158 NativeRescanRequired,
160 BackendError,
162 UnrepresentablePath,
164 AmbiguousRename,
166 RootChanged,
168}
169
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub enum HostedSourceState {
173 Clean,
175 PendingCapture,
177 NeedsRescan(HostedSourceInvalidation),
179 Conflict,
181 Sealed,
183}
184
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub struct HostedSourceResult {
188 pub state: HostedSourceState,
190 pub generation_id: Option<GenerationId>,
192}
193
194#[tonic::async_trait]
196pub trait FilesystemSourceProvider: Send + Sync + 'static {
197 async fn state(&self, scope: HostedSourceScope) -> Result<HostedSourceResult, Status>;
199 async fn reconcile(
201 &self,
202 operation: HostedSourceOperation,
203 ) -> Result<HostedSourceResult, Status>;
204 async fn rescan(&self, operation: HostedSourceOperation) -> Result<HostedSourceResult, Status>;
206 async fn seal(&self, operation: HostedSourceOperation) -> Result<HostedSourceResult, Status>;
208}
209
210#[derive(Clone)]
215pub struct FilesystemWireService<A, O> {
216 filesystem: Fs<A, O>,
217 limits: FilesystemWireLimits,
218 credential_issuer: Option<Arc<dyn FilesystemCredentialIssuer>>,
219 source_provider: Option<Arc<dyn FilesystemSourceProvider>>,
220}
221
222impl<A, O> FilesystemWireService<A, O> {
223 pub fn new(filesystem: Fs<A, O>, limits: FilesystemWireLimits) -> Result<Self, Status> {
229 if limits.maximum_request_bytes == 0
230 || limits.maximum_response_bytes == 0
231 || limits.maximum_transaction_mutations == 0
232 || limits.maximum_page_items == 0
233 || limits.maximum_credential_seconds == 0
234 {
235 return Err(Status::invalid_argument(
236 "filesystem wire bounds must be nonzero",
237 ));
238 }
239 Ok(Self {
240 filesystem,
241 limits,
242 credential_issuer: None,
243 source_provider: None,
244 })
245 }
246
247 #[must_use]
249 pub const fn filesystem(&self) -> &Fs<A, O> {
250 &self.filesystem
251 }
252
253 #[must_use]
255 pub fn with_credential_issuer(mut self, issuer: Arc<dyn FilesystemCredentialIssuer>) -> Self {
256 self.credential_issuer = Some(issuer);
257 self
258 }
259
260 #[must_use]
262 pub fn with_source_provider(mut self, provider: Arc<dyn FilesystemSourceProvider>) -> Self {
263 self.source_provider = Some(provider);
264 self
265 }
266}
267
268impl<A: AsyncAuthorityStore, O: AsyncObjectStore> FilesystemWireService<A, O> {
269 async fn workspace(
270 &self,
271 reference: Option<wire::WorkspaceRef>,
272 ) -> Result<Workspace<A, O>, Status> {
273 let reference = required(reference, "workspace")?;
274 let workspace = self
275 .filesystem
276 .open_workspace(&reference.name)
277 .await
278 .map_err(|error| status(&error))?;
279 if workspace.id().into_bytes().as_slice() != reference.workspace_id {
280 return Err(Status::failed_precondition(
281 "workspace name and identity disagree",
282 ));
283 }
284 Ok(workspace)
285 }
286
287 async fn source_scope(
288 &self,
289 reference: Option<wire::WorkspaceRef>,
290 ) -> Result<(Workspace<A, O>, HostedSourceScope), Status> {
291 let workspace = self.workspace(reference).await?;
292 let scope = HostedSourceScope {
293 workspace_id: workspace.id(),
294 workspace_name: workspace.name().clone(),
295 };
296 Ok((workspace, scope))
297 }
298
299 fn source_provider(&self) -> Result<&Arc<dyn FilesystemSourceProvider>, Status> {
300 self.source_provider
301 .as_ref()
302 .ok_or_else(|| Status::unimplemented("hosted source reconciliation is unavailable"))
303 }
304
305 async fn source_response(
306 &self,
307 workspace: &Workspace<A, O>,
308 result: HostedSourceResult,
309 ) -> Result<wire::SourceResponse, Status> {
310 let generation = match result.generation_id {
311 Some(id) => Some(generation_ref(
312 &workspace
313 .generation(id)
314 .await
315 .map_err(|error| status(&error))?,
316 )),
317 None => None,
318 };
319 let (state, reason) = source_state_message(result.state);
320 let requires_generation = matches!(
321 result.state,
322 HostedSourceState::Clean | HostedSourceState::Sealed
323 );
324 if requires_generation != generation.is_some() {
325 return Err(Status::data_loss(
326 "hosted source result generation does not match its state",
327 ));
328 }
329 Ok(wire::SourceResponse {
330 state: state as i32,
331 reason: reason as i32,
332 generation,
333 })
334 }
335
336 async fn generation(
337 &self,
338 reference: Option<wire::GenerationRef>,
339 ) -> Result<Generation<A, O>, Status> {
340 let reference = required(reference, "generation")?;
341 let workspace = self.workspace(reference.workspace).await?;
342 let id = generation_id(&reference.generation_id)?;
343 workspace
344 .generation(id)
345 .await
346 .map_err(|error| status(&error))
347 }
348
349 async fn join_target_head(
350 &self,
351 target: &Workspace<A, O>,
352 expected: GenerationId,
353 operation: IdempotencyKey,
354 ) -> Result<JoinTargetHead<A, O>, Status> {
355 let committed = self
356 .filesystem
357 .observe_volume_operation(
358 target.id().volume_id(),
359 operation.operation_id(),
360 crate::WorkBudget::UNBOUNDED,
361 &CancellationToken::new(),
362 )
363 .await
364 .map_err(|error| status(&WorkspaceError::engine(error)))?;
365 if let Some(commit) = committed.value {
366 let sequence = commit
367 .sequence
368 .get()
369 .checked_sub(1)
370 .ok_or_else(|| Status::data_loss("operation has invalid authority sequence"))?;
371 return Ok(JoinTargetHead::Expected {
372 head: crate::Head {
373 epoch: commit.epoch,
374 sequence: Sequence::new(sequence),
375 digest: commit.previous_digest,
376 },
377 existing_operation: true,
378 });
379 }
380 let (current, head) = self
381 .filesystem
382 .workspace_head_state(&target.volume)
383 .await
384 .map_err(|error| status(&error))?;
385 if current == expected {
386 Ok(JoinTargetHead::Expected {
387 head,
388 existing_operation: false,
389 })
390 } else {
391 Ok(JoinTargetHead::Stale(
392 target
393 .generation(current)
394 .await
395 .map_err(|error| status(&error))?,
396 ))
397 }
398 }
399
400 fn admit<M: Message>(&self, request: &Request<M>) -> Result<(), Status> {
401 self.admit_message(request.get_ref())
402 }
403
404 fn admit_message<M: Message>(&self, message: &M) -> Result<(), Status> {
405 let bytes = u64::try_from(message.encoded_len()).unwrap_or(u64::MAX);
406 if bytes > self.limits.maximum_request_bytes {
407 return Err(Status::resource_exhausted(
408 "encoded request exceeds service bound",
409 ));
410 }
411 Ok(())
412 }
413}
414
415#[tonic::async_trait]
416impl<A, O> wire::filesystem_service_server::FilesystemService for FilesystemWireService<A, O>
417where
418 A: AsyncAuthorityStore + Send + Sync + 'static,
419 O: AsyncObjectStore + Send + Sync + 'static,
420{
421 async fn handshake(
422 &self,
423 request: Request<wire::HandshakeRequest>,
424 ) -> Result<Response<wire::HandshakeResponse>, Status> {
425 self.admit(&request)?;
426 if let Some(requested) = request
427 .into_inner()
428 .harness
429 .and_then(|value| value.protocol)
430 {
431 if !requested.version.is_empty() && requested.version != "1" {
432 return Err(Status::failed_precondition(
433 "unsupported filesystem contract version",
434 ));
435 }
436 let digest = crate::descriptor_digest();
437 if !requested.descriptor_digest.is_empty() && requested.descriptor_digest != digest {
438 return Err(Status::failed_precondition(
439 "filesystem descriptor digest mismatch",
440 ));
441 }
442 }
443 let protocol = harness::ProtocolIdentity {
444 version: "1".to_owned(),
445 descriptor_digest: crate::descriptor_digest(),
446 };
447 Ok(Response::new(wire::HandshakeResponse {
448 harness: Some(harness::HandshakeResponse {
449 protocol: Some(protocol),
450 supported: Some(harness::CapabilitySet {
451 capabilities: vec![harness::Capability {
452 name: "filesystem".to_owned(),
453 version: "1".to_owned(),
454 }],
455 }),
456 }),
457 capabilities: Some(wire::Capabilities {
458 contract_version: "1".to_owned(),
459 profiles: vec![
460 wire::FilesystemProfile::Portable as i32,
461 wire::FilesystemProfile::Posix as i32,
462 wire::FilesystemProfile::Windows as i32,
463 wire::FilesystemProfile::Browser as i32,
464 ],
465 maximum_request_bytes: self.limits.maximum_request_bytes,
466 maximum_response_bytes: self.limits.maximum_response_bytes,
467 maximum_transaction_mutations: self.limits.maximum_transaction_mutations,
468 maximum_page_items: self.limits.maximum_page_items,
469 native_mount_credentials: self
470 .credential_issuer
471 .as_ref()
472 .is_some_and(|issuer| issuer.mount_enabled()),
473 s3_credentials: self
474 .credential_issuer
475 .as_ref()
476 .is_some_and(|issuer| issuer.s3_enabled()),
477 source_reconciliation: self.source_provider.is_some(),
478 }),
479 }))
480 }
481
482 async fn create_workspace(
483 &self,
484 request: Request<wire::CreateWorkspaceRequest>,
485 ) -> Result<Response<wire::WorkspaceResponse>, Status> {
486 self.admit(&request)?;
487 let request = request.into_inner();
488 let operation_id = operation(request.operation)?.operation_id();
489 let profile = profile(request.profile)?;
490 let lifecycle = if self.filesystem.capabilities().durable {
491 Lifecycle::Durable
492 } else {
493 Lifecycle::Ephemeral
494 };
495 let mut config = VolumeConfig::portable(lifecycle);
496 config.profile = profile;
497 let workspace = self
498 .filesystem
499 .create_workspace_with_config_operation(request.name, config, Some(operation_id))
500 .await
501 .map_err(|error| status(&error))?;
502 Ok(Response::new(wire::WorkspaceResponse {
503 workspace: Some(workspace_message(&workspace).await?),
504 status: wire::MutationStatus::Committed as i32,
505 }))
506 }
507
508 async fn open_workspace(
509 &self,
510 request: Request<wire::OpenWorkspaceRequest>,
511 ) -> Result<Response<wire::WorkspaceResponse>, Status> {
512 self.admit(&request)?;
513 let workspace = match required(request.into_inner().selector, "workspace selector")? {
514 wire::open_workspace_request::Selector::Workspace(reference) => {
515 self.workspace(Some(reference)).await?
516 }
517 wire::open_workspace_request::Selector::Name(name) => self
518 .filesystem
519 .open_workspace(name)
520 .await
521 .map_err(|error| status(&error))?,
522 };
523 Ok(Response::new(wire::WorkspaceResponse {
524 workspace: Some(workspace_message(&workspace).await?),
525 status: wire::MutationStatus::Committed as i32,
526 }))
527 }
528
529 async fn delete_workspace(
530 &self,
531 request: Request<wire::DeleteWorkspaceRequest>,
532 ) -> Result<Response<wire::MutationResponse>, Status> {
533 self.admit(&request)?;
534 let request = request.into_inner();
535 let workspace = self.workspace(request.workspace).await?;
536 let outcome = workspace
537 .delete(operation(request.operation)?)
538 .await
539 .map_err(|error| status(&error))?;
540 let status = match outcome {
541 WorkspaceDelete::Deleted => wire::MutationStatus::Committed,
542 WorkspaceDelete::AlreadyDeleted => wire::MutationStatus::AlreadyCommitted,
543 WorkspaceDelete::Conflict => wire::MutationStatus::Conflict,
544 WorkspaceDelete::IdempotencyConflict => wire::MutationStatus::IdempotencyConflict,
545 };
546 Ok(Response::new(mutation(status, None, None)))
547 }
548
549 async fn get_head(
550 &self,
551 request: Request<wire::GetHeadRequest>,
552 ) -> Result<Response<wire::GenerationResponse>, Status> {
553 self.admit(&request)?;
554 let workspace = self.workspace(request.into_inner().workspace).await?;
555 let generation = workspace.head().await.map_err(|error| status(&error))?;
556 Ok(Response::new(generation_message(&generation).await?))
557 }
558
559 async fn get_generation(
560 &self,
561 request: Request<wire::GetGenerationRequest>,
562 ) -> Result<Response<wire::GenerationResponse>, Status> {
563 self.admit(&request)?;
564 let generation = self.generation(request.into_inner().generation).await?;
565 Ok(Response::new(generation_message(&generation).await?))
566 }
567
568 async fn read(
569 &self,
570 request: Request<wire::ReadRequest>,
571 ) -> Result<Response<wire::ReadResponse>, Status> {
572 self.admit(&request)?;
573 let request = request.into_inner();
574 if request.maximum_bytes == 0 || request.maximum_bytes > self.limits.maximum_response_bytes
575 {
576 return Err(Status::invalid_argument(
577 "maximum_bytes is outside service bounds",
578 ));
579 }
580 let generation = self.generation(request.generation).await?;
581 let bytes = match request.range {
582 Some(range) => {
583 if range.length > request.maximum_bytes {
584 return Err(Status::invalid_argument("range exceeds maximum_bytes"));
585 }
586 generation
587 .read_range(&request.path, range.offset, range.length)
588 .await
589 }
590 None => generation.read(&request.path, request.maximum_bytes).await,
591 }
592 .map_err(|error| status(&error))?;
593 Ok(Response::new(wire::ReadResponse {
594 contents: bytes.to_vec(),
595 }))
596 }
597
598 async fn stat(
599 &self,
600 request: Request<wire::StatRequest>,
601 ) -> Result<Response<wire::StatResponse>, Status> {
602 self.admit(&request)?;
603 let request = request.into_inner();
604 let value = self
605 .generation(request.generation)
606 .await?
607 .stat(&request.path)
608 .await
609 .map_err(|error| status(&error))?;
610 Ok(Response::new(wire::StatResponse {
611 stat: Some(stat_message(&value)),
612 }))
613 }
614
615 async fn list_directory(
616 &self,
617 request: Request<wire::ListDirectoryRequest>,
618 ) -> Result<Response<wire::ListDirectoryResponse>, Status> {
619 self.admit(&request)?;
620 let request = request.into_inner();
621 let page = required(request.page, "page")?;
622 if page.maximum_items == 0 || page.maximum_items > self.limits.maximum_page_items {
623 return Err(Status::invalid_argument("directory page bound is invalid"));
624 }
625 let generation = self.generation(request.generation).await?;
626 let after = page.after.map(input_logical_name).transpose()?;
627 let listed = generation
628 .list_directory(&request.path, after.as_ref(), page.maximum_items)
629 .await
630 .map_err(|error| status(&error))?;
631 let next = listed
632 .has_more
633 .then(|| listed.entries.last().map(|entry| logical_name(&entry.name)))
634 .flatten();
635 let mut entries = Vec::new();
636 entries
637 .try_reserve_exact(listed.entries.len())
638 .map_err(|_| Status::resource_exhausted("directory page allocation failed"))?;
639 for entry in listed.entries {
640 let name = std::str::from_utf8(entry.name.as_bytes())
641 .map_err(|_| Status::data_loss("portable directory name is not UTF-8"))?;
642 let child = if request.path == "/" {
643 format!("/{name}")
644 } else {
645 format!("{}/{name}", request.path.trim_end_matches('/'))
646 };
647 let stat = generation
648 .stat(&child)
649 .await
650 .map_err(|error| status(&error))?;
651 if stat.file_id != entry.file_id || stat.kind != entry.kind {
652 return Err(Status::data_loss(
653 "directory binding changed within immutable generation",
654 ));
655 }
656 entries.push(wire::DirectoryEntry {
657 name: Some(logical_name(&entry.name)),
658 stat: Some(stat_message(&stat)),
659 });
660 }
661 Ok(Response::new(wire::ListDirectoryResponse {
662 page: Some(wire::DirectoryPage { entries, next }),
663 }))
664 }
665
666 async fn read_link(
667 &self,
668 request: Request<wire::ReadLinkRequest>,
669 ) -> Result<Response<wire::ReadResponse>, Status> {
670 self.admit(&request)?;
671 let request = request.into_inner();
672 let contents = self
673 .generation(request.generation)
674 .await?
675 .read_symbolic_link(&request.path)
676 .await
677 .map_err(|error| status(&error))?;
678 if u64::try_from(contents.len()).unwrap_or(u64::MAX) > request.maximum_bytes
679 || request.maximum_bytes > self.limits.maximum_response_bytes
680 {
681 return Err(Status::resource_exhausted(
682 "symbolic link exceeds response bound",
683 ));
684 }
685 Ok(Response::new(wire::ReadResponse {
686 contents: contents.to_vec(),
687 }))
688 }
689
690 async fn plan_extents(
691 &self,
692 request: Request<wire::PlanExtentsRequest>,
693 ) -> Result<Response<wire::PlanExtentsResponse>, Status> {
694 self.admit(&request)?;
695 let request = request.into_inner();
696 if request.maximum_extents == 0 || request.maximum_extents > self.limits.maximum_page_items
697 {
698 return Err(Status::invalid_argument("extent bound is invalid"));
699 }
700 let range = required(request.range, "range")?;
701 let plan = self
702 .generation(request.generation)
703 .await?
704 .plan_extents(
705 &request.path,
706 range.offset,
707 range.length,
708 request.maximum_extents,
709 )
710 .await
711 .map_err(|error| status(&error))?;
712 let truncated =
713 u32::try_from(plan.spans.len()).unwrap_or(u32::MAX) == request.maximum_extents;
714 Ok(Response::new(wire::PlanExtentsResponse {
715 extents: plan
716 .spans
717 .into_iter()
718 .map(|span| wire::Extent {
719 range: Some(wire::ByteRange {
720 offset: span.offset,
721 length: span.length,
722 }),
723 kind: match span.kind {
724 WorkspaceExtentKind::Hole => wire::ExtentKind::Hole as i32,
725 WorkspaceExtentKind::AllocatedZero => {
726 wire::ExtentKind::AllocatedZero as i32
727 }
728 WorkspaceExtentKind::Content => wire::ExtentKind::Content as i32,
729 },
730 })
731 .collect(),
732 truncated,
733 }))
734 }
735
736 async fn apply_transaction(
737 &self,
738 request: Request<wire::ApplyTransactionRequest>,
739 ) -> Result<Response<wire::MutationResponse>, Status> {
740 self.admit(&request)?;
741 let request = request.into_inner();
742 if request.mutations.is_empty()
743 || u32::try_from(request.mutations.len()).unwrap_or(u32::MAX)
744 > self.limits.maximum_transaction_mutations
745 {
746 return Err(Status::invalid_argument(
747 "transaction mutation count is invalid",
748 ));
749 }
750 if request.maximum_conflicts == 0
751 || request.maximum_conflicts > self.limits.maximum_page_items
752 {
753 return Err(Status::invalid_argument(
754 "transaction conflict bound is invalid",
755 ));
756 }
757 let base = self.generation(request.base).await?;
758 let workspace = self.workspace(generation_ref(&base).workspace).await?;
759 let mut transaction = workspace
760 .begin_transaction_at(&base, operation(request.operation)?)
761 .await
762 .map_err(|error| status(&error))?;
763 for mutation in request.mutations {
764 apply_mutation(&mut transaction, mutation).await?;
765 }
766 let first = transaction.commit().await.map_err(|error| status(&error))?;
767 if !matches!(first, TransactionCommit::Conflict { .. }) {
768 return Ok(Response::new(commit_message(first)));
769 }
770 match transaction
771 .rebase(request.maximum_conflicts)
772 .await
773 .map_err(|error| status(&error))?
774 {
775 TransactionRebase::Rebased(_) => Ok(Response::new(commit_message(
776 transaction.commit().await.map_err(|error| status(&error))?,
777 ))),
778 TransactionRebase::Conflicted {
779 conflicts,
780 truncated,
781 } => Ok(Response::new(mutation_with_conflicts(
782 wire::MutationStatus::Conflict,
783 conflicts.into_iter().map(transaction_conflict).collect(),
784 truncated,
785 ))),
786 }
787 }
788
789 async fn rebase_transaction(
790 &self,
791 request: Request<wire::RebaseTransactionRequest>,
792 ) -> Result<Response<wire::RebaseTransactionResponse>, Status> {
793 self.admit(&request)?;
794 let request = request.into_inner();
795 if request.mutations.is_empty()
796 || u32::try_from(request.mutations.len()).unwrap_or(u32::MAX)
797 > self.limits.maximum_transaction_mutations
798 || request.maximum_conflicts == 0
799 || request.maximum_conflicts > self.limits.maximum_page_items
800 {
801 return Err(Status::invalid_argument(
802 "transaction rebase bounds are invalid",
803 ));
804 }
805 let base = self.generation(request.base).await?;
806 let workspace = self.workspace(generation_ref(&base).workspace).await?;
807 let mut transaction = workspace
808 .begin_transaction_at(&base, operation(request.operation)?)
809 .await
810 .map_err(|error| status(&error))?;
811 for mutation in request.mutations {
812 apply_mutation(&mut transaction, mutation).await?;
813 }
814 match transaction
815 .rebase(request.maximum_conflicts)
816 .await
817 .map_err(|error| status(&error))?
818 {
819 TransactionRebase::Rebased(generation) => {
820 Ok(Response::new(wire::RebaseTransactionResponse {
821 base: Some(generation_ref(&generation)),
822 conflicts: Vec::new(),
823 truncated: false,
824 }))
825 }
826 TransactionRebase::Conflicted {
827 conflicts,
828 truncated,
829 } => Ok(Response::new(wire::RebaseTransactionResponse {
830 base: Some(generation_ref(&base)),
831 conflicts: conflicts.into_iter().map(transaction_conflict).collect(),
832 truncated,
833 })),
834 }
835 }
836
837 async fn fork_workspace(
838 &self,
839 request: Request<wire::ForkWorkspaceRequest>,
840 ) -> Result<Response<wire::WorkspaceResponse>, Status> {
841 self.admit(&request)?;
842 let request = request.into_inner();
843 let idempotency_key = operation(request.operation)?;
844 let source = self.generation(request.source).await?;
845 let source_workspace = source.workspace.clone();
846 let destination = source_workspace
847 .fork(
848 request.destination_name,
849 ForkOptions::from_generation(source, idempotency_key),
850 )
851 .await
852 .map_err(|error| status(&error))?;
853 Ok(Response::new(wire::WorkspaceResponse {
854 workspace: Some(workspace_message(&destination).await?),
855 status: wire::MutationStatus::Committed as i32,
856 }))
857 }
858
859 async fn diff(
860 &self,
861 request: Request<wire::DiffRequest>,
862 ) -> Result<Response<wire::DiffResponse>, Status> {
863 self.admit(&request)?;
864 let request = request.into_inner();
865 if request.maximum_changes == 0 || request.maximum_changes > self.limits.maximum_page_items
866 {
867 return Err(Status::invalid_argument("diff bound is invalid"));
868 }
869 let from = self.generation(request.from).await?;
870 let to = self.generation(request.to).await?;
871 let workspace = from.workspace.clone();
872 let changes = workspace
873 .diff(&from, &to, request.maximum_changes)
874 .await
875 .map_err(|error| status(&error))?;
876 Ok(Response::new(wire::DiffResponse {
877 from: Some(generation_ref(changes.from())),
878 to: Some(generation_ref(changes.to())),
879 files: changes
880 .changes()
881 .files
882 .iter()
883 .map(file_record_change)
884 .collect(),
885 bindings: changes
886 .changes()
887 .bindings
888 .iter()
889 .map(directory_binding_change)
890 .collect(),
891 truncated: changes.changes().truncated,
892 work: Some(work_counters(changes.work())),
893 }))
894 }
895
896 async fn rebase(
897 &self,
898 request: Request<wire::RebaseRequest>,
899 ) -> Result<Response<wire::RebaseResponse>, Status> {
900 self.admit(&request)?;
901 let request = request.into_inner();
902 if request.maximum_generations == 0
903 || request.maximum_generations > self.limits.maximum_page_items
904 || request.maximum_changes == 0
905 || request.maximum_changes > self.limits.maximum_page_items
906 || request.maximum_conflicts == 0
907 || request.maximum_conflicts > self.limits.maximum_page_items
908 {
909 return Err(Status::invalid_argument("rebase bounds are invalid"));
910 }
911 let workspace = self.workspace(request.workspace).await?;
912 let outcome = workspace
913 .live_rebase(
914 operation(request.operation)?,
915 request.maximum_generations,
916 request.maximum_changes,
917 request.maximum_conflicts,
918 )
919 .await
920 .map_err(|error| status(&error))?;
921 Ok(Response::new(rebase_message(outcome)))
922 }
923
924 async fn plan_join(
925 &self,
926 request: Request<wire::PlanJoinRequest>,
927 ) -> Result<Response<wire::JoinPlan>, Status> {
928 self.admit(&request)?;
929 let request = request.into_inner();
930 if request.maximum_generations == 0
931 || request.maximum_generations > self.limits.maximum_page_items
932 || request.maximum_changes == 0
933 || request.maximum_changes > self.limits.maximum_page_items
934 || request.maximum_conflicts == 0
935 || request.maximum_conflicts > self.limits.maximum_page_items
936 {
937 return Err(Status::invalid_argument("join bounds are invalid"));
938 }
939 let history = join_history(request.history)?;
940 let source = self.generation(request.source).await?;
941 let target = self.generation(request.target).await?;
942 let source_workspace = source.workspace.clone();
943 let target_workspace = target.workspace.clone();
944 if source_workspace
945 .head()
946 .await
947 .map_err(|error| status(&error))?
948 .id()
949 != source.id()
950 || target_workspace
951 .head()
952 .await
953 .map_err(|error| status(&error))?
954 .id()
955 != target.id()
956 {
957 return Err(Status::failed_precondition(
958 "join endpoints must be current workspace heads",
959 ));
960 }
961 let plan = source_workspace
962 .join_into(&target_workspace)
963 .history(history)
964 .bounds(
965 request.maximum_generations,
966 request.maximum_changes,
967 request.maximum_conflicts,
968 )
969 .plan()
970 .await
971 .map_err(|error| status(&error))?;
972 if plan.source_head() != source.id() || plan.target_head() != target.id() {
973 return Err(Status::failed_precondition(
974 "join endpoints changed while the plan was computed",
975 ));
976 }
977 let source_ref = generation_ref_for(&source_workspace, plan.source_head());
980 let target_ref = generation_ref_for(&target_workspace, plan.target_head());
981 let base_ref = join_common_ancestor_ref(&plan, &source_workspace, &target_workspace);
982 let plan_id = join_plan_id(
983 &source_ref,
984 &target_ref,
985 &base_ref,
986 history,
987 request.maximum_generations,
988 request.maximum_changes,
989 request.maximum_conflicts,
990 );
991 let changes = plan
992 .source_changes()
993 .await
994 .map_err(|error| status(&error))?;
995 let file_changes = changes.value.files.iter().map(file_record_change).collect();
996 let binding_changes = changes
997 .value
998 .bindings
999 .iter()
1000 .map(directory_binding_change)
1001 .collect();
1002 Ok(Response::new(wire::JoinPlan {
1003 plan_id: plan_id.to_vec(),
1004 source: Some(source_ref),
1005 expected_target: Some(target_ref),
1006 file_changes,
1007 conflicts: Vec::new(),
1008 truncated: changes.value.truncated,
1009 common_ancestor: Some(base_ref),
1010 maximum_generations: request.maximum_generations,
1011 maximum_changes: request.maximum_changes,
1012 maximum_conflicts: request.maximum_conflicts,
1013 history: request.history,
1014 binding_changes,
1015 }))
1016 }
1017
1018 async fn apply_join(
1019 &self,
1020 request: Request<wire::ApplyJoinRequest>,
1021 ) -> Result<Response<wire::JoinResponse>, Status> {
1022 self.admit(&request)?;
1023 let request = request.into_inner();
1024 let supplied = required(request.plan, "join plan")?;
1025 let source_ref = required(supplied.source.clone(), "join source")?;
1026 let target_ref = required(supplied.expected_target.clone(), "join target")?;
1027 let base_ref = required(supplied.common_ancestor.clone(), "join common ancestor")?;
1028 let history = join_history(supplied.history)?;
1029 if supplied.maximum_generations == 0
1030 || supplied.maximum_generations > self.limits.maximum_page_items
1031 || supplied.maximum_changes == 0
1032 || supplied.maximum_changes > self.limits.maximum_page_items
1033 || supplied.maximum_conflicts == 0
1034 || supplied.maximum_conflicts > self.limits.maximum_page_items
1035 {
1036 return Err(Status::invalid_argument("join bounds are invalid"));
1037 }
1038 if supplied.plan_id
1039 != join_plan_id(
1040 &source_ref,
1041 &target_ref,
1042 &base_ref,
1043 history,
1044 supplied.maximum_generations,
1045 supplied.maximum_changes,
1046 supplied.maximum_conflicts,
1047 )
1048 {
1049 return Err(Status::invalid_argument("join plan identity mismatch"));
1050 }
1051 let source = self.generation(Some(source_ref)).await?;
1052 let target = self.generation(Some(target_ref)).await?;
1053 let source_workspace = source.workspace.clone();
1054 let target_workspace = target.workspace.clone();
1055 let source_id = source.id();
1056 let target_id = target.id();
1057 let operation = operation(request.operation)?;
1058 let (target_authority_head, existing_operation) = match self
1059 .join_target_head(&target_workspace, target_id, operation)
1060 .await?
1061 {
1062 JoinTargetHead::Expected {
1063 head,
1064 existing_operation,
1065 } => (head, existing_operation),
1066 JoinTargetHead::Stale(actual) => {
1067 return Ok(Response::new(join_response(
1068 wire::JoinStatus::StaleTarget,
1069 Some(&actual),
1070 )));
1071 }
1072 };
1073 let plan = source_workspace
1074 .join_into(&target_workspace)
1075 .history(history)
1076 .bounds(
1077 supplied.maximum_generations,
1078 supplied.maximum_changes,
1079 supplied.maximum_conflicts,
1080 )
1081 .plan_generations(source, target, target_authority_head)
1082 .await
1083 .map_err(|error| status(&error))?;
1084 let expected_base_ref =
1085 join_common_ancestor_ref(&plan, &source_workspace, &target_workspace);
1086 if plan.source_head() != source_id
1087 || plan.target_head() != target_id
1088 || base_ref != expected_base_ref
1089 {
1090 let actual = target_workspace
1091 .head()
1092 .await
1093 .map_err(|error| status(&error))?;
1094 return Ok(Response::new(wire::JoinResponse {
1095 status: wire::JoinStatus::StaleTarget as i32,
1096 generation: Some(generation_ref(&actual)),
1097 conflicts: Vec::new(),
1098 truncated: false,
1099 }));
1100 }
1101 let outcome = plan
1102 .apply(ApplyOptions {
1103 if_target: target_id,
1104 idempotency_key: operation,
1105 })
1106 .await
1107 .map_err(|error| status(&error))?;
1108 let outcome = if existing_operation
1109 && !matches!(
1110 &outcome,
1111 JoinOutcome::AlreadyApplied(_) | JoinOutcome::IdempotencyConflict
1112 ) {
1113 JoinOutcome::IdempotencyConflict
1114 } else {
1115 outcome
1116 };
1117 Ok(Response::new(join_message(outcome)))
1118 }
1119
1120 async fn checkpoint(
1121 &self,
1122 request: Request<wire::RetainGenerationRequest>,
1123 ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1124 self.retain(request, true).await
1125 }
1126
1127 async fn pin(
1128 &self,
1129 request: Request<wire::RetainGenerationRequest>,
1130 ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1131 self.retain(request, false).await
1132 }
1133
1134 type ExportStream = WireStream<wire::ExportChunk>;
1135
1136 async fn export(
1137 &self,
1138 request: Request<wire::ExportRequest>,
1139 ) -> Result<Response<Self::ExportStream>, Status> {
1140 self.admit(&request)?;
1141 let request = request.into_inner();
1142 if request.maximum_objects == 0
1143 || request.maximum_objects > self.limits.maximum_page_items
1144 || request.maximum_bytes == 0
1145 || request.maximum_bytes > self.limits.maximum_response_bytes
1146 {
1147 return Err(Status::invalid_argument("export bounds are invalid"));
1148 }
1149 let generation = self.generation(request.generation).await?;
1150 let checkout = generation
1151 .workspace
1152 .engine_checkout(
1153 crate::model::GenerationSelector::Exact(generation.id()),
1154 crate::model::CheckoutMode::read_only_pinned(),
1155 )
1156 .await
1157 .map_err(|error| status(&error))?;
1158 let manifest = checkout
1159 .export_manifest(crate::WorkBudget::UNBOUNDED, &CancellationToken::new())
1160 .await
1161 .map_err(|failure| Status::unavailable(failure.error.to_string()))?
1162 .value;
1163 let encoded = crate::encode_generation_export_manifest(&manifest)
1164 .map_err(|error| Status::data_loss(error.to_string()))?;
1165 let retained = u64::try_from(encoded.len()).unwrap_or(u64::MAX);
1166 if retained > request.maximum_bytes {
1167 return Err(Status::resource_exhausted(
1168 "export manifest exceeds byte bound",
1169 ));
1170 }
1171 let start = transfer_cursor(&request.after)?;
1172 let start_index = usize::try_from(start)
1173 .map_err(|_| Status::invalid_argument("export cursor is out of range"))?;
1174 if start_index > manifest.objects.len() {
1175 return Err(Status::invalid_argument("export cursor is out of range"));
1176 }
1177 let manifest_chunk = (start == 0).then(|| wire::ExportChunk {
1178 cursor: 0_u64.to_le_bytes().to_vec(),
1179 object_id: Vec::new(),
1180 contents: encoded,
1181 terminal: manifest.objects.is_empty(),
1182 });
1183 let filesystem = self.filesystem.clone();
1184 let objects = manifest.objects;
1185 let maximum_objects = request.maximum_objects;
1186 let maximum_bytes = request.maximum_bytes;
1187 let object_stream = stream::try_unfold(
1188 (filesystem, objects, start_index, start, retained, 0_u32),
1189 move |(filesystem, objects, index, cursor, retained, emitted)| async move {
1190 if index == objects.len() || emitted == maximum_objects || retained == maximum_bytes
1191 {
1192 return Ok(None);
1193 }
1194 let Some(&object) = objects.get(index) else {
1195 return Ok(None);
1196 };
1197 let remaining = maximum_bytes.saturating_sub(retained);
1198 let body = filesystem
1199 .export_object(
1200 object,
1201 remaining,
1202 crate::WorkBudget::UNBOUNDED,
1203 &CancellationToken::new(),
1204 )
1205 .await
1206 .map_err(|failure| Status::resource_exhausted(failure.error.to_string()))?
1207 .value
1208 .bytes;
1209 let retained = retained
1210 .checked_add(u64::try_from(body.len()).unwrap_or(u64::MAX))
1211 .ok_or_else(|| Status::resource_exhausted("export byte count overflow"))?;
1212 let next_index = index.saturating_add(1);
1213 let next_cursor = cursor.saturating_add(1);
1214 let chunk = wire::ExportChunk {
1215 cursor: next_cursor.to_le_bytes().to_vec(),
1216 object_id: encode_object_id(object),
1217 contents: body.to_vec(),
1218 terminal: next_index == objects.len(),
1219 };
1220 Ok(Some((
1221 chunk,
1222 (
1223 filesystem,
1224 objects,
1225 next_index,
1226 next_cursor,
1227 retained,
1228 emitted.saturating_add(1),
1229 ),
1230 )))
1231 },
1232 );
1233 let output = stream::iter(manifest_chunk.map(Ok)).chain(object_stream);
1234 Ok(Response::new(Box::pin(output)))
1235 }
1236
1237 async fn import(
1238 &self,
1239 request: Request<tonic::Streaming<wire::ImportChunk>>,
1240 ) -> Result<Response<wire::ImportResponse>, Status> {
1241 let mut stream = request.into_inner();
1242 let first = stream
1243 .message()
1244 .await?
1245 .ok_or_else(|| Status::invalid_argument("import stream is empty"))?;
1246 self.admit_message(&first)?;
1247 if !first.object_id.is_empty() || first.contents.is_empty() {
1248 return Err(Status::invalid_argument(
1249 "import must begin with a manifest chunk",
1250 ));
1251 }
1252 let workspace_ref = required(first.workspace, "workspace")?;
1253 let operation_bytes = first.operation_id.clone();
1254 let operation_id = operation(Some(wire::OperationOptions {
1255 idempotency_key: operation_bytes.clone(),
1256 }))?
1257 .operation_id();
1258 let manifest = crate::decode_generation_export_manifest(
1259 &first.contents,
1260 self.limits.maximum_response_bytes,
1261 u64::from(self.limits.maximum_page_items),
1262 )
1263 .map_err(|error| Status::invalid_argument(error.to_string()))?;
1264 let expected_workspace = self
1265 .filesystem
1266 .workspace_id(&workspace_ref.name)
1267 .map_err(|error| Status::invalid_argument(error.to_string()))?;
1268 if expected_workspace.into_bytes().as_slice() != workspace_ref.workspace_id
1269 || expected_workspace.volume_id() != manifest.volume_id
1270 {
1271 return Err(Status::failed_precondition(
1272 "import workspace identity mismatch",
1273 ));
1274 }
1275 let mut expected_index = 0_usize;
1276 let mut terminal = first.terminal;
1277 while let Some(chunk) = stream.message().await? {
1278 self.admit_message(&chunk)?;
1279 if expected_index >= self.limits.maximum_page_items as usize {
1280 return Err(Status::resource_exhausted(
1281 "import object count exceeds service bound",
1282 ));
1283 }
1284 if chunk.workspace.as_ref() != Some(&workspace_ref)
1285 || chunk.operation_id != operation_bytes
1286 || terminal
1287 {
1288 return Err(Status::invalid_argument(
1289 "import stream continuity mismatch",
1290 ));
1291 }
1292 let object = decode_object_id(&chunk.object_id)?;
1293 if manifest.objects.get(expected_index).copied() != Some(object) {
1294 return Err(Status::invalid_argument("import object order mismatch"));
1295 }
1296 self.filesystem
1297 .import_object(
1298 object,
1299 Bytes::from(chunk.contents),
1300 crate::WorkBudget::UNBOUNDED,
1301 &CancellationToken::new(),
1302 )
1303 .await
1304 .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1305 expected_index = expected_index.saturating_add(1);
1306 if transfer_cursor(&chunk.cursor)? != expected_index as u64 {
1307 return Err(Status::invalid_argument("import cursor mismatch"));
1308 }
1309 terminal = chunk.terminal;
1310 }
1311 if !terminal || expected_index != manifest.objects.len() {
1312 return Err(Status::invalid_argument(
1313 "import stream ended before its closure",
1314 ));
1315 }
1316 self.filesystem
1317 .restore_volume(
1318 &manifest,
1319 operation_id,
1320 crate::WorkBudget::UNBOUNDED,
1321 &CancellationToken::new(),
1322 )
1323 .await
1324 .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1325 let workspace = self
1326 .filesystem
1327 .open_workspace(&workspace_ref.name)
1328 .await
1329 .map_err(|error| status(&error))?;
1330 Ok(Response::new(wire::ImportResponse {
1331 outcome: Some(mutation(
1332 wire::MutationStatus::Committed,
1333 Some(generation_ref(
1334 &workspace.head().await.map_err(|error| status(&error))?,
1335 )),
1336 None,
1337 )),
1338 }))
1339 }
1340
1341 async fn issue_mount_credential(
1342 &self,
1343 request: Request<wire::CredentialRequest>,
1344 ) -> Result<Response<wire::CredentialResponse>, Status> {
1345 self.issue_credential(request, CredentialKind::Mount).await
1346 }
1347
1348 async fn issue_s3_credential(
1349 &self,
1350 request: Request<wire::CredentialRequest>,
1351 ) -> Result<Response<wire::CredentialResponse>, Status> {
1352 self.issue_credential(request, CredentialKind::S3).await
1353 }
1354
1355 async fn get_source_state(
1356 &self,
1357 request: Request<wire::SourceStateRequest>,
1358 ) -> Result<Response<wire::SourceResponse>, Status> {
1359 self.admit(&request)?;
1360 let (workspace, scope) = self.source_scope(request.into_inner().workspace).await?;
1361 let result = self.source_provider()?.state(scope).await?;
1362 Ok(Response::new(
1363 self.source_response(&workspace, result).await?,
1364 ))
1365 }
1366
1367 async fn reconcile_source(
1368 &self,
1369 request: Request<wire::SourceOperationRequest>,
1370 ) -> Result<Response<wire::SourceResponse>, Status> {
1371 self.admit(&request)?;
1372 let request = request.into_inner();
1373 let idempotency_key = operation(request.operation)?;
1374 let (workspace, scope) = self.source_scope(request.workspace).await?;
1375 let result = self
1376 .source_provider()?
1377 .reconcile(HostedSourceOperation {
1378 scope,
1379 idempotency_key,
1380 })
1381 .await?;
1382 Ok(Response::new(
1383 self.source_response(&workspace, result).await?,
1384 ))
1385 }
1386
1387 async fn rescan_source(
1388 &self,
1389 request: Request<wire::SourceOperationRequest>,
1390 ) -> Result<Response<wire::SourceResponse>, Status> {
1391 self.admit(&request)?;
1392 let request = request.into_inner();
1393 let idempotency_key = operation(request.operation)?;
1394 let (workspace, scope) = self.source_scope(request.workspace).await?;
1395 let result = self
1396 .source_provider()?
1397 .rescan(HostedSourceOperation {
1398 scope,
1399 idempotency_key,
1400 })
1401 .await?;
1402 Ok(Response::new(
1403 self.source_response(&workspace, result).await?,
1404 ))
1405 }
1406
1407 async fn seal_source(
1408 &self,
1409 request: Request<wire::SourceOperationRequest>,
1410 ) -> Result<Response<wire::SourceResponse>, Status> {
1411 self.admit(&request)?;
1412 let request = request.into_inner();
1413 let idempotency_key = operation(request.operation)?;
1414 let (workspace, scope) = self.source_scope(request.workspace).await?;
1415 let result = self
1416 .source_provider()?
1417 .seal(HostedSourceOperation {
1418 scope,
1419 idempotency_key,
1420 })
1421 .await?;
1422 Ok(Response::new(
1423 self.source_response(&workspace, result).await?,
1424 ))
1425 }
1426
1427 async fn observe(
1428 &self,
1429 request: Request<wire::ObserveRequest>,
1430 ) -> Result<Response<wire::ObserveResponse>, Status> {
1431 self.admit(&request)?;
1432 let request = request.into_inner();
1433 let workspace = self.workspace(request.workspace).await?;
1434 let operation_id = raw_operation(&request.operation_id)?;
1435 let observed = self
1436 .filesystem
1437 .observe_volume_operation(
1438 workspace.id().volume_id(),
1439 operation_id,
1440 crate::WorkBudget::UNBOUNDED,
1441 &CancellationToken::new(),
1442 )
1443 .await
1444 .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1445 let response = match observed.value {
1446 Some(commit) => wire::ObserveResponse {
1447 state: "completed".to_owned(),
1448 outcome: Some(observed_mutation(&workspace, &commit)?),
1449 },
1450 None => wire::ObserveResponse {
1451 state: "unknown".to_owned(),
1452 outcome: None,
1453 },
1454 };
1455 Ok(Response::new(response))
1456 }
1457
1458 async fn cancel(
1459 &self,
1460 request: Request<wire::CancelRequest>,
1461 ) -> Result<Response<wire::CancelResponse>, Status> {
1462 self.admit(&request)?;
1463 let request = request.into_inner();
1464 let observed = self
1465 .observe(Request::new(wire::ObserveRequest {
1466 workspace: request.workspace,
1467 operation_id: request.operation_id,
1468 }))
1469 .await?
1470 .into_inner();
1471 Ok(Response::new(wire::CancelResponse {
1472 operation: Some(observed),
1473 }))
1474 }
1475}
1476
1477impl<A: AsyncAuthorityStore, O: AsyncObjectStore> FilesystemWireService<A, O> {
1478 async fn issue_credential(
1479 &self,
1480 request: Request<wire::CredentialRequest>,
1481 kind: CredentialKind,
1482 ) -> Result<Response<wire::CredentialResponse>, Status> {
1483 self.admit(&request)?;
1484 let request = request.into_inner();
1485 if request.expires_after_seconds == 0
1486 || request.expires_after_seconds > self.limits.maximum_credential_seconds
1487 {
1488 return Err(Status::invalid_argument(
1489 "credential lifetime exceeds the configured bound",
1490 ));
1491 }
1492 let workspace = self.workspace(request.workspace).await?;
1493 let generation = match request.generation {
1494 Some(reference) => {
1495 let generation = self.generation(Some(reference)).await?;
1496 if generation.workspace.id() != workspace.id() {
1497 return Err(Status::failed_precondition(
1498 "credential generation belongs to another workspace",
1499 ));
1500 }
1501 generation
1502 }
1503 None => workspace.head().await.map_err(|error| status(&error))?,
1504 };
1505 let issuer = self.credential_issuer.as_ref().ok_or_else(|| {
1506 Status::unimplemented("credential issuance is unavailable in this deployment")
1507 })?;
1508 let enabled = match kind {
1509 CredentialKind::Mount => issuer.mount_enabled(),
1510 CredentialKind::S3 => issuer.s3_enabled(),
1511 };
1512 if !enabled {
1513 return Err(Status::unimplemented(
1514 "requested credential surface is unavailable",
1515 ));
1516 }
1517 let grant = issuer
1518 .issue(CredentialGrantRequest {
1519 kind,
1520 workspace_id: workspace.id(),
1521 workspace_name: workspace.name().clone(),
1522 generation_id: generation.id(),
1523 writable: request.writable,
1524 expires_after_seconds: request.expires_after_seconds,
1525 idempotency_key: operation(request.operation)?,
1526 })
1527 .await?;
1528 let response = match grant {
1529 CredentialGrant::Bearer {
1530 endpoint,
1531 token,
1532 expires_at_unix_seconds,
1533 } if kind == CredentialKind::Mount
1534 && !endpoint.is_empty()
1535 && !token.is_empty()
1536 && expires_at_unix_seconds > 0 =>
1537 {
1538 wire::CredentialResponse {
1539 endpoint,
1540 expires_at_unix_seconds,
1541 credential: Some(wire::credential_response::Credential::BearerToken(token)),
1542 }
1543 }
1544 CredentialGrant::S3 {
1545 endpoint,
1546 bucket,
1547 region,
1548 access_key_id,
1549 secret_access_key,
1550 session_token,
1551 expires_at_unix_seconds,
1552 } if kind == CredentialKind::S3
1553 && !endpoint.is_empty()
1554 && !bucket.is_empty()
1555 && !region.is_empty()
1556 && !access_key_id.is_empty()
1557 && !secret_access_key.is_empty()
1558 && expires_at_unix_seconds > 0 =>
1559 {
1560 wire::CredentialResponse {
1561 endpoint,
1562 expires_at_unix_seconds,
1563 credential: Some(wire::credential_response::Credential::S3(
1564 wire::S3Credential {
1565 bucket,
1566 region,
1567 access_key_id,
1568 secret_access_key,
1569 session_token,
1570 },
1571 )),
1572 }
1573 }
1574 _ => {
1575 return Err(Status::internal(
1576 "credential issuer returned an invalid grant",
1577 ));
1578 }
1579 };
1580 Ok(Response::new(response))
1581 }
1582
1583 async fn retain(
1584 &self,
1585 request: Request<wire::RetainGenerationRequest>,
1586 checkpoint: bool,
1587 ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1588 self.admit(&request)?;
1589 let request = request.into_inner();
1590 let _ = operation(request.operation)?;
1591 let generation = self.generation(request.generation).await?;
1592 if checkpoint {
1593 let workspace = self
1594 .workspace(generation_ref(&generation).workspace)
1595 .await?;
1596 let head = workspace.head().await.map_err(|error| status(&error))?;
1597 if head.id() != generation.id() {
1598 return Err(Status::failed_precondition(
1599 "checkpoint requires the current head",
1600 ));
1601 }
1602 workspace
1603 .checkpoint(&request.identity)
1604 .await
1605 .map_err(|error| status(&error))?;
1606 } else {
1607 generation
1608 .pin(&request.identity)
1609 .await
1610 .map_err(|error| status(&error))?;
1611 }
1612 Ok(Response::new(wire::RetainGenerationResponse {
1613 generation: Some(generation_ref(&generation)),
1614 identity: request.identity,
1615 status: wire::MutationStatus::Committed as i32,
1616 }))
1617 }
1618}
1619
1620async fn apply_mutation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1621 transaction: &mut Transaction<A, O>,
1622 value: wire::Mutation,
1623) -> Result<(), Status> {
1624 use wire::mutation::Mutation;
1625 match required(value.mutation, "mutation")? {
1626 Mutation::CreateFile(value) => {
1627 transaction
1628 .create_file(
1629 &value.path,
1630 Bytes::from(value.contents),
1631 metadata(value.metadata)?,
1632 )
1633 .await
1634 }
1635 Mutation::CreateDirectory(value) => {
1636 transaction
1637 .create_directory(&value.path)
1638 .await
1639 .map_err(|error| status(&error))?;
1640 transaction
1641 .set_metadata(&value.path, metadata(value.metadata)?)
1642 .await
1643 }
1644 Mutation::CreateSymbolicLink(value) => {
1645 transaction
1646 .create_symbolic_link(&value.path, Bytes::from(value.target))
1647 .await
1648 .map_err(|error| status(&error))?;
1649 transaction
1650 .set_metadata(&value.path, metadata(value.metadata)?)
1651 .await
1652 }
1653 Mutation::Remove(value) => transaction.remove(&value.path).await,
1654 Mutation::Rename(value) => {
1655 transaction
1656 .rename_with_replace(&value.source, &value.destination, value.replace)
1657 .await
1658 }
1659 Mutation::HardLink(value) => {
1660 transaction
1661 .hard_link(&value.source, &value.destination)
1662 .await
1663 }
1664 Mutation::Write(value) => {
1665 transaction
1666 .write_range(&value.path, value.offset, Bytes::from(value.contents))
1667 .await
1668 }
1669 Mutation::Resize(value) => transaction.resize(&value.path, value.logical_bytes).await,
1670 Mutation::ZeroRange(value) => {
1671 let range = required(value.range, "range")?;
1672 transaction
1673 .zero_range(
1674 &value.path,
1675 ByteRange {
1676 offset: range.offset,
1677 length: range.length,
1678 },
1679 value.allocated,
1680 value.extend,
1681 )
1682 .await
1683 }
1684 Mutation::Preallocate(value) => {
1685 let range = required(value.range, "range")?;
1686 transaction
1687 .preallocate(
1688 &value.path,
1689 ByteRange {
1690 offset: range.offset,
1691 length: range.length,
1692 },
1693 value.keep_size,
1694 )
1695 .await
1696 }
1697 Mutation::CloneRange(value) => {
1698 transaction
1699 .clone_range(
1700 &value.source,
1701 value.source_offset,
1702 &value.destination,
1703 value.destination_offset,
1704 value.length,
1705 )
1706 .await
1707 }
1708 Mutation::SetMetadata(value) => {
1709 transaction
1710 .set_metadata(&value.path, metadata(value.metadata)?)
1711 .await
1712 }
1713 Mutation::CreateDirectories(value) => transaction.create_dir_all(&value.path).await,
1714 Mutation::PutFile(value) => {
1715 transaction
1716 .write(&value.path, Bytes::from(value.contents))
1717 .await
1718 }
1719 Mutation::CopyFile(value) => transaction.copy(&value.source, &value.destination).await,
1720 }
1721 .map_err(|error| status(&error))
1722}
1723
1724fn metadata(value: Option<wire::Metadata>) -> Result<FileMetadata, Status> {
1725 let value = value.unwrap_or_default();
1726 if value.has_named_attributes || value.has_acl || value.has_security_descriptor {
1727 return Err(Status::invalid_argument(
1728 "opaque metadata bodies are required separately",
1729 ));
1730 }
1731 Ok(FileMetadata {
1732 posix_mode: input_u32(value.posix_mode)?,
1733 posix_uid: input_u32(value.posix_uid)?,
1734 posix_gid: input_u32(value.posix_gid)?,
1735 posix_flags: input_u64(value.posix_flags)?,
1736 windows_attributes: input_u32(value.windows_attributes)?,
1737 created_ns: input_i64(value.created_ns)?,
1738 modified_ns: input_i64(value.modified_ns)?,
1739 accessed_ns: input_i64(value.accessed_ns)?,
1740 changed_ns: input_i64(value.changed_ns)?,
1741 named_attributes: MetadataField::Unavailable,
1742 acl: MetadataField::Unavailable,
1743 security_descriptor: MetadataField::Unavailable,
1744 })
1745}
1746
1747fn input_u32(value: Option<wire::OptionalU32>) -> Result<MetadataField<u32>, Status> {
1748 match value.and_then(|value| value.value) {
1749 None | Some(wire::optional_u32::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1750 Some(wire::optional_u32::Value::Present(value)) => Ok(MetadataField::Value(value)),
1751 Some(wire::optional_u32::Value::Unavailable(false)) => {
1752 Err(Status::invalid_argument("unavailable marker must be true"))
1753 }
1754 }
1755}
1756
1757fn input_u64(value: Option<wire::OptionalU64>) -> Result<MetadataField<u64>, Status> {
1758 match value.and_then(|value| value.value) {
1759 None | Some(wire::optional_u64::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1760 Some(wire::optional_u64::Value::Present(value)) => Ok(MetadataField::Value(value)),
1761 Some(wire::optional_u64::Value::Unavailable(false)) => {
1762 Err(Status::invalid_argument("unavailable marker must be true"))
1763 }
1764 }
1765}
1766
1767fn input_i64(value: Option<wire::OptionalI64>) -> Result<MetadataField<i64>, Status> {
1768 match value.and_then(|value| value.value) {
1769 None | Some(wire::optional_i64::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1770 Some(wire::optional_i64::Value::Present(value)) => Ok(MetadataField::Value(value)),
1771 Some(wire::optional_i64::Value::Unavailable(false)) => {
1772 Err(Status::invalid_argument("unavailable marker must be true"))
1773 }
1774 }
1775}
1776
1777fn profile(value: i32) -> Result<EngineProfile, Status> {
1778 match wire::FilesystemProfile::try_from(value).ok() {
1779 Some(wire::FilesystemProfile::Portable) => Ok(EngineProfile::Portable),
1780 Some(wire::FilesystemProfile::Posix) => Ok(EngineProfile::Posix),
1781 Some(wire::FilesystemProfile::Windows) => Ok(EngineProfile::Windows),
1782 Some(wire::FilesystemProfile::Browser) => Ok(EngineProfile::Browser),
1783 Some(wire::FilesystemProfile::Unspecified) | None => {
1784 Err(Status::invalid_argument("filesystem profile is required"))
1785 }
1786 }
1787}
1788
1789fn profile_message(value: EngineProfile) -> i32 {
1790 match value {
1791 EngineProfile::Portable => wire::FilesystemProfile::Portable as i32,
1792 EngineProfile::Posix => wire::FilesystemProfile::Posix as i32,
1793 EngineProfile::Windows => wire::FilesystemProfile::Windows as i32,
1794 EngineProfile::Browser => wire::FilesystemProfile::Browser as i32,
1795 }
1796}
1797
1798fn source_state_message(
1799 value: HostedSourceState,
1800) -> (wire::SourceState, wire::SourceInvalidationReason) {
1801 let none = wire::SourceInvalidationReason::Unspecified;
1802 match value {
1803 HostedSourceState::Clean => (wire::SourceState::Clean, none),
1804 HostedSourceState::PendingCapture => (wire::SourceState::PendingCapture, none),
1805 HostedSourceState::Conflict => (wire::SourceState::Conflict, none),
1806 HostedSourceState::Sealed => (wire::SourceState::Sealed, none),
1807 HostedSourceState::NeedsRescan(reason) => (
1808 wire::SourceState::NeedsRescan,
1809 match reason {
1810 HostedSourceInvalidation::InitialSnapshotRequired => {
1811 wire::SourceInvalidationReason::InitialSnapshotRequired
1812 }
1813 HostedSourceInvalidation::QueueOverflow => {
1814 wire::SourceInvalidationReason::QueueOverflow
1815 }
1816 HostedSourceInvalidation::NativeRescanRequired => {
1817 wire::SourceInvalidationReason::NativeRescanRequired
1818 }
1819 HostedSourceInvalidation::BackendError => {
1820 wire::SourceInvalidationReason::BackendError
1821 }
1822 HostedSourceInvalidation::UnrepresentablePath => {
1823 wire::SourceInvalidationReason::UnrepresentablePath
1824 }
1825 HostedSourceInvalidation::AmbiguousRename => {
1826 wire::SourceInvalidationReason::AmbiguousRename
1827 }
1828 HostedSourceInvalidation::RootChanged => {
1829 wire::SourceInvalidationReason::RootChanged
1830 }
1831 },
1832 ),
1833 }
1834}
1835
1836fn operation(value: Option<wire::OperationOptions>) -> Result<IdempotencyKey, Status> {
1837 let value = required(value, "operation")?;
1838 Ok(IdempotencyKey::from_bytes(raw_operation_bytes(
1839 &value.idempotency_key,
1840 )?))
1841}
1842
1843fn raw_operation(value: &[u8]) -> Result<crate::OperationId, Status> {
1844 Ok(crate::OperationId::from_bytes(raw_operation_bytes(value)?))
1845}
1846
1847fn raw_operation_bytes(value: &[u8]) -> Result<[u8; 16], Status> {
1848 value
1849 .try_into()
1850 .map_err(|_| Status::invalid_argument("idempotency_key must contain 16 bytes"))
1851}
1852
1853fn generation_id(value: &[u8]) -> Result<GenerationId, Status> {
1854 let bytes: [u8; 32] = value
1855 .try_into()
1856 .map_err(|_| Status::invalid_argument("generation_id must contain 32 bytes"))?;
1857 Ok(GenerationId::new(Digest::from_bytes(bytes)))
1858}
1859
1860fn workspace_ref<A, O>(workspace: &Workspace<A, O>) -> wire::WorkspaceRef {
1861 wire::WorkspaceRef {
1862 workspace_id: workspace.id().into_bytes().to_vec(),
1863 name: workspace.name().as_str().to_owned(),
1864 }
1865}
1866
1867fn generation_ref<A, O>(generation: &Generation<A, O>) -> wire::GenerationRef {
1868 generation_ref_for(&generation.workspace, generation.id())
1869}
1870
1871fn generation_ref_for<A, O>(
1872 workspace: &Workspace<A, O>,
1873 generation: GenerationId,
1874) -> wire::GenerationRef {
1875 wire::GenerationRef {
1876 workspace: Some(workspace_ref(workspace)),
1877 generation_id: generation.digest().into_bytes().to_vec(),
1878 }
1879}
1880
1881fn join_common_ancestor_ref<A, O>(
1882 plan: &crate::JoinPlan<A, O>,
1883 source: &Workspace<A, O>,
1884 target: &Workspace<A, O>,
1885) -> wire::GenerationRef {
1886 let volume = plan.common_ancestor_volume();
1887 let workspace = if volume == source.id().volume_id() {
1888 Some(workspace_ref(source))
1889 } else if volume == target.id().volume_id() {
1890 Some(workspace_ref(target))
1891 } else {
1892 None
1893 };
1894 wire::GenerationRef {
1895 workspace,
1896 generation_id: plan.common_ancestor().digest().into_bytes().to_vec(),
1897 }
1898}
1899
1900fn observed_mutation<A, O>(
1901 workspace: &Workspace<A, O>,
1902 commit: &DurableCommit,
1903) -> Result<wire::MutationResponse, Status> {
1904 const MAXIMUM_EVENT_BYTES: u64 = 4 * 1024;
1905 let expected = workspace.id().volume_id();
1906 if let Ok(deleted) =
1907 crate::kernel::decode_workspace_deleted(&commit.payload, MAXIMUM_EVENT_BYTES)
1908 {
1909 if deleted != expected {
1910 return Err(Status::data_loss("operation workspace identity mismatch"));
1911 }
1912 return Ok(mutation(wire::MutationStatus::Committed, None, None));
1913 }
1914 let root = if let Ok(publication) =
1915 crate::kernel::decode_published_generation(&commit.payload, MAXIMUM_EVENT_BYTES)
1916 {
1917 if publication.volume_id != expected {
1918 return Err(Status::data_loss("operation workspace identity mismatch"));
1919 }
1920 publication.generation_root
1921 } else if let Ok(created) =
1922 crate::kernel::decode_volume_created(&commit.payload, MAXIMUM_EVENT_BYTES)
1923 {
1924 if created.volume_id != expected {
1925 return Err(Status::data_loss("operation workspace identity mismatch"));
1926 }
1927 created.initial_generation_root
1928 } else {
1929 return Err(Status::data_loss(
1930 "operation does not contain a workspace mutation",
1931 ));
1932 };
1933 Ok(mutation(
1934 wire::MutationStatus::Committed,
1935 Some(wire::GenerationRef {
1936 workspace: Some(workspace_ref(workspace)),
1937 generation_id: root.digest.into_bytes().to_vec(),
1938 }),
1939 None,
1940 ))
1941}
1942
1943async fn workspace_message<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1944 workspace: &Workspace<A, O>,
1945) -> Result<wire::Workspace, Status> {
1946 let head = workspace.head().await.map_err(|error| status(&error))?;
1947 Ok(wire::Workspace {
1948 workspace: Some(workspace_ref(workspace)),
1949 name: workspace.name().as_str().to_owned(),
1950 profile: profile_message(workspace.profile()),
1951 head: Some(generation_ref(&head)),
1952 deleted: false,
1953 })
1954}
1955
1956async fn generation_message<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1957 generation: &Generation<A, O>,
1958) -> Result<wire::GenerationResponse, Status> {
1959 let reference = generation_ref(generation);
1960 let workspace = reference.workspace.clone();
1961 let parents = generation
1962 .parents()
1963 .await
1964 .map_err(|error| status(&error))?
1965 .into_iter()
1966 .map(|id| wire::GenerationRef {
1967 workspace: workspace.clone(),
1968 generation_id: id.digest().into_bytes().to_vec(),
1969 })
1970 .collect();
1971 Ok(wire::GenerationResponse {
1972 generation: Some(reference),
1973 parents,
1974 })
1975}
1976
1977fn commit_message<A, O>(value: TransactionCommit<A, O>) -> wire::MutationResponse {
1978 match value {
1979 TransactionCommit::Committed(generation) => mutation(
1980 wire::MutationStatus::Committed,
1981 Some(generation_ref(&generation)),
1982 None,
1983 ),
1984 TransactionCommit::AlreadyCommitted(generation) => mutation(
1985 wire::MutationStatus::AlreadyCommitted,
1986 Some(generation_ref(&generation)),
1987 None,
1988 ),
1989 TransactionCommit::Conflict { actual } => mutation(
1990 wire::MutationStatus::Conflict,
1991 None,
1992 Some(generation_ref(&actual)),
1993 ),
1994 TransactionCommit::Fenced => mutation(wire::MutationStatus::Fenced, None, None),
1995 TransactionCommit::IdempotencyConflict => {
1996 mutation(wire::MutationStatus::IdempotencyConflict, None, None)
1997 }
1998 }
1999}
2000
2001fn join_message<A, O>(value: JoinOutcome<A, O>) -> wire::JoinResponse {
2002 match value {
2003 JoinOutcome::Applied(generation) => {
2004 join_response(wire::JoinStatus::Applied, Some(&generation))
2005 }
2006 JoinOutcome::AlreadyApplied(generation) => {
2007 join_response(wire::JoinStatus::AlreadyApplied, Some(&generation))
2008 }
2009 JoinOutcome::NoChanges(generation) => {
2010 join_response(wire::JoinStatus::NoChanges, Some(&generation))
2011 }
2012 JoinOutcome::StaleTarget(actual) => {
2013 join_response(wire::JoinStatus::StaleTarget, Some(&actual))
2014 }
2015 JoinOutcome::Conflicted {
2016 conflicts,
2017 truncated,
2018 } => wire::JoinResponse {
2019 status: wire::JoinStatus::Conflicted as i32,
2020 generation: None,
2021 conflicts: conflicts.into_iter().map(merge_conflict).collect(),
2022 truncated,
2023 },
2024 JoinOutcome::Fenced => join_response::<A, O>(wire::JoinStatus::Fenced, None),
2025 JoinOutcome::IdempotencyConflict => {
2026 join_response::<A, O>(wire::JoinStatus::IdempotencyConflict, None)
2027 }
2028 }
2029}
2030
2031fn join_response<A, O>(
2032 status: wire::JoinStatus,
2033 generation: Option<&Generation<A, O>>,
2034) -> wire::JoinResponse {
2035 wire::JoinResponse {
2036 status: status as i32,
2037 generation: generation.map(generation_ref),
2038 conflicts: Vec::new(),
2039 truncated: false,
2040 }
2041}
2042
2043fn file_record_change(change: &crate::FileRecordChange) -> wire::FileRecordChange {
2044 wire::FileRecordChange {
2045 file_id: change.file_id.into_bytes().to_vec(),
2046 before: change.before.map(file_record_snapshot),
2047 after: change.after.map(file_record_snapshot),
2048 }
2049}
2050
2051fn file_record_snapshot(record: crate::kernel::FileRecord) -> wire::FileRecordSnapshot {
2052 let mut value = wire::FileRecordSnapshot {
2053 file_id: record.file_id.into_bytes().to_vec(),
2054 file_kind: file_kind(record.kind),
2055 link_count: record.link_count,
2056 metadata_object: encode_object_id(record.metadata),
2057 payload_kind: String::new(),
2058 logical_bytes: None,
2059 payload_object: Vec::new(),
2060 inline_bytes: Vec::new(),
2061 device_major: None,
2062 device_minor: None,
2063 };
2064 match record.payload {
2065 FilePayload::InlineRegular(bytes) => {
2066 value.payload_kind = "inline-regular".to_owned();
2067 value.logical_bytes = Some(output_u64(Some(
2068 u64::try_from(bytes.as_bytes().len()).unwrap_or(u64::MAX),
2069 )));
2070 value.inline_bytes = bytes.as_bytes().to_vec();
2071 }
2072 FilePayload::Regular {
2073 logical_bytes,
2074 extents,
2075 } => {
2076 value.payload_kind = "regular".to_owned();
2077 value.logical_bytes = Some(output_u64(Some(logical_bytes)));
2078 value.payload_object = encode_object_id(extents);
2079 }
2080 FilePayload::Directory { entries } => {
2081 value.payload_kind = "directory".to_owned();
2082 value.payload_object = encode_object_id(entries);
2083 }
2084 FilePayload::SymbolicLink {
2085 target_bytes,
2086 target,
2087 } => {
2088 value.payload_kind = "symbolic-link".to_owned();
2089 value.logical_bytes = Some(output_u64(Some(target_bytes)));
2090 value.payload_object = encode_object_id(target);
2091 }
2092 FilePayload::Empty => value.payload_kind = "empty".to_owned(),
2093 FilePayload::Device { major, minor } => {
2094 value.payload_kind = "device".to_owned();
2095 value.device_major = Some(output_u32(Some(major)));
2096 value.device_minor = Some(output_u32(Some(minor)));
2097 }
2098 FilePayload::ReparsePoint {
2099 payload_bytes,
2100 payload,
2101 } => {
2102 value.payload_kind = "reparse-point".to_owned();
2103 value.logical_bytes = Some(output_u64(Some(payload_bytes)));
2104 value.payload_object = encode_object_id(payload);
2105 }
2106 }
2107 value
2108}
2109
2110fn directory_binding_change(
2111 change: &crate::DirectoryBindingChange,
2112) -> wire::DirectoryBindingChange {
2113 wire::DirectoryBindingChange {
2114 directory_id: change.directory_id.into_bytes().to_vec(),
2115 name: Some(logical_name(&change.name)),
2116 before: change.before.as_ref().map(tree_entry_snapshot),
2117 after: change.after.as_ref().map(tree_entry_snapshot),
2118 }
2119}
2120
2121fn tree_entry_snapshot(entry: &TreeEntry) -> wire::TreeEntrySnapshot {
2122 wire::TreeEntrySnapshot {
2123 name: Some(logical_name(&entry.name)),
2124 file_id: entry.file_id.into_bytes().to_vec(),
2125 file_kind: file_kind(entry.kind),
2126 }
2127}
2128
2129fn logical_name(name: &LogicalName) -> wire::LogicalName {
2130 let encoding = match name.encoding() {
2131 NameEncoding::Utf8 => wire::NameEncoding::Utf8,
2132 NameEncoding::PosixBytes => wire::NameEncoding::PosixBytes,
2133 NameEncoding::WindowsUtf16Le => wire::NameEncoding::WindowsUtf16le,
2134 };
2135 wire::LogicalName {
2136 encoding: encoding as i32,
2137 bytes: name.as_bytes().to_vec(),
2138 }
2139}
2140
2141fn input_logical_name(name: wire::LogicalName) -> Result<LogicalName, Status> {
2142 let encoding = match wire::NameEncoding::try_from(name.encoding)
2143 .map_err(|_| Status::invalid_argument("name encoding is invalid"))?
2144 {
2145 wire::NameEncoding::Utf8 => NameEncoding::Utf8,
2146 wire::NameEncoding::PosixBytes => NameEncoding::PosixBytes,
2147 wire::NameEncoding::WindowsUtf16le => NameEncoding::WindowsUtf16Le,
2148 wire::NameEncoding::Unspecified => {
2149 return Err(Status::invalid_argument("name encoding is required"));
2150 }
2151 };
2152 LogicalName::new(encoding, name.bytes, 255)
2153 .map_err(|error| Status::invalid_argument(error.to_string()))
2154}
2155
2156fn work_counters(work: crate::WorkCounters) -> wire::WorkCounters {
2157 wire::WorkCounters {
2158 authority_records_read: work.authority_records_read,
2159 authority_records_appended: work.authority_records_appended,
2160 authority_bytes_read: work.authority_bytes_read,
2161 authority_bytes_written: work.authority_bytes_written,
2162 object_probes: work.object_probes,
2163 backend_read_operations: work.backend_read_operations,
2164 backend_write_operations: work.backend_write_operations,
2165 durability_operations: work.durability_operations,
2166 page_reads: work.page_reads,
2167 page_writes: work.page_writes,
2168 object_bytes_read: work.object_bytes_read,
2169 object_bytes_written: work.object_bytes_written,
2170 bytes_hashed: work.bytes_hashed,
2171 bytes_copied: work.bytes_copied,
2172 bytes_encoded: work.bytes_encoded,
2173 source_bytes_read: work.source_bytes_read,
2174 output_bytes: work.output_bytes,
2175 items_examined: work.items_examined,
2176 items_returned: work.items_returned,
2177 allocation_operations: work.allocation_operations,
2178 peak_allocation_bytes: work.peak_allocation_bytes,
2179 materializations: work.materializations,
2180 }
2181}
2182
2183fn join_plan_id(
2184 source: &wire::GenerationRef,
2185 target: &wire::GenerationRef,
2186 base: &wire::GenerationRef,
2187 history: JoinHistory,
2188 maximum_generations: u32,
2189 maximum_changes: u32,
2190 maximum_conflicts: u32,
2191) -> [u8; 32] {
2192 let mut hasher = blake3::Hasher::new();
2193 hasher.update(b"acyclic-fs-wire-join-plan-v2\0");
2194 for reference in [source, target, base] {
2195 if let Some(workspace) = &reference.workspace {
2196 hasher.update(&(workspace.workspace_id.len() as u64).to_le_bytes());
2197 hasher.update(&workspace.workspace_id);
2198 hasher.update(&(workspace.name.len() as u64).to_le_bytes());
2199 hasher.update(workspace.name.as_bytes());
2200 } else {
2201 hasher.update(&0_u64.to_le_bytes());
2202 hasher.update(&0_u64.to_le_bytes());
2203 }
2204 hasher.update(&(reference.generation_id.len() as u64).to_le_bytes());
2205 hasher.update(&reference.generation_id);
2206 }
2207 hasher.update(&[join_history_code(history)]);
2208 hasher.update(&maximum_generations.to_le_bytes());
2209 hasher.update(&maximum_changes.to_le_bytes());
2210 hasher.update(&maximum_conflicts.to_le_bytes());
2211 *hasher.finalize().as_bytes()
2212}
2213
2214fn join_history(value: i32) -> Result<JoinHistory, Status> {
2215 match wire::JoinHistory::try_from(value)
2216 .map_err(|_| Status::invalid_argument("join history is invalid"))?
2217 {
2218 wire::JoinHistory::Merge => Ok(JoinHistory::Merge),
2219 wire::JoinHistory::Rebase => Ok(JoinHistory::Rebase),
2220 wire::JoinHistory::Squash => Ok(JoinHistory::Squash),
2221 wire::JoinHistory::CherryPick => Ok(JoinHistory::CherryPick),
2222 wire::JoinHistory::Unspecified => {
2223 Err(Status::invalid_argument("join history must be specified"))
2224 }
2225 }
2226}
2227
2228const fn join_history_code(value: JoinHistory) -> u8 {
2229 match value {
2230 JoinHistory::Merge => 1,
2231 JoinHistory::Rebase => 2,
2232 JoinHistory::Squash => 3,
2233 JoinHistory::CherryPick => 4,
2234 }
2235}
2236
2237fn encode_object_id(object: ObjectId) -> Vec<u8> {
2238 let mut bytes = Vec::with_capacity(33);
2239 bytes.push(object.kind.canonical_tag());
2240 bytes.extend_from_slice(object.digest.as_bytes());
2241 bytes
2242}
2243
2244fn decode_object_id(bytes: &[u8]) -> Result<ObjectId, Status> {
2245 if bytes.len() != 33 {
2246 return Err(Status::invalid_argument(
2247 "object identity must contain kind and digest",
2248 ));
2249 }
2250 let (&tag, digest_bytes) = bytes
2251 .split_first()
2252 .ok_or_else(|| Status::invalid_argument("object identity must contain kind and digest"))?;
2253 let kind = ObjectKind::from_canonical_tag(tag)
2254 .map_err(|error| Status::invalid_argument(error.to_string()))?;
2255 let digest: [u8; 32] = digest_bytes
2256 .try_into()
2257 .map_err(|_| Status::invalid_argument("object digest is malformed"))?;
2258 Ok(ObjectId {
2259 kind,
2260 digest: Digest::from_bytes(digest),
2261 })
2262}
2263
2264fn transfer_cursor(bytes: &[u8]) -> Result<u64, Status> {
2265 if bytes.is_empty() {
2266 return Ok(0);
2267 }
2268 let cursor: [u8; 8] = bytes
2269 .try_into()
2270 .map_err(|_| Status::invalid_argument("transfer cursor must contain 8 bytes"))?;
2271 Ok(u64::from_le_bytes(cursor))
2272}
2273
2274fn mutation(
2275 status: wire::MutationStatus,
2276 generation: Option<wire::GenerationRef>,
2277 actual_head: Option<wire::GenerationRef>,
2278) -> wire::MutationResponse {
2279 wire::MutationResponse {
2280 status: status as i32,
2281 generation,
2282 actual_head,
2283 conflicts: Vec::new(),
2284 truncated: false,
2285 }
2286}
2287
2288fn mutation_with_conflicts(
2289 status: wire::MutationStatus,
2290 conflicts: Vec<wire::Conflict>,
2291 truncated: bool,
2292) -> wire::MutationResponse {
2293 wire::MutationResponse {
2294 status: status as i32,
2295 generation: None,
2296 actual_head: None,
2297 conflicts,
2298 truncated,
2299 }
2300}
2301
2302fn transaction_conflict(value: TransactionConflict) -> wire::Conflict {
2303 use wire::conflict::Region;
2304 let region = match value.region {
2305 TransactionConflictRegion::FileRecord(file_id) => Region::FileRecord(wire::FileConflict {
2306 file_id: file_id.into_bytes().to_vec(),
2307 }),
2308 TransactionConflictRegion::Metadata(file_id) => Region::Metadata(wire::FileConflict {
2309 file_id: file_id.into_bytes().to_vec(),
2310 }),
2311 TransactionConflictRegion::FileLength(file_id) => Region::FileLength(wire::FileConflict {
2312 file_id: file_id.into_bytes().to_vec(),
2313 }),
2314 TransactionConflictRegion::ContentRange {
2315 file_id,
2316 offset,
2317 length,
2318 } => Region::ContentRange(wire::ContentConflict {
2319 file_id: file_id.into_bytes().to_vec(),
2320 range: Some(wire::ByteRange { offset, length }),
2321 }),
2322 TransactionConflictRegion::SparseSeek {
2323 file_id,
2324 offset,
2325 target,
2326 } => Region::SparseSeek(wire::SparseConflict {
2327 file_id: file_id.into_bytes().to_vec(),
2328 offset,
2329 target: match target {
2330 TransactionSparseSeek::Data => wire::SparseTarget::Data as i32,
2331 TransactionSparseSeek::Hole => wire::SparseTarget::Hole as i32,
2332 },
2333 }),
2334 TransactionConflictRegion::DirectoryName { directory_id, name } => {
2335 Region::DirectoryName(wire::DirectoryNameConflict {
2336 directory_id: directory_id.into_bytes().to_vec(),
2337 name: Some(logical_name(&name)),
2338 })
2339 }
2340 TransactionConflictRegion::DirectoryRange {
2341 directory_id,
2342 after,
2343 maximum_entries,
2344 } => Region::DirectoryRange(wire::DirectoryRangeConflict {
2345 directory_id: directory_id.into_bytes().to_vec(),
2346 after: after.as_ref().map(logical_name),
2347 maximum_entries,
2348 }),
2349 };
2350 wire::Conflict {
2351 region: Some(region),
2352 r#use: match value.usage {
2353 TransactionDependencyUse::Observation => wire::ConflictUse::Observation as i32,
2354 TransactionDependencyUse::Mutation => wire::ConflictUse::Mutation as i32,
2355 TransactionDependencyUse::ObservationAndMutation => {
2356 wire::ConflictUse::ObservationAndMutation as i32
2357 }
2358 },
2359 expected_digest: value
2360 .expected
2361 .map_or_else(Vec::new, |value| value.as_bytes().to_vec()),
2362 actual_digest: value
2363 .actual
2364 .map_or_else(Vec::new, |value| value.as_bytes().to_vec()),
2365 }
2366}
2367
2368fn merge_conflict(value: MergeConflict) -> wire::Conflict {
2369 let region = match value {
2370 MergeConflict::File(file_id) => wire::conflict::Region::FileRecord(wire::FileConflict {
2371 file_id: file_id.into_bytes().to_vec(),
2372 }),
2373 MergeConflict::Binding { directory_id, name } => {
2374 wire::conflict::Region::DirectoryName(wire::DirectoryNameConflict {
2375 directory_id: directory_id.into_bytes().to_vec(),
2376 name: Some(logical_name(&name)),
2377 })
2378 }
2379 };
2380 wire::Conflict {
2381 region: Some(region),
2382 r#use: wire::ConflictUse::Unspecified as i32,
2383 expected_digest: Vec::new(),
2384 actual_digest: Vec::new(),
2385 }
2386}
2387
2388fn rebase_message<A, O>(value: WorkspaceRebase<A, O>) -> wire::RebaseResponse {
2389 match value {
2390 WorkspaceRebase::Rebased(generation) => wire::RebaseResponse {
2391 status: wire::RebaseStatus::Rebased as i32,
2392 generation: Some(generation_ref(&generation)),
2393 conflicts: Vec::new(),
2394 truncated: false,
2395 },
2396 WorkspaceRebase::AlreadyRebased(generation) => wire::RebaseResponse {
2397 status: wire::RebaseStatus::AlreadyRebased as i32,
2398 generation: Some(generation_ref(&generation)),
2399 conflicts: Vec::new(),
2400 truncated: false,
2401 },
2402 WorkspaceRebase::Current(generation) => wire::RebaseResponse {
2403 status: wire::RebaseStatus::Current as i32,
2404 generation: Some(generation_ref(&generation)),
2405 conflicts: Vec::new(),
2406 truncated: false,
2407 },
2408 WorkspaceRebase::Stale(generation) => wire::RebaseResponse {
2409 status: wire::RebaseStatus::Stale as i32,
2410 generation: Some(generation_ref(&generation)),
2411 conflicts: Vec::new(),
2412 truncated: false,
2413 },
2414 WorkspaceRebase::Conflicted {
2415 conflicts,
2416 truncated,
2417 } => wire::RebaseResponse {
2418 status: wire::RebaseStatus::Conflicted as i32,
2419 generation: None,
2420 conflicts: conflicts.into_iter().map(merge_conflict).collect(),
2421 truncated,
2422 },
2423 WorkspaceRebase::Fenced => wire::RebaseResponse {
2424 status: wire::RebaseStatus::Fenced as i32,
2425 generation: None,
2426 conflicts: Vec::new(),
2427 truncated: false,
2428 },
2429 WorkspaceRebase::IdempotencyConflict => wire::RebaseResponse {
2430 status: wire::RebaseStatus::IdempotencyConflict as i32,
2431 generation: None,
2432 conflicts: Vec::new(),
2433 truncated: false,
2434 },
2435 }
2436}
2437
2438fn stat_message(value: &crate::WorkspaceStat) -> wire::FileStat {
2439 wire::FileStat {
2440 file_id: value.file_id.into_bytes().to_vec(),
2441 kind: file_kind(value.kind),
2442 link_count: value.link_count,
2443 logical_bytes: Some(output_u64(value.logical_bytes)),
2444 metadata: Some(metadata_message(value.metadata)),
2445 }
2446}
2447
2448fn metadata_message(value: WorkspaceMetadata) -> wire::Metadata {
2449 wire::Metadata {
2450 posix_mode: Some(output_u32(value.posix_mode)),
2451 posix_uid: Some(output_u32(value.posix_uid)),
2452 posix_gid: Some(output_u32(value.posix_gid)),
2453 posix_flags: Some(output_u64(value.posix_flags)),
2454 windows_attributes: Some(output_u32(value.windows_attributes)),
2455 created_ns: Some(output_i64(value.created_ns)),
2456 modified_ns: Some(output_i64(value.modified_ns)),
2457 accessed_ns: Some(output_i64(value.accessed_ns)),
2458 changed_ns: Some(output_i64(value.changed_ns)),
2459 has_named_attributes: value.has_named_attributes,
2460 has_acl: value.has_acl,
2461 has_security_descriptor: value.has_security_descriptor,
2462 }
2463}
2464
2465fn output_u32(value: Option<u32>) -> wire::OptionalU32 {
2466 wire::OptionalU32 {
2467 value: Some(value.map_or(
2468 wire::optional_u32::Value::Unavailable(true),
2469 wire::optional_u32::Value::Present,
2470 )),
2471 }
2472}
2473
2474fn output_u64(value: Option<u64>) -> wire::OptionalU64 {
2475 wire::OptionalU64 {
2476 value: Some(value.map_or(
2477 wire::optional_u64::Value::Unavailable(true),
2478 wire::optional_u64::Value::Present,
2479 )),
2480 }
2481}
2482
2483fn output_i64(value: Option<i64>) -> wire::OptionalI64 {
2484 wire::OptionalI64 {
2485 value: Some(value.map_or(
2486 wire::optional_i64::Value::Unavailable(true),
2487 wire::optional_i64::Value::Present,
2488 )),
2489 }
2490}
2491
2492fn file_kind(value: EngineFileKind) -> i32 {
2493 (match value {
2494 EngineFileKind::Regular => wire::FileKind::Regular,
2495 EngineFileKind::Directory => wire::FileKind::Directory,
2496 EngineFileKind::SymbolicLink => wire::FileKind::SymbolicLink,
2497 EngineFileKind::Fifo => wire::FileKind::Fifo,
2498 EngineFileKind::Socket => wire::FileKind::Socket,
2499 EngineFileKind::CharacterDevice => wire::FileKind::CharacterDevice,
2500 EngineFileKind::BlockDevice => wire::FileKind::BlockDevice,
2501 EngineFileKind::ReparsePoint => wire::FileKind::ReparsePoint,
2502 EngineFileKind::MountBoundary => wire::FileKind::MountBoundary,
2503 }) as i32
2504}
2505
2506fn required<T>(value: Option<T>, name: &'static str) -> Result<T, Status> {
2507 value.ok_or_else(|| Status::invalid_argument(format!("{name} is required")))
2508}
2509
2510fn status(error: &WorkspaceError) -> Status {
2511 match error {
2512 WorkspaceError::NotFound => Status::not_found(error.to_string()),
2513 WorkspaceError::Name(_)
2514 | WorkspaceError::Path(_)
2515 | WorkspaceError::ReadLimitExceeded
2516 | WorkspaceError::InvalidMergeResolution
2517 | WorkspaceError::NotRegularFile
2518 | WorkspaceError::NotDirectory
2519 | WorkspaceError::EmptyContentSet
2520 | WorkspaceError::ContentLengthOverflow => Status::invalid_argument(error.to_string()),
2521 WorkspaceError::ForeignGeneration
2522 | WorkspaceError::StaleGeneration
2523 | WorkspaceError::StaleIdentity
2524 | WorkspaceError::IncompatibleWorkspace
2525 | WorkspaceError::ChangeSetContinuity
2526 | WorkspaceError::RetentionConflict
2527 | WorkspaceError::NoCommonAncestor
2528 | WorkspaceError::LineageLimit
2529 | WorkspaceError::JoinLimit
2530 | WorkspaceError::ChangedPathLimit
2531 | WorkspaceError::NotFork => Status::failed_precondition(error.to_string()),
2532 WorkspaceError::Cancelled(_) => Status::cancelled(error.to_string()),
2533 WorkspaceError::Work(_) => Status::resource_exhausted(error.to_string()),
2534 WorkspaceError::Engine(_) => Status::unavailable(error.to_string()),
2535 }
2536}
2537
2538#[cfg(test)]
2539mod tests {
2540 use super::*;
2541 use crate::wire::filesystem::v2::filesystem_service_server::FilesystemService as _;
2542
2543 struct TestIssuer;
2544
2545 struct TestSourceProvider {
2546 generation: std::sync::Mutex<Option<GenerationId>>,
2547 }
2548
2549 #[tonic::async_trait]
2550 impl FilesystemSourceProvider for TestSourceProvider {
2551 async fn state(&self, _scope: HostedSourceScope) -> Result<HostedSourceResult, Status> {
2552 Ok(HostedSourceResult {
2553 state: HostedSourceState::NeedsRescan(HostedSourceInvalidation::QueueOverflow),
2554 generation_id: None,
2555 })
2556 }
2557
2558 async fn reconcile(
2559 &self,
2560 operation: HostedSourceOperation,
2561 ) -> Result<HostedSourceResult, Status> {
2562 if operation.idempotency_key != IdempotencyKey::from_bytes([9; 16]) {
2563 return Err(Status::invalid_argument(
2564 "unexpected reconciliation identity",
2565 ));
2566 }
2567 Ok(HostedSourceResult {
2568 state: HostedSourceState::Clean,
2569 generation_id: *self
2570 .generation
2571 .lock()
2572 .map_err(|_| Status::internal("lock"))?,
2573 })
2574 }
2575
2576 async fn rescan(
2577 &self,
2578 _operation: HostedSourceOperation,
2579 ) -> Result<HostedSourceResult, Status> {
2580 Ok(HostedSourceResult {
2581 state: HostedSourceState::Clean,
2582 generation_id: *self
2583 .generation
2584 .lock()
2585 .map_err(|_| Status::internal("lock"))?,
2586 })
2587 }
2588
2589 async fn seal(
2590 &self,
2591 _operation: HostedSourceOperation,
2592 ) -> Result<HostedSourceResult, Status> {
2593 Ok(HostedSourceResult {
2594 state: HostedSourceState::Sealed,
2595 generation_id: *self
2596 .generation
2597 .lock()
2598 .map_err(|_| Status::internal("lock"))?,
2599 })
2600 }
2601 }
2602
2603 #[tonic::async_trait]
2604 impl FilesystemCredentialIssuer for TestIssuer {
2605 fn mount_enabled(&self) -> bool {
2606 true
2607 }
2608
2609 fn s3_enabled(&self) -> bool {
2610 true
2611 }
2612
2613 async fn issue(&self, request: CredentialGrantRequest) -> Result<CredentialGrant, Status> {
2614 Ok(match request.kind {
2615 CredentialKind::Mount => CredentialGrant::Bearer {
2616 endpoint: "mount://test".to_owned(),
2617 token: hex::encode(request.workspace_id.into_bytes()),
2618 expires_at_unix_seconds: 1_900_000_000,
2619 },
2620 CredentialKind::S3 => CredentialGrant::S3 {
2621 endpoint: "https://s3.test".to_owned(),
2622 bucket: "opaque-bucket".to_owned(),
2623 region: "auto".to_owned(),
2624 access_key_id: "access".to_owned(),
2625 secret_access_key: "secret".to_owned(),
2626 session_token: String::new(),
2627 expires_at_unix_seconds: 1_900_000_000,
2628 },
2629 })
2630 }
2631 }
2632
2633 fn operation(value: u8) -> Option<wire::OperationOptions> {
2634 Some(wire::OperationOptions {
2635 idempotency_key: operation_bytes(value),
2636 })
2637 }
2638
2639 fn operation_bytes(value: u8) -> Vec<u8> {
2640 let mut id = [0_u8; 16];
2641 id[15] = value;
2642 id.to_vec()
2643 }
2644
2645 fn mutation(value: wire::mutation::Mutation) -> wire::Mutation {
2646 wire::Mutation {
2647 mutation: Some(value),
2648 }
2649 }
2650
2651 #[tokio::test]
2652 async fn handshake_and_creation_expose_every_canonical_profile()
2653 -> Result<(), Box<dyn std::error::Error>> {
2654 let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?;
2655 let advertised = service
2656 .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2657 .await?
2658 .into_inner()
2659 .capabilities
2660 .ok_or("missing capabilities")?;
2661 let profiles = [
2662 wire::FilesystemProfile::Portable,
2663 wire::FilesystemProfile::Posix,
2664 wire::FilesystemProfile::Windows,
2665 wire::FilesystemProfile::Browser,
2666 ];
2667 assert_eq!(advertised.profiles, profiles.map(|profile| profile as i32));
2668 for (index, selected) in profiles.into_iter().enumerate() {
2669 let created = service
2670 .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2671 name: format!("profile-{index}"),
2672 profile: selected as i32,
2673 operation: operation(u8::try_from(index + 1)?),
2674 }))
2675 .await?
2676 .into_inner()
2677 .workspace
2678 .ok_or("missing workspace")?;
2679 assert_eq!(created.profile, selected as i32);
2680 let observed = service
2681 .observe(Request::new(wire::ObserveRequest {
2682 workspace: created.workspace.clone(),
2683 operation_id: operation_bytes(u8::try_from(index + 1)?),
2684 }))
2685 .await?
2686 .into_inner();
2687 assert_eq!(observed.state, "completed");
2688 assert_eq!(
2689 observed.outcome.and_then(|outcome| outcome.generation),
2690 created.head
2691 );
2692 }
2693 Ok(())
2694 }
2695
2696 #[tokio::test]
2697 async fn source_provider_controls_capability_and_all_lifecycle_rpcs()
2698 -> Result<(), Box<dyn std::error::Error>> {
2699 let absent = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?;
2700 let capabilities = absent
2701 .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2702 .await?
2703 .into_inner()
2704 .capabilities
2705 .ok_or("missing capabilities")?;
2706 assert!(!capabilities.source_reconciliation);
2707
2708 let provider = Arc::new(TestSourceProvider {
2709 generation: std::sync::Mutex::new(None),
2710 });
2711 let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?
2712 .with_source_provider(provider.clone());
2713 let capabilities = service
2714 .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2715 .await?
2716 .into_inner()
2717 .capabilities
2718 .ok_or("missing capabilities")?;
2719 assert!(capabilities.source_reconciliation);
2720 let created = service
2721 .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2722 name: "source".to_owned(),
2723 profile: wire::FilesystemProfile::Portable as i32,
2724 operation: operation(1),
2725 }))
2726 .await?
2727 .into_inner()
2728 .workspace
2729 .ok_or("missing workspace")?;
2730 let workspace = created.workspace.ok_or("missing workspace reference")?;
2731 let head = created.head.ok_or("missing head")?;
2732 *provider
2733 .generation
2734 .lock()
2735 .map_err(|_| "provider lock poisoned")? = Some(generation_id(&head.generation_id)?);
2736
2737 let state = service
2738 .get_source_state(Request::new(wire::SourceStateRequest {
2739 workspace: Some(workspace.clone()),
2740 }))
2741 .await?
2742 .into_inner();
2743 assert_eq!(state.state, wire::SourceState::NeedsRescan as i32);
2744 assert_eq!(
2745 state.reason,
2746 wire::SourceInvalidationReason::QueueOverflow as i32
2747 );
2748 assert!(state.generation.is_none());
2749
2750 let reconciled = service
2751 .reconcile_source(Request::new(wire::SourceOperationRequest {
2752 workspace: Some(workspace.clone()),
2753 operation: Some(wire::OperationOptions {
2754 idempotency_key: vec![9; 16],
2755 }),
2756 }))
2757 .await?
2758 .into_inner();
2759 assert_eq!(reconciled.state, wire::SourceState::Clean as i32);
2760 assert_eq!(reconciled.generation.as_ref(), Some(&head));
2761
2762 for response in [
2763 service
2764 .rescan_source(Request::new(wire::SourceOperationRequest {
2765 workspace: Some(workspace.clone()),
2766 operation: operation(10),
2767 }))
2768 .await?
2769 .into_inner(),
2770 service
2771 .seal_source(Request::new(wire::SourceOperationRequest {
2772 workspace: Some(workspace),
2773 operation: operation(11),
2774 }))
2775 .await?
2776 .into_inner(),
2777 ] {
2778 assert_eq!(response.generation.as_ref(), Some(&head));
2779 }
2780 Ok(())
2781 }
2782
2783 #[tokio::test]
2784 async fn wire_adapter_uses_one_engine_for_bounded_customer_semantics()
2785 -> Result<(), Box<dyn std::error::Error>> {
2786 let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?
2787 .with_credential_issuer(Arc::new(TestIssuer));
2788 let created = service
2789 .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2790 name: "main".to_owned(),
2791 profile: wire::FilesystemProfile::Portable as i32,
2792 operation: operation(1),
2793 }))
2794 .await?
2795 .into_inner()
2796 .workspace
2797 .ok_or("missing workspace")?;
2798 let initial = created.head.ok_or("missing head")?;
2799 let workspace = created.workspace.ok_or("missing workspace ref")?;
2800
2801 let credential = service
2802 .issue_mount_credential(Request::new(wire::CredentialRequest {
2803 workspace: Some(workspace.clone()),
2804 generation: Some(initial.clone()),
2805 writable: false,
2806 expires_after_seconds: 60,
2807 operation: operation(8),
2808 }))
2809 .await?
2810 .into_inner();
2811 assert_eq!(credential.endpoint, "mount://test");
2812 assert!(matches!(
2813 credential.credential,
2814 Some(wire::credential_response::Credential::BearerToken(token)) if !token.is_empty()
2815 ));
2816
2817 let committed = service
2818 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
2819 base: Some(initial),
2820 mutations: vec![
2821 mutation(wire::mutation::Mutation::CreateDirectory(
2822 wire::CreateDirectory {
2823 path: "/dir".to_owned(),
2824 metadata: None,
2825 },
2826 )),
2827 mutation(wire::mutation::Mutation::CreateFile(wire::CreateFile {
2828 path: "/dir/value".to_owned(),
2829 contents: b"value".to_vec(),
2830 metadata: None,
2831 })),
2832 ],
2833 operation: operation(2),
2834 maximum_conflicts: 32,
2835 }))
2836 .await?
2837 .into_inner();
2838 assert_eq!(committed.status, wire::MutationStatus::Committed as i32);
2839 let generation = committed.generation.ok_or("missing generation")?;
2840 let observed = service
2841 .observe(Request::new(wire::ObserveRequest {
2842 workspace: Some(workspace.clone()),
2843 operation_id: operation_bytes(2),
2844 }))
2845 .await?
2846 .into_inner();
2847 assert_eq!(observed.state, "completed");
2848 assert_eq!(
2849 observed
2850 .outcome
2851 .as_ref()
2852 .and_then(|outcome| outcome.generation.as_ref()),
2853 Some(&generation)
2854 );
2855 let unknown = service
2856 .cancel(Request::new(wire::CancelRequest {
2857 workspace: Some(workspace.clone()),
2858 operation_id: operation_bytes(99),
2859 }))
2860 .await?
2861 .into_inner()
2862 .operation
2863 .ok_or("missing cancellation observation")?;
2864 assert_eq!(unknown.state, "unknown");
2865 assert!(unknown.outcome.is_none());
2866 let read = service
2867 .read(Request::new(wire::ReadRequest {
2868 generation: Some(generation.clone()),
2869 path: "/dir/value".to_owned(),
2870 range: None,
2871 maximum_bytes: 5,
2872 }))
2873 .await?
2874 .into_inner();
2875 assert_eq!(read.contents, b"value");
2876
2877 let listed = service
2878 .list_directory(Request::new(wire::ListDirectoryRequest {
2879 generation: Some(generation.clone()),
2880 path: "/dir".to_owned(),
2881 page: Some(wire::PageOptions {
2882 maximum_items: 8,
2883 after: None,
2884 }),
2885 }))
2886 .await?
2887 .into_inner()
2888 .page
2889 .ok_or("missing directory page")?;
2890 assert_eq!(listed.entries.len(), 1);
2891 assert_eq!(
2892 listed.entries[0]
2893 .name
2894 .as_ref()
2895 .map(|name| name.bytes.as_slice()),
2896 Some(b"value".as_slice())
2897 );
2898
2899 let pinned = service
2900 .pin(Request::new(wire::RetainGenerationRequest {
2901 generation: Some(generation.clone()),
2902 identity: "release".to_owned(),
2903 operation: operation(3),
2904 }))
2905 .await?
2906 .into_inner();
2907 assert_eq!(pinned.status, wire::MutationStatus::Committed as i32);
2908
2909 let forked = service
2910 .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
2911 source: Some(generation),
2912 destination_name: "agent".to_owned(),
2913 operation: operation(4),
2914 }))
2915 .await?
2916 .into_inner()
2917 .workspace
2918 .ok_or("missing fork")?;
2919 assert_ne!(
2920 forked
2921 .workspace
2922 .as_ref()
2923 .ok_or("missing fork ref")?
2924 .workspace_id,
2925 workspace.workspace_id
2926 );
2927
2928 let main_head = service
2929 .get_head(Request::new(wire::GetHeadRequest {
2930 workspace: Some(workspace.clone()),
2931 }))
2932 .await?
2933 .into_inner()
2934 .generation
2935 .ok_or("missing main head")?;
2936 let fork_head = forked.head.clone().ok_or("missing fork head")?;
2937 let no_op_plan = service
2938 .plan_join(Request::new(wire::PlanJoinRequest {
2939 source: Some(main_head.clone()),
2940 target: Some(fork_head.clone()),
2941 maximum_changes: 32,
2942 maximum_conflicts: 8,
2943 maximum_generations: 32,
2944 history: wire::JoinHistory::Merge as i32,
2945 }))
2946 .await?
2947 .into_inner();
2948 let changed = service
2949 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
2950 base: Some(main_head),
2951 mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
2952 path: "/dir/value".to_owned(),
2953 offset: 0,
2954 contents: b"new!!".to_vec(),
2955 }))],
2956 operation: operation(5),
2957 maximum_conflicts: 32,
2958 }))
2959 .await?
2960 .into_inner()
2961 .generation
2962 .ok_or("missing changed generation")?;
2963 let plan = service
2964 .plan_join(Request::new(wire::PlanJoinRequest {
2965 source: Some(changed.clone()),
2966 target: Some(fork_head),
2967 maximum_changes: 32,
2968 maximum_conflicts: 8,
2969 maximum_generations: 32,
2970 history: wire::JoinHistory::Merge as i32,
2971 }))
2972 .await?
2973 .into_inner();
2974 let fresh_no_op = service
2975 .apply_join(Request::new(wire::ApplyJoinRequest {
2976 plan: Some(no_op_plan.clone()),
2977 operation: operation(12),
2978 }))
2979 .await?
2980 .into_inner();
2981 assert_eq!(fresh_no_op.status, wire::JoinStatus::NoChanges as i32);
2982 let joined = service
2983 .apply_join(Request::new(wire::ApplyJoinRequest {
2984 plan: Some(plan.clone()),
2985 operation: operation(6),
2986 }))
2987 .await?
2988 .into_inner();
2989 assert_eq!(joined.status, wire::JoinStatus::Applied as i32);
2990 let joined_generation = joined.generation.ok_or("missing joined generation")?;
2991 let no_op_before_advance = service
2992 .apply_join(Request::new(wire::ApplyJoinRequest {
2993 plan: Some(no_op_plan.clone()),
2994 operation: operation(6),
2995 }))
2996 .await?
2997 .into_inner();
2998 assert_eq!(
2999 no_op_before_advance.status,
3000 wire::JoinStatus::IdempotencyConflict as i32
3001 );
3002 let advanced_target = service
3003 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3004 base: Some(joined_generation.clone()),
3005 mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3006 wire::CreateFile {
3007 path: "/target-only".to_owned(),
3008 contents: b"target".to_vec(),
3009 metadata: None,
3010 },
3011 ))],
3012 operation: operation(11),
3013 maximum_conflicts: 32,
3014 }))
3015 .await?
3016 .into_inner()
3017 .generation
3018 .ok_or("missing advanced target generation")?;
3019 let repeated = service
3020 .apply_join(Request::new(wire::ApplyJoinRequest {
3021 plan: Some(plan.clone()),
3022 operation: operation(6),
3023 }))
3024 .await?
3025 .into_inner();
3026 assert_eq!(repeated.status, wire::JoinStatus::AlreadyApplied as i32);
3027 assert_eq!(repeated.generation.as_ref(), Some(&joined_generation));
3028 let no_op_after_advance = service
3029 .apply_join(Request::new(wire::ApplyJoinRequest {
3030 plan: Some(no_op_plan),
3031 operation: operation(6),
3032 }))
3033 .await?
3034 .into_inner();
3035 assert_eq!(
3036 no_op_after_advance.status,
3037 wire::JoinStatus::IdempotencyConflict as i32
3038 );
3039 let mut altered_bounds = plan.clone();
3040 altered_bounds.maximum_generations += 1;
3041 altered_bounds.plan_id = join_plan_id(
3042 altered_bounds.source.as_ref().ok_or("missing source")?,
3043 altered_bounds
3044 .expected_target
3045 .as_ref()
3046 .ok_or("missing target")?,
3047 altered_bounds
3048 .common_ancestor
3049 .as_ref()
3050 .ok_or("missing common ancestor")?,
3051 join_history(altered_bounds.history)?,
3052 altered_bounds.maximum_generations,
3053 altered_bounds.maximum_changes,
3054 altered_bounds.maximum_conflicts,
3055 )
3056 .to_vec();
3057 let altered_retry = service
3058 .apply_join(Request::new(wire::ApplyJoinRequest {
3059 plan: Some(altered_bounds),
3060 operation: operation(6),
3061 }))
3062 .await?
3063 .into_inner();
3064 assert_eq!(
3065 altered_retry.status,
3066 wire::JoinStatus::IdempotencyConflict as i32
3067 );
3068 let stale = service
3069 .apply_join(Request::new(wire::ApplyJoinRequest {
3070 plan: Some(plan),
3071 operation: operation(9),
3072 }))
3073 .await?
3074 .into_inner();
3075 assert_eq!(stale.status, wire::JoinStatus::StaleTarget as i32);
3076 assert_eq!(stale.generation.as_ref(), Some(&advanced_target));
3077
3078 let further_changed = service
3079 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3080 base: Some(changed.clone()),
3081 mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3082 wire::CreateFile {
3083 path: "/dir/next".to_owned(),
3084 contents: b"next".to_vec(),
3085 metadata: None,
3086 },
3087 ))],
3088 operation: operation(10),
3089 maximum_conflicts: 32,
3090 }))
3091 .await?
3092 .into_inner()
3093 .generation
3094 .ok_or("missing further generation")?;
3095 let different_plan = service
3096 .plan_join(Request::new(wire::PlanJoinRequest {
3097 source: Some(further_changed),
3098 target: Some(advanced_target),
3099 maximum_changes: 32,
3100 maximum_conflicts: 8,
3101 maximum_generations: 32,
3102 history: wire::JoinHistory::Merge as i32,
3103 }))
3104 .await?
3105 .into_inner();
3106 let conflicting_retry = service
3107 .apply_join(Request::new(wire::ApplyJoinRequest {
3108 plan: Some(different_plan),
3109 operation: operation(6),
3110 }))
3111 .await?
3112 .into_inner();
3113 assert_eq!(
3114 conflicting_retry.status,
3115 wire::JoinStatus::IdempotencyConflict as i32
3116 );
3117
3118 let conflict_target = service
3119 .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3120 source: Some(changed.clone()),
3121 destination_name: "conflict-target".to_owned(),
3122 operation: operation(20),
3123 }))
3124 .await?
3125 .into_inner()
3126 .workspace
3127 .ok_or("missing conflict target")?;
3128 let conflict_source = service
3129 .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3130 source: Some(changed.clone()),
3131 destination_name: "conflict-source".to_owned(),
3132 operation: operation(21),
3133 }))
3134 .await?
3135 .into_inner()
3136 .workspace
3137 .ok_or("missing conflict source")?;
3138 let benign_source = service
3139 .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3140 source: Some(changed.clone()),
3141 destination_name: "benign-source".to_owned(),
3142 operation: operation(22),
3143 }))
3144 .await?
3145 .into_inner()
3146 .workspace
3147 .ok_or("missing benign source")?;
3148
3149 let conflict_target_head = service
3150 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3151 base: conflict_target.head,
3152 mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
3153 path: "/dir/value".to_owned(),
3154 offset: 0,
3155 contents: b"target".to_vec(),
3156 }))],
3157 operation: operation(23),
3158 maximum_conflicts: 32,
3159 }))
3160 .await?
3161 .into_inner()
3162 .generation
3163 .ok_or("missing conflict target generation")?;
3164 let conflict_source_head = service
3165 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3166 base: conflict_source.head,
3167 mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
3168 path: "/dir/value".to_owned(),
3169 offset: 0,
3170 contents: b"source".to_vec(),
3171 }))],
3172 operation: operation(24),
3173 maximum_conflicts: 32,
3174 }))
3175 .await?
3176 .into_inner()
3177 .generation
3178 .ok_or("missing conflict source generation")?;
3179 let conflict_plan = service
3180 .plan_join(Request::new(wire::PlanJoinRequest {
3181 source: Some(conflict_source_head),
3182 target: Some(conflict_target_head.clone()),
3183 maximum_changes: 32,
3184 maximum_conflicts: 8,
3185 maximum_generations: 32,
3186 history: wire::JoinHistory::Merge as i32,
3187 }))
3188 .await?
3189 .into_inner();
3190 let fresh_conflict = service
3191 .apply_join(Request::new(wire::ApplyJoinRequest {
3192 plan: Some(conflict_plan.clone()),
3193 operation: operation(25),
3194 }))
3195 .await?
3196 .into_inner();
3197 assert_eq!(fresh_conflict.status, wire::JoinStatus::Conflicted as i32);
3198
3199 let benign_source_head = service
3200 .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3201 base: benign_source.head,
3202 mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3203 wire::CreateFile {
3204 path: "/benign".to_owned(),
3205 contents: b"benign".to_vec(),
3206 metadata: None,
3207 },
3208 ))],
3209 operation: operation(26),
3210 maximum_conflicts: 32,
3211 }))
3212 .await?
3213 .into_inner()
3214 .generation
3215 .ok_or("missing benign source generation")?;
3216 let benign_plan = service
3217 .plan_join(Request::new(wire::PlanJoinRequest {
3218 source: Some(benign_source_head),
3219 target: Some(conflict_target_head),
3220 maximum_changes: 32,
3221 maximum_conflicts: 8,
3222 maximum_generations: 32,
3223 history: wire::JoinHistory::Merge as i32,
3224 }))
3225 .await?
3226 .into_inner();
3227 let benign_join = service
3228 .apply_join(Request::new(wire::ApplyJoinRequest {
3229 plan: Some(benign_plan),
3230 operation: operation(27),
3231 }))
3232 .await?
3233 .into_inner();
3234 assert_eq!(benign_join.status, wire::JoinStatus::Applied as i32);
3235 let bound_conflict = service
3236 .apply_join(Request::new(wire::ApplyJoinRequest {
3237 plan: Some(conflict_plan),
3238 operation: operation(27),
3239 }))
3240 .await?
3241 .into_inner();
3242 assert_eq!(
3243 bound_conflict.status,
3244 wire::JoinStatus::IdempotencyConflict as i32
3245 );
3246
3247 let mut exported = service
3248 .export(Request::new(wire::ExportRequest {
3249 generation: Some(changed),
3250 after: Vec::new(),
3251 maximum_objects: 64,
3252 maximum_bytes: 1024 * 1024,
3253 }))
3254 .await?
3255 .into_inner();
3256 let mut export_chunks = Vec::new();
3257 while let Some(chunk) = exported.next().await {
3258 export_chunks.push(chunk?);
3259 }
3260 assert!(export_chunks.len() > 1);
3261 assert!(export_chunks[0].object_id.is_empty());
3262 assert!(
3263 export_chunks
3264 .last()
3265 .ok_or("missing export terminal")?
3266 .terminal
3267 );
3268
3269 let deleted = service
3270 .delete_workspace(Request::new(wire::DeleteWorkspaceRequest {
3271 workspace: Some(workspace),
3272 operation: operation(7),
3273 }))
3274 .await?
3275 .into_inner();
3276 assert_eq!(deleted.status, wire::MutationStatus::Committed as i32);
3277 Ok(())
3278 }
3279}