1use crate::cancellation::CancellationToken;
8use crate::kernel::{NamespacePath, NamespacePathError};
9use crate::performance::{OperationFailure, OperationReceipt, WorkCounters};
10use bytes::Bytes;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
16pub struct SourceReference {
17 pub identity: [u8; 16],
19 pub epoch: u64,
21}
22
23#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
25pub struct SourceVersion(pub [u8; 32]);
26
27#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
29pub enum SourceNodeKind {
30 Directory,
32 RegularFile,
34 SymbolicLink,
36 Fifo,
38 Socket,
40 CharacterDevice,
42 BlockDevice,
44 Unsupported,
46}
47
48#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
51pub struct SourceMetadata {
52 pub posix_mode: Option<u32>,
54 pub posix_uid: Option<u32>,
56 pub posix_gid: Option<u32>,
58 pub posix_flags: Option<u64>,
60 pub windows_attributes: Option<u32>,
62 pub created_ns: Option<i128>,
64 pub modified_ns: Option<i128>,
66 pub accessed_ns: Option<i128>,
68 pub changed_ns: Option<i128>,
70}
71
72#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct SourceNode {
75 pub kind: SourceNodeKind,
77 pub file_identity: [u8; 32],
79 pub link_count: Option<u64>,
81 pub device: Option<(u32, u32)>,
83 pub logical_bytes: Option<u64>,
85 pub version: SourceVersion,
87 pub metadata: SourceMetadata,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct SourceDirectoryEntry {
94 pub name: crate::kernel::LogicalName,
96 pub kind: SourceNodeKind,
98}
99
100#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct SourceCursor {
103 source: SourceReference,
104 directory: NamespacePath,
105 directory_version: SourceVersion,
106 token: u64,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct SourceDirectoryPage {
112 pub entries: Vec<SourceDirectoryEntry>,
114 pub next: Option<SourceCursor>,
116 pub version: SourceVersion,
118}
119
120#[derive(Debug, Error)]
122pub enum DemandError {
123 #[error("source reference is stale")]
125 StaleSource,
126 #[error("source node changed during the request")]
128 StaleVersion,
129 #[error("source directory cursor is expired or mismatched")]
131 StaleCursor,
132 #[error("source directory cursor is in use")]
134 CursorBusy,
135 #[error("source root is unavailable")]
137 SourceUnavailable,
138 #[error("source path is absent")]
140 Absent,
141 #[error("source node is not a regular file")]
143 NotRegularFile,
144 #[error("source node is not a directory")]
146 NotDirectory,
147 #[error("source request is invalid")]
149 InvalidRequest,
150 #[error("source request was cancelled")]
152 Cancelled,
153 #[error("native source worker failed")]
155 WorkerUnavailable,
156 #[error("source failed: {0}")]
158 Io(#[from] std::io::Error),
159 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
161 #[error(transparent)]
162 Observation(#[from] crate::watch::NativeWatchError),
163}
164
165impl From<NamespacePathError> for DemandError {
166 fn from(_: NamespacePathError) -> Self {
167 Self::InvalidRequest
168 }
169}
170
171pub type DemandResult<T> = Result<OperationReceipt<T>, OperationFailure<DemandError>>;
173
174#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
175fn measured<T>(
176 operation: impl FnOnce(&mut WorkCounters) -> Result<T, DemandError>,
177) -> DemandResult<T> {
178 let mut work = WorkCounters::default();
179 operation(&mut work)
180 .map(|value| OperationReceipt { value, work })
181 .map_err(|error| OperationFailure::new(error, work))
182}
183
184#[async_trait::async_trait]
189pub trait DemandSource: Send + Sync {
190 fn reference(&self) -> SourceReference;
192
193 async fn lookup(
195 &self,
196 source: SourceReference,
197 path: &NamespacePath,
198 cancellation: &CancellationToken,
199 ) -> DemandResult<Option<SourceNode>>;
200
201 async fn list_page(
203 &self,
204 source: SourceReference,
205 directory: &NamespacePath,
206 cursor: Option<SourceCursor>,
207 maximum_entries: u32,
208 cancellation: &CancellationToken,
209 ) -> DemandResult<SourceDirectoryPage>;
210
211 async fn read_range(
213 &self,
214 source: SourceReference,
215 path: &NamespacePath,
216 expected: SourceVersion,
217 offset: u64,
218 length: u64,
219 cancellation: &CancellationToken,
220 ) -> DemandResult<Bytes>;
221
222 async fn read_link(
224 &self,
225 source: SourceReference,
226 path: &NamespacePath,
227 expected: SourceVersion,
228 cancellation: &CancellationToken,
229 ) -> DemandResult<Bytes>;
230
231 async fn read_metadata(
233 &self,
234 source: SourceReference,
235 path: &NamespacePath,
236 expected: Option<SourceVersion>,
237 cancellation: &CancellationToken,
238 ) -> DemandResult<Option<SourceNode>> {
239 let answer = self.lookup(source, path, cancellation).await?;
240 if let Some(expected) = expected {
241 match answer.value {
242 Some(node) if node.version == expected => {}
243 _ => {
244 return Err(OperationFailure::new(
245 DemandError::StaleVersion,
246 answer.work,
247 ));
248 }
249 }
250 }
251 Ok(answer)
252 }
253}
254
255#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
263pub trait DemandDirectoryObserver: Send + Sync {
264 fn observe_directory(&self, directory: &NamespacePath) -> Result<(), DemandError>;
270}
271
272#[derive(Clone)]
276pub struct FilteredDemandSource<D> {
277 inner: D,
278 excluded: std::sync::Arc<Vec<NamespacePath>>,
279}
280
281impl<D> FilteredDemandSource<D> {
282 #[must_use]
284 pub fn new(inner: D, excluded: Vec<NamespacePath>) -> Self {
285 Self {
286 inner,
287 excluded: std::sync::Arc::new(excluded),
288 }
289 }
290
291 #[must_use]
293 pub const fn inner(&self) -> &D {
294 &self.inner
295 }
296
297 fn excludes(&self, path: &NamespacePath) -> bool {
298 self.excluded.iter().any(|root| path.is_within(root))
299 }
300
301 fn excludes_child(&self, directory: &NamespacePath, name: &crate::kernel::LogicalName) -> bool {
302 self.excluded.iter().any(|root| {
303 root.components().len() == directory.components().len().saturating_add(1)
304 && root.components().starts_with(directory.components())
305 && root.components().last() == Some(name)
306 })
307 }
308}
309
310#[async_trait::async_trait]
311impl<D: DemandSource> DemandSource for FilteredDemandSource<D> {
312 fn reference(&self) -> SourceReference {
313 self.inner.reference()
314 }
315
316 async fn lookup(
317 &self,
318 source: SourceReference,
319 path: &NamespacePath,
320 cancellation: &CancellationToken,
321 ) -> DemandResult<Option<SourceNode>> {
322 if self.excludes(path) {
323 return Ok(OperationReceipt {
324 value: None,
325 work: WorkCounters::default(),
326 });
327 }
328 self.inner.lookup(source, path, cancellation).await
329 }
330
331 async fn list_page(
332 &self,
333 source: SourceReference,
334 directory: &NamespacePath,
335 cursor: Option<SourceCursor>,
336 maximum_entries: u32,
337 cancellation: &CancellationToken,
338 ) -> DemandResult<SourceDirectoryPage> {
339 if self.excludes(directory) {
340 return Err(OperationFailure::new(
341 DemandError::Absent,
342 WorkCounters::default(),
343 ));
344 }
345 let mut page = self
346 .inner
347 .list_page(source, directory, cursor, maximum_entries, cancellation)
348 .await?;
349 page.value
350 .entries
351 .retain(|entry| !self.excludes_child(directory, &entry.name));
352 Ok(page)
353 }
354
355 async fn read_range(
356 &self,
357 source: SourceReference,
358 path: &NamespacePath,
359 expected: SourceVersion,
360 offset: u64,
361 length: u64,
362 cancellation: &CancellationToken,
363 ) -> DemandResult<Bytes> {
364 if self.excludes(path) {
365 return Err(OperationFailure::new(
366 DemandError::Absent,
367 WorkCounters::default(),
368 ));
369 }
370 self.inner
371 .read_range(source, path, expected, offset, length, cancellation)
372 .await
373 }
374
375 async fn read_link(
376 &self,
377 source: SourceReference,
378 path: &NamespacePath,
379 expected: SourceVersion,
380 cancellation: &CancellationToken,
381 ) -> DemandResult<Bytes> {
382 if self.excludes(path) {
383 return Err(OperationFailure::new(
384 DemandError::Absent,
385 WorkCounters::default(),
386 ));
387 }
388 self.inner
389 .read_link(source, path, expected, cancellation)
390 .await
391 }
392}
393
394#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
396pub mod native {
397 use super::*;
398 use crate::model::{FilesystemProfile, VolumeLimits};
399 use crate::native_host::HostRoot;
400 #[cfg(unix)]
401 use cap_std::fs::FileTypeExt as _;
402 use cap_std::fs::{Metadata, MetadataExt, ReadDir};
403 use std::collections::{BTreeMap, VecDeque};
404 use std::path::{Path, PathBuf};
405 use std::sync::atomic::{AtomicU64, Ordering};
406 use std::sync::{Arc, Mutex};
407 use tokio::sync::Semaphore;
408
409 const MAXIMUM_NATIVE_REQUESTS: usize = 64;
410
411 struct DirectoryState {
412 directory: NamespacePath,
413 version: SourceVersion,
414 entries: ReadDir,
415 pending: Option<cap_std::fs::DirEntry>,
416 replay: VecDeque<SourceDirectoryEntry>,
417 }
418
419 struct CursorState {
420 directory: Option<DirectoryState>,
421 }
422
423 pub(super) struct CursorLease<'a> {
424 source: &'a NativeDemandSource,
425 reference: SourceReference,
426 token: u64,
427 state: Option<DirectoryState>,
428 restore: bool,
429 }
430
431 impl CursorLease<'_> {
432 fn state(&mut self) -> Result<&mut DirectoryState, DemandError> {
433 self.state.as_mut().ok_or(DemandError::StaleCursor)
434 }
435
436 fn take(&mut self) -> Result<DirectoryState, DemandError> {
437 self.state.take().ok_or(DemandError::StaleCursor)
438 }
439
440 fn discard(&mut self) {
441 self.state.take();
442 if self.restore {
443 if let Ok(mut active) = self.source.inner.cursors.lock() {
444 active.remove(&self.token);
445 }
446 self.restore = false;
447 }
448 }
449
450 fn abandon(&mut self) {
451 self.discard();
452 }
453
454 fn commit(&mut self) -> Result<(), DemandError> {
455 let mut active = self
456 .source
457 .inner
458 .cursors
459 .lock()
460 .map_err(|_| DemandError::StaleCursor)?;
461 if self.source.reference() != self.reference {
462 return Err(DemandError::StaleSource);
463 }
464 if self.restore {
465 let Some(slot) = active.get_mut(&self.token) else {
466 return Err(DemandError::StaleCursor);
467 };
468 slot.directory = Some(self.take()?);
469 self.restore = false;
470 } else {
471 if active.len() >= 1024
472 && let Some(oldest) = active.keys().next().copied()
473 {
474 active.remove(&oldest);
475 }
476 active.insert(
477 self.token,
478 CursorState {
479 directory: Some(self.take()?),
480 },
481 );
482 }
483 Ok(())
484 }
485 }
486
487 impl Drop for CursorLease<'_> {
488 fn drop(&mut self) {
489 if !self.restore {
490 return;
491 }
492 if let Ok(mut active) = self.source.inner.cursors.lock()
493 && self.source.reference() == self.reference
494 && let Some(slot) = active.get_mut(&self.token)
495 {
496 if let Some(state) = self.state.take() {
497 slot.directory = Some(state);
498 } else {
499 active.remove(&self.token);
500 }
501 }
502 }
503 }
504
505 #[derive(Clone)]
508 pub struct NativeDemandSource {
509 inner: Arc<NativeDemandInner>,
510 }
511
512 struct NativeDemandInner {
513 path: PathBuf,
514 root: HostRoot,
515 identity: [u8; 16],
516 epoch: AtomicU64,
517 profile: FilesystemProfile,
518 limits: VolumeLimits,
519 cursors: Mutex<BTreeMap<u64, CursorState>>,
520 next_cursor: AtomicU64,
521 requests: Arc<Semaphore>,
522 observer: Option<Arc<dyn DemandDirectoryObserver>>,
523 }
524
525 struct CancelWorkerOnDrop(CancellationToken);
526
527 impl Drop for CancelWorkerOnDrop {
528 fn drop(&mut self) {
529 self.0.cancel();
530 }
531 }
532
533 impl NativeDemandSource {
534 #[must_use]
536 pub fn root_identity(&self) -> crate::NativeRootIdentity {
537 self.inner.root.identity()
538 }
539
540 #[cfg(test)]
541 pub(super) async fn occupy_all_requests(
542 &self,
543 ) -> Result<tokio::sync::OwnedSemaphorePermit, tokio::sync::AcquireError> {
544 self.inner.requests.clone().acquire_many_owned(64).await
545 }
546
547 #[cfg(test)]
548 pub(super) async fn wait_for_cancelled_worker(
549 &self,
550 started: tokio::sync::oneshot::Sender<()>,
551 stopped: tokio::sync::oneshot::Sender<()>,
552 ) {
553 let cancellation = CancellationToken::new();
554 let _ = self
555 .run_blocking(&cancellation, move |_, worker_cancellation| {
556 let _ = started.send(());
557 while !worker_cancellation.is_cancelled() {
558 std::thread::yield_now();
559 }
560 let _ = stopped.send(());
561 Ok(OperationReceipt {
562 value: (),
563 work: WorkCounters::default(),
564 })
565 })
566 .await;
567 }
568
569 #[cfg(test)]
570 pub(super) async fn validate_after_work_for_test(
571 &self,
572 work: WorkCounters,
573 cancellation: &CancellationToken,
574 ) -> DemandResult<()> {
575 self.run_blocking_after_work(work, cancellation, |_, _| {
576 unreachable!("validation must not start")
577 })
578 .await
579 }
580
581 #[cfg(test)]
582 pub(super) fn cancel_during_page(
583 &self,
584 source: SourceReference,
585 directory: &NamespacePath,
586 cursor: SourceCursor,
587 cancellation: &CancellationToken,
588 ) -> DemandResult<()> {
589 measured(|work| {
590 let metadata = self.metadata(directory)?.ok_or(DemandError::Absent)?;
591 let mut lease =
592 self.cursor_lease(source, directory, version(&metadata), Some(cursor))?;
593 let mut visited = 0;
594 let result = self.page_entries(lease.state()?, 4096, cancellation, work, || {
595 visited += 1;
596 if visited == 2 {
597 cancellation.cancel();
598 }
599 });
600 match result {
601 Err(DemandError::Cancelled) => {
602 drop(lease);
603 Ok(())
604 }
605 Err(error) => Err(error),
606 Ok(_) => Err(DemandError::InvalidRequest),
607 }
608 })
609 }
610
611 pub async fn open(
613 path: impl AsRef<Path>,
614 profile: FilesystemProfile,
615 limits: VolumeLimits,
616 ) -> Result<Self, DemandError> {
617 Self::open_configured(
618 path,
619 profile,
620 limits,
621 SourceReference {
622 identity: *uuid::Uuid::new_v4().as_bytes(),
623 epoch: 0,
624 },
625 None,
626 )
627 .await
628 }
629
630 pub async fn open_with_observer(
637 path: impl AsRef<Path>,
638 profile: FilesystemProfile,
639 limits: VolumeLimits,
640 observer: Arc<dyn DemandDirectoryObserver>,
641 ) -> Result<Self, DemandError> {
642 Self::open_with_reference_and_observer(
643 path,
644 profile,
645 limits,
646 SourceReference {
647 identity: *uuid::Uuid::new_v4().as_bytes(),
648 epoch: 0,
649 },
650 observer,
651 )
652 .await
653 }
654
655 pub async fn open_with_reference(
660 path: impl AsRef<Path>,
661 profile: FilesystemProfile,
662 limits: VolumeLimits,
663 reference: SourceReference,
664 ) -> Result<Self, DemandError> {
665 Self::open_configured(path, profile, limits, reference, None).await
666 }
667
668 pub async fn open_with_reference_and_observer(
676 path: impl AsRef<Path>,
677 profile: FilesystemProfile,
678 limits: VolumeLimits,
679 reference: SourceReference,
680 observer: Arc<dyn DemandDirectoryObserver>,
681 ) -> Result<Self, DemandError> {
682 Self::open_configured(path, profile, limits, reference, Some(observer)).await
683 }
684
685 async fn open_configured(
686 path: impl AsRef<Path>,
687 profile: FilesystemProfile,
688 limits: VolumeLimits,
689 reference: SourceReference,
690 observer: Option<Arc<dyn DemandDirectoryObserver>>,
691 ) -> Result<Self, DemandError> {
692 let path = path.as_ref().to_path_buf();
693 tokio::task::spawn_blocking(move || {
694 Self::open_blocking(path, profile, limits, reference, observer)
695 })
696 .await
697 .map_err(|_| DemandError::WorkerUnavailable)?
698 }
699
700 fn open_blocking(
701 path: PathBuf,
702 profile: FilesystemProfile,
703 limits: VolumeLimits,
704 reference: SourceReference,
705 observer: Option<Arc<dyn DemandDirectoryObserver>>,
706 ) -> Result<Self, DemandError> {
707 let path = if path.is_absolute() {
708 path
709 } else {
710 std::env::current_dir()?.join(path)
711 };
712 let root = HostRoot::open(&path)?;
713 Ok(Self {
714 inner: Arc::new(NativeDemandInner {
715 path,
716 root,
717 identity: reference.identity,
718 epoch: AtomicU64::new(reference.epoch),
719 profile,
720 limits,
721 cursors: Mutex::new(BTreeMap::new()),
722 next_cursor: AtomicU64::new(0),
723 requests: Arc::new(Semaphore::new(MAXIMUM_NATIVE_REQUESTS)),
724 observer,
725 }),
726 })
727 }
728
729 fn observe_directory(&self, directory: &NamespacePath) -> Result<(), DemandError> {
730 self.inner
731 .observer
732 .as_ref()
733 .map_or(Ok(()), |observer| observer.observe_directory(directory))
734 }
735
736 fn observe_parent(&self, path: &NamespacePath) -> Result<(), DemandError> {
737 let parent = path.parent().unwrap_or_else(|| path.clone());
738 self.observe_directory(&parent)
739 }
740
741 async fn run_blocking<T: Send + 'static>(
742 &self,
743 cancellation: &CancellationToken,
744 job: impl FnOnce(Self, CancellationToken) -> DemandResult<T> + Send + 'static,
745 ) -> DemandResult<T> {
746 if cancellation.is_cancelled() {
747 return Err(OperationFailure::before_work(DemandError::Cancelled));
748 }
749 let permit = tokio::select! {
750 acquired = self.inner.requests.clone().acquire_owned() =>
751 acquired.map_err(|_| OperationFailure::before_work(DemandError::WorkerUnavailable))?,
752 () = cancellation.cancelled() =>
753 return Err(OperationFailure::before_work(DemandError::Cancelled)),
754 };
755 if cancellation.is_cancelled() {
756 return Err(OperationFailure::before_work(DemandError::Cancelled));
757 }
758 let source = self.clone();
759 let cancel_on_drop = CancelWorkerOnDrop(CancellationToken::new());
760 let worker_cancellation = cancel_on_drop.0.clone();
761 let mut worker = tokio::task::spawn_blocking(move || {
762 let _permit = permit;
763 job(source, worker_cancellation)
764 });
765 let result = tokio::select! {
766 result = &mut worker => result,
767 () = cancellation.cancelled() => {
768 cancel_on_drop.0.cancel();
769 worker.await
770 },
771 };
772 result.map_err(|_| OperationFailure::before_work(DemandError::WorkerUnavailable))?
773 }
774
775 async fn run_blocking_after_work<T: Send + 'static>(
776 &self,
777 work: WorkCounters,
778 cancellation: &CancellationToken,
779 job: impl FnOnce(Self, CancellationToken) -> DemandResult<T> + Send + 'static,
780 ) -> DemandResult<T> {
781 if cancellation.is_cancelled() {
782 return Err(OperationFailure::new(DemandError::Cancelled, work));
783 }
784 let permit = tokio::select! {
785 acquired = self.inner.requests.clone().acquire_owned() =>
786 acquired.map_err(|_| OperationFailure::new(DemandError::WorkerUnavailable, work))?,
787 () = cancellation.cancelled() =>
788 return Err(OperationFailure::new(DemandError::Cancelled, work)),
789 };
790 let source = self.clone();
791 let cancel_on_drop = CancelWorkerOnDrop(CancellationToken::new());
792 let worker_cancellation = cancel_on_drop.0.clone();
793 let mut worker = tokio::task::spawn_blocking(move || {
794 let _permit = permit;
795 job(source, worker_cancellation)
796 });
797 let result = tokio::select! {
798 result = &mut worker => result,
799 () = cancellation.cancelled() => {
800 cancel_on_drop.0.cancel();
801 worker.await
802 },
803 };
804 result.map_err(|_| OperationFailure::new(DemandError::WorkerUnavailable, work))?
805 }
806
807 async fn validate_read_after_work(
808 &self,
809 relative: PathBuf,
810 validation_file: std::fs::File,
811 source: SourceReference,
812 expected: SourceVersion,
813 work: WorkCounters,
814 cancellation: &CancellationToken,
815 ) -> DemandResult<()> {
816 self.run_blocking_after_work(
817 work,
818 cancellation,
819 move |provider, worker_cancellation| {
820 let after = cap_std::fs::File::from_std(validation_file)
821 .metadata()
822 .map_err(|error| OperationFailure::new(error.into(), work))?;
823 if version(&after) != expected
824 || provider
825 .inner
826 .root
827 .symlink_metadata(&relative)
828 .as_ref()
829 .map(version)
830 .ok()
831 != Some(expected)
832 {
833 return Err(OperationFailure::new(DemandError::StaleVersion, work));
834 }
835 provider
836 .check(source, &worker_cancellation)
837 .map_err(|error| OperationFailure::new(error, work))?;
838 Ok(OperationReceipt { value: (), work })
839 },
840 )
841 .await
842 }
843
844 pub fn invalidate(&self) -> SourceReference {
847 let epoch = self.inner.epoch.fetch_add(1, Ordering::AcqRel) + 1;
848 if let Ok(mut cursors) = self.inner.cursors.lock() {
849 cursors.clear();
850 }
851 SourceReference {
852 identity: self.inner.identity,
853 epoch,
854 }
855 }
856
857 fn check(
858 &self,
859 source: SourceReference,
860 cancellation: &CancellationToken,
861 ) -> Result<(), DemandError> {
862 if source != self.reference() {
863 return Err(DemandError::StaleSource);
864 }
865 if cancellation.is_cancelled() {
866 return Err(DemandError::Cancelled);
867 }
868 let current =
869 HostRoot::open(&self.inner.path).map_err(|_| DemandError::SourceUnavailable)?;
870 if current.identity() != self.inner.root.identity() {
871 return Err(DemandError::SourceUnavailable);
872 }
873 Ok(())
874 }
875
876 fn relative(&self, path: &NamespacePath) -> Result<PathBuf, DemandError> {
877 let limits = self.inner.limits;
878 if path.depth() > usize::from(limits.maximum_path_depth)
879 || path.encoded_bytes() > limits.maximum_path_bytes
880 || path.components().iter().any(|component| {
881 component.as_bytes().len()
882 > usize::try_from(limits.maximum_component_bytes).unwrap_or(usize::MAX)
883 })
884 {
885 return Err(DemandError::InvalidRequest);
886 }
887 crate::native_capture::namespace_to_host_path(path)
888 .map_err(|_| DemandError::InvalidRequest)
889 }
890
891 fn metadata(&self, path: &NamespacePath) -> Result<Option<Metadata>, DemandError> {
892 match self.inner.root.symlink_metadata(&self.relative(path)?) {
893 Ok(metadata) => Ok(Some(metadata)),
894 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
895 Err(error) => Err(error.into()),
896 }
897 }
898
899 fn node(metadata: &Metadata) -> SourceNode {
900 let kind = node_kind(metadata.file_type());
901 SourceNode {
902 kind,
903 file_identity: file_identity(metadata),
904 link_count: link_count(metadata),
905 device: device_identity(metadata, kind),
906 logical_bytes: (kind == SourceNodeKind::RegularFile
907 || cfg!(unix) && kind == SourceNodeKind::SymbolicLink)
908 .then_some(metadata.len()),
909 version: version(metadata),
910 metadata: source_metadata(metadata),
911 }
912 }
913
914 pub(super) fn cursor_lease(
915 &self,
916 source: SourceReference,
917 directory: &NamespacePath,
918 observed: SourceVersion,
919 cursor: Option<SourceCursor>,
920 ) -> Result<CursorLease<'_>, DemandError> {
921 let restore = cursor.is_some();
922 let (token, state) = if let Some(cursor) = cursor {
923 if cursor.source != source
924 || cursor.directory != *directory
925 || cursor.directory_version != observed
926 {
927 return Err(DemandError::StaleCursor);
928 }
929 let mut active = self
930 .inner
931 .cursors
932 .lock()
933 .map_err(|_| DemandError::StaleCursor)?;
934 let slot = active
935 .get_mut(&cursor.token)
936 .ok_or(DemandError::StaleCursor)?;
937 let state = slot.directory.take().ok_or(DemandError::CursorBusy)?;
938 (cursor.token, state)
939 } else {
940 let token = self
941 .inner
942 .next_cursor
943 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
944 current.checked_add(1)
945 })
946 .map_err(|_| DemandError::StaleCursor)?
947 + 1;
948 (token, self.open_directory_state(directory, observed)?)
949 };
950 Ok(CursorLease {
951 source: self,
952 reference: source,
953 token,
954 state: Some(state),
955 restore,
956 })
957 }
958
959 fn open_directory_state(
960 &self,
961 directory: &NamespacePath,
962 observed: SourceVersion,
963 ) -> Result<DirectoryState, DemandError> {
964 Ok(DirectoryState {
965 directory: directory.clone(),
966 version: observed,
967 entries: self.inner.root.read_dir(&self.relative(directory)?)?,
968 pending: None,
969 replay: VecDeque::new(),
970 })
971 }
972
973 fn next_entry(
974 &self,
975 entry: &cap_std::fs::DirEntry,
976 ) -> Result<SourceDirectoryEntry, DemandError> {
977 let (encoding, bytes) = crate::native_name::host_name_bytes(
978 &entry.file_name(),
979 self.inner.profile,
980 self.inner.limits.maximum_component_bytes,
981 )
982 .map_err(|_| DemandError::InvalidRequest)?;
983 let name = crate::kernel::LogicalName::new(
984 encoding,
985 bytes,
986 self.inner.limits.maximum_component_bytes,
987 )
988 .map_err(|_| DemandError::InvalidRequest)?;
989 let kind = node_kind(entry.file_type()?);
990 Ok(SourceDirectoryEntry { name, kind })
991 }
992
993 fn page_entries(
994 &self,
995 state: &mut DirectoryState,
996 maximum_entries: u32,
997 cancellation: &CancellationToken,
998 work: &mut WorkCounters,
999 mut on_entry: impl FnMut(),
1000 ) -> Result<Vec<SourceDirectoryEntry>, DemandError> {
1001 let mut entries = Vec::with_capacity(maximum_entries as usize);
1002 let result = (|| {
1003 while entries.len() < maximum_entries as usize {
1004 if cancellation.is_cancelled() {
1005 return Err(DemandError::Cancelled);
1006 }
1007 if let Some(entry) = state.replay.pop_front() {
1008 entries.push(entry);
1009 continue;
1010 }
1011 let next = if let Some(pending) = state.pending.take() {
1012 Some(Ok(pending))
1013 } else {
1014 let next = state.entries.next();
1015 if next.is_some() {
1016 work.source_entries_visited += 1;
1017 }
1018 next
1019 };
1020 let Some(entry) = next else { break };
1021 let entry = entry?;
1022 match self.next_entry(&entry) {
1023 Ok(converted) => entries.push(converted),
1024 Err(error) => {
1025 state.pending = Some(entry);
1026 return Err(error);
1027 }
1028 }
1029 on_entry();
1030 }
1031 if state.pending.is_none()
1032 && let Some(next) = state.entries.next()
1033 {
1034 state.pending = Some(next?);
1035 work.source_entries_visited += 1;
1036 }
1037 Ok(())
1038 })();
1039 if result.is_err() {
1040 entries.extend(state.replay.drain(..));
1041 state.replay = std::mem::take(&mut entries).into();
1042 }
1043 result.map(|()| entries)
1044 }
1045 }
1046
1047 fn node_kind(file_type: cap_std::fs::FileType) -> SourceNodeKind {
1048 if file_type.is_dir() {
1049 SourceNodeKind::Directory
1050 } else if file_type.is_file() {
1051 SourceNodeKind::RegularFile
1052 } else if file_type.is_symlink() {
1053 SourceNodeKind::SymbolicLink
1054 } else {
1055 special_node_kind(&file_type)
1056 }
1057 }
1058
1059 #[cfg(unix)]
1060 fn special_node_kind(file_type: &cap_std::fs::FileType) -> SourceNodeKind {
1061 if file_type.is_fifo() {
1062 SourceNodeKind::Fifo
1063 } else if file_type.is_socket() {
1064 SourceNodeKind::Socket
1065 } else if file_type.is_char_device() {
1066 SourceNodeKind::CharacterDevice
1067 } else if file_type.is_block_device() {
1068 SourceNodeKind::BlockDevice
1069 } else {
1070 SourceNodeKind::Unsupported
1071 }
1072 }
1073
1074 #[cfg(not(unix))]
1075 fn special_node_kind(_file_type: &cap_std::fs::FileType) -> SourceNodeKind {
1076 SourceNodeKind::Unsupported
1077 }
1078
1079 #[async_trait::async_trait]
1080 impl DemandSource for NativeDemandSource {
1081 fn reference(&self) -> SourceReference {
1082 SourceReference {
1083 identity: self.inner.identity,
1084 epoch: self.inner.epoch.load(Ordering::Acquire),
1085 }
1086 }
1087
1088 async fn lookup(
1089 &self,
1090 source: SourceReference,
1091 path: &NamespacePath,
1092 cancellation: &CancellationToken,
1093 ) -> DemandResult<Option<SourceNode>> {
1094 let path = path.clone();
1095 self.run_blocking(cancellation, move |provider, request_cancellation| {
1096 measured(|work| {
1097 provider.check(source, &request_cancellation)?;
1098 provider.observe_parent(&path)?;
1099 work.source_path_components = path.depth() as u64;
1100 let node = provider.metadata(&path)?.as_ref().map(Self::node);
1101 provider.check(source, &request_cancellation)?;
1102 Ok(node)
1103 })
1104 })
1105 .await
1106 }
1107
1108 async fn list_page(
1109 &self,
1110 source: SourceReference,
1111 directory: &NamespacePath,
1112 cursor: Option<SourceCursor>,
1113 maximum_entries: u32,
1114 cancellation: &CancellationToken,
1115 ) -> DemandResult<SourceDirectoryPage> {
1116 let directory = directory.clone();
1117 self.run_blocking(cancellation, move |provider, request_cancellation| {
1118 measured(|work| {
1119 provider.check(source, &request_cancellation)?;
1120 provider.observe_directory(&directory)?;
1121 work.source_path_components = directory.depth() as u64;
1122 if maximum_entries == 0 || maximum_entries > 4096 {
1123 return Err(DemandError::InvalidRequest);
1124 }
1125 let metadata = provider.metadata(&directory)?.ok_or(DemandError::Absent)?;
1126 if !metadata.is_dir() {
1127 return Err(DemandError::NotDirectory);
1128 }
1129 let observed = version(&metadata);
1130 let mut lease = provider.cursor_lease(source, &directory, observed, cursor)?;
1131 if lease.state()?.directory != directory || lease.state()?.version != observed {
1132 lease.discard();
1133 return Err(DemandError::StaleCursor);
1134 }
1135 let entries = provider.page_entries(
1136 lease.state()?,
1137 maximum_entries,
1138 &request_cancellation,
1139 work,
1140 || {},
1141 );
1142 if entries.is_err() && !matches!(entries, Err(DemandError::Cancelled)) {
1143 lease.abandon();
1144 }
1145 let entries = entries?;
1146 if let Err(error) = provider.check(source, &request_cancellation) {
1147 if matches!(error, DemandError::Cancelled) {
1148 lease.state()?.replay.extend(entries);
1149 } else {
1150 lease.abandon();
1151 }
1152 return Err(error);
1153 }
1154 let current = provider.metadata(&directory);
1155 if current.is_err() {
1156 lease.abandon();
1157 }
1158 if current?.as_ref().map(version) != Some(observed) {
1159 lease.discard();
1160 return Err(DemandError::StaleVersion);
1161 }
1162 let next =
1163 if lease.state()?.pending.is_some() || !lease.state()?.replay.is_empty() {
1164 let token = lease.token;
1165 lease.commit()?;
1166 Some(SourceCursor {
1167 source,
1168 directory: directory.clone(),
1169 directory_version: observed,
1170 token,
1171 })
1172 } else {
1173 lease.discard();
1174 None
1175 };
1176 work.items_returned = entries.len() as u64;
1177 Ok(SourceDirectoryPage {
1178 entries,
1179 next,
1180 version: observed,
1181 })
1182 })
1183 })
1184 .await
1185 }
1186
1187 async fn read_range(
1188 &self,
1189 source: SourceReference,
1190 path: &NamespacePath,
1191 expected: SourceVersion,
1192 offset: u64,
1193 length: u64,
1194 cancellation: &CancellationToken,
1195 ) -> DemandResult<Bytes> {
1196 if cancellation.is_cancelled() {
1197 return Err(OperationFailure::before_work(DemandError::Cancelled));
1198 }
1199 let length = usize::try_from(length)
1200 .map_err(|_| OperationFailure::before_work(DemandError::InvalidRequest))?;
1201 if length > 1024 * 1024 || offset.checked_add(length as u64).is_none() {
1202 return Err(OperationFailure::before_work(DemandError::InvalidRequest));
1203 }
1204 let mut work = WorkCounters {
1205 source_path_components: path.depth() as u64,
1206 ..WorkCounters::default()
1207 };
1208 let path = path.clone();
1209 let prepared = self
1210 .run_blocking(cancellation, move |provider, worker_cancellation| {
1211 provider
1212 .check(source, &worker_cancellation)
1213 .map_err(|error| OperationFailure::new(error, work))?;
1214 provider
1215 .observe_parent(&path)
1216 .map_err(|error| OperationFailure::new(error, work))?;
1217 let relative = provider
1218 .relative(&path)
1219 .map_err(|error| OperationFailure::new(error, work))?;
1220 let file = match provider.inner.root.open_file(&relative) {
1221 Ok(file) => file,
1222 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1223 return Err(OperationFailure::new(DemandError::Absent, work));
1224 }
1225 Err(error) => {
1226 return Err(OperationFailure::new(error.into(), work));
1227 }
1228 };
1229 let before = file
1230 .metadata()
1231 .map_err(|error| OperationFailure::new(error.into(), work))?;
1232 if !before.is_file() {
1233 return Err(OperationFailure::new(DemandError::NotRegularFile, work));
1234 }
1235 if version(&before) != expected {
1236 return Err(OperationFailure::new(DemandError::StaleVersion, work));
1237 }
1238 let available = before.len().saturating_sub(offset);
1239 let length = length.min(usize::try_from(available).unwrap_or(usize::MAX));
1240 Ok(OperationReceipt {
1241 value: (relative, file.into_std(), length),
1242 work,
1243 })
1244 })
1245 .await?
1246 .value;
1247 let (relative, file, length) = prepared;
1248 let validation_file = file
1249 .try_clone()
1250 .map_err(|error| OperationFailure::new(error.into(), work))?;
1251 let mut read = acyclic_native_runtime::read_batch_async(
1252 file,
1253 vec![acyclic_native_runtime::OwnedRead { offset, length }],
1254 );
1255 let mut cancelled = false;
1256 let result = tokio::select! {
1257 result = &mut read => result,
1258 () = cancellation.cancelled() => {
1259 cancelled = true;
1260 read.await
1261 }
1262 }
1263 .map_err(|error| OperationFailure::new(error.into(), work))?;
1264 let bytes = result
1265 .into_iter()
1266 .next()
1267 .ok_or_else(|| OperationFailure::new(DemandError::SourceUnavailable, work))?;
1268 work.source_bytes_read = bytes.len() as u64;
1269 if cancelled {
1270 return Err(OperationFailure::new(DemandError::Cancelled, work));
1271 }
1272 self.validate_read_after_work(
1273 relative,
1274 validation_file,
1275 source,
1276 expected,
1277 work,
1278 cancellation,
1279 )
1280 .await?;
1281 work.output_bytes = bytes.len() as u64;
1282 Ok(OperationReceipt { value: bytes, work })
1283 }
1284
1285 async fn read_link(
1286 &self,
1287 source: SourceReference,
1288 path: &NamespacePath,
1289 expected: SourceVersion,
1290 cancellation: &CancellationToken,
1291 ) -> DemandResult<Bytes> {
1292 let path = path.clone();
1293 self.run_blocking(cancellation, move |provider, request_cancellation| {
1294 measured(|work| {
1295 provider.check(source, &request_cancellation)?;
1296 provider.observe_parent(&path)?;
1297 work.source_path_components = path.depth() as u64;
1298 let before = provider.metadata(&path)?.ok_or(DemandError::Absent)?;
1299 if !before.file_type().is_symlink() {
1300 return Err(DemandError::InvalidRequest);
1301 }
1302 if version(&before) != expected {
1303 return Err(DemandError::StaleVersion);
1304 }
1305 let target = provider.inner.root.read_link(&provider.relative(&path)?)?;
1306 let bytes = os_string_bytes(target.as_os_str());
1307 work.source_bytes_read = bytes.len() as u64;
1308 work.output_bytes = bytes.len() as u64;
1309 let after = provider.metadata(&path)?.ok_or(DemandError::Absent)?;
1310 if version(&after) != expected {
1311 return Err(DemandError::StaleVersion);
1312 }
1313 provider.check(source, &request_cancellation)?;
1314 Ok(Bytes::from(bytes))
1315 })
1316 })
1317 .await
1318 }
1319 }
1320
1321 #[cfg(unix)]
1322 fn os_string_bytes(value: &std::ffi::OsStr) -> Vec<u8> {
1323 use std::os::unix::ffi::OsStrExt as _;
1324 value.as_bytes().to_vec()
1325 }
1326
1327 #[cfg(windows)]
1328 fn os_string_bytes(value: &std::ffi::OsStr) -> Vec<u8> {
1329 use std::os::windows::ffi::OsStrExt as _;
1330 value.encode_wide().flat_map(u16::to_le_bytes).collect()
1331 }
1332
1333 fn version(metadata: &Metadata) -> SourceVersion {
1334 let mut hash = blake3::Hasher::new();
1335 hash.update(b"acyclic-fs-native-source-version-v1\0");
1336 hash.update(&metadata.len().to_le_bytes());
1337 #[cfg(unix)]
1338 {
1339 hash.update(&metadata.dev().to_le_bytes());
1340 hash.update(&metadata.ino().to_le_bytes());
1341 hash.update(&metadata.mtime().to_le_bytes());
1342 hash.update(&metadata.mtime_nsec().to_le_bytes());
1343 hash.update(&metadata.ctime().to_le_bytes());
1344 hash.update(&metadata.ctime_nsec().to_le_bytes());
1345 }
1346 #[cfg(windows)]
1347 {
1348 hash.update(&metadata.file_attributes().to_le_bytes());
1349 hash.update(&metadata.last_write_time().to_le_bytes());
1350 hash.update(&metadata.creation_time().to_le_bytes());
1351 hash.update(
1352 &cap_primitives::fs::_WindowsByHandle::volume_serial_number(metadata)
1353 .unwrap_or_default()
1354 .to_le_bytes(),
1355 );
1356 hash.update(
1357 &cap_primitives::fs::_WindowsByHandle::file_index(metadata)
1358 .unwrap_or_default()
1359 .to_le_bytes(),
1360 );
1361 }
1362 SourceVersion(*hash.finalize().as_bytes())
1363 }
1364
1365 fn file_identity(metadata: &Metadata) -> [u8; 32] {
1366 let mut hash = blake3::Hasher::new();
1367 hash.update(b"acyclic-fs-native-source-file-v1\0");
1368 #[cfg(unix)]
1369 {
1370 hash.update(&metadata.dev().to_le_bytes());
1371 hash.update(&metadata.ino().to_le_bytes());
1372 }
1373 #[cfg(windows)]
1374 {
1375 hash.update(
1376 &cap_primitives::fs::_WindowsByHandle::volume_serial_number(metadata)
1377 .unwrap_or_default()
1378 .to_le_bytes(),
1379 );
1380 hash.update(
1381 &cap_primitives::fs::_WindowsByHandle::file_index(metadata)
1382 .unwrap_or_default()
1383 .to_le_bytes(),
1384 );
1385 }
1386 *hash.finalize().as_bytes()
1387 }
1388
1389 fn link_count(metadata: &Metadata) -> Option<u64> {
1390 #[cfg(unix)]
1391 {
1392 Some(metadata.nlink())
1393 }
1394 #[cfg(windows)]
1395 {
1396 cap_primitives::fs::_WindowsByHandle::number_of_links(metadata).map(u64::from)
1397 }
1398 #[cfg(not(any(unix, windows)))]
1399 {
1400 let _ = metadata;
1401 None
1402 }
1403 }
1404
1405 #[allow(
1406 clippy::useless_conversion,
1407 reason = "dev_t and major/minor result widths differ across Unix targets"
1408 )]
1409 fn device_identity(metadata: &Metadata, kind: SourceNodeKind) -> Option<(u32, u32)> {
1410 #[cfg(unix)]
1411 {
1412 if !matches!(
1413 kind,
1414 SourceNodeKind::CharacterDevice | SourceNodeKind::BlockDevice
1415 ) {
1416 return None;
1417 }
1418 let raw: libc::dev_t = metadata.rdev().try_into().ok()?;
1419 Some((
1420 libc::major(raw).try_into().ok()?,
1421 libc::minor(raw).try_into().ok()?,
1422 ))
1423 }
1424 #[cfg(not(unix))]
1425 {
1426 let _ = (metadata, kind);
1427 None
1428 }
1429 }
1430
1431 fn source_metadata(metadata: &Metadata) -> SourceMetadata {
1432 let mut result = SourceMetadata {
1433 created_ns: metadata.created().ok().and_then(system_time_nanos),
1434 modified_ns: metadata.modified().ok().and_then(system_time_nanos),
1435 accessed_ns: metadata.accessed().ok().and_then(system_time_nanos),
1436 ..SourceMetadata::default()
1437 };
1438 #[cfg(unix)]
1439 {
1440 result.posix_mode = Some(metadata.mode());
1441 result.posix_uid = Some(metadata.uid());
1442 result.posix_gid = Some(metadata.gid());
1443 result.changed_ns = Some(
1444 i128::from(metadata.ctime()) * 1_000_000_000 + i128::from(metadata.ctime_nsec()),
1445 );
1446 }
1447 #[cfg(windows)]
1448 {
1449 result.windows_attributes = Some(metadata.file_attributes());
1450 }
1451 result
1452 }
1453
1454 fn system_time_nanos(time: cap_std::time::SystemTime) -> Option<i128> {
1455 match time.into_std().duration_since(std::time::UNIX_EPOCH) {
1456 Ok(duration) => Some(
1457 i128::from(duration.as_secs()) * 1_000_000_000
1458 + i128::from(duration.subsec_nanos()),
1459 ),
1460 Err(error) => Some(
1461 -(i128::from(error.duration().as_secs()) * 1_000_000_000
1462 + i128::from(error.duration().subsec_nanos())),
1463 ),
1464 }
1465 }
1466}
1467
1468#[cfg(all(test, feature = "native-watch", not(target_arch = "wasm32")))]
1469mod tests {
1470 use super::native::NativeDemandSource;
1471 use super::*;
1472 use crate::model::{FilesystemProfile, VolumeLimits};
1473 use std::error::Error;
1474 use std::sync::{Arc, Mutex};
1475
1476 #[derive(Default)]
1477 struct RecordingObserver {
1478 directories: Mutex<Vec<NamespacePath>>,
1479 }
1480
1481 impl DemandDirectoryObserver for RecordingObserver {
1482 fn observe_directory(&self, directory: &NamespacePath) -> Result<(), DemandError> {
1483 self.directories
1484 .lock()
1485 .map_err(|_| DemandError::SourceUnavailable)?
1486 .push(directory.clone());
1487 Ok(())
1488 }
1489 }
1490
1491 fn path(value: &str) -> Result<NamespacePath, Box<dyn Error>> {
1492 let portable = crate::path::PortablePath::parse(value, VolumeLimits::default())?;
1493 Ok(NamespacePath::from_portable(
1494 &portable,
1495 VolumeLimits::default(),
1496 )?)
1497 }
1498
1499 #[cfg(unix)]
1500 #[tokio::test]
1501 async fn native_source_reports_symbolic_link_target_bytes() -> Result<(), Box<dyn Error>> {
1502 let root = tempfile::tempdir()?;
1503 std::os::unix::fs::symlink("target", root.path().join("link"))?;
1504 let source = NativeDemandSource::open(
1505 root.path(),
1506 FilesystemProfile::Posix,
1507 VolumeLimits::default(),
1508 )
1509 .await?;
1510 let reference = source.reference();
1511 let cancellation = CancellationToken::new();
1512 let node = source
1513 .lookup(reference, &path("/link")?, &cancellation)
1514 .await?
1515 .value
1516 .ok_or("symbolic link was absent")?;
1517
1518 assert_eq!(node.kind, SourceNodeKind::SymbolicLink);
1519 assert_eq!(node.logical_bytes, Some(6));
1520 assert_eq!(
1521 source
1522 .read_link(reference, &path("/link")?, node.version, &cancellation)
1523 .await?
1524 .value
1525 .as_ref(),
1526 b"target"
1527 );
1528 Ok(())
1529 }
1530
1531 #[tokio::test]
1532 async fn native_source_observes_only_the_demanded_directory_before_reading()
1533 -> Result<(), Box<dyn Error>> {
1534 let root = tempfile::tempdir()?;
1535 std::fs::create_dir(root.path().join("nested"))?;
1536 std::fs::write(root.path().join("nested/file"), b"content")?;
1537 let observer = Arc::new(RecordingObserver::default());
1538 let source = NativeDemandSource::open_with_observer(
1539 root.path(),
1540 FilesystemProfile::Portable,
1541 VolumeLimits::default(),
1542 observer.clone(),
1543 )
1544 .await?;
1545 let reference = source.reference();
1546 source
1547 .lookup(reference, &path("/nested/file")?, &CancellationToken::new())
1548 .await?;
1549 assert_eq!(
1550 observer
1551 .directories
1552 .lock()
1553 .map_err(|_| "observer state poisoned")?
1554 .as_slice(),
1555 &[path("/nested")?]
1556 );
1557 Ok(())
1558 }
1559
1560 #[tokio::test]
1561 async fn native_provider_reads_do_not_visit_unrelated_content() -> Result<(), Box<dyn Error>> {
1562 let root = tempfile::tempdir()?;
1563 std::fs::create_dir(root.path().join("forbidden"))?;
1564 std::fs::write(
1565 root.path().join("forbidden").join("unrelated"),
1566 vec![7; 8 * 1024 * 1024],
1567 )?;
1568 let mut body = vec![b'0'; 1024 * 1024];
1569 body[3..7].copy_from_slice(b"3456");
1570 std::fs::write(root.path().join("seen"), body)?;
1571
1572 let source = NativeDemandSource::open(
1573 root.path(),
1574 FilesystemProfile::Portable,
1575 VolumeLimits::default(),
1576 )
1577 .await?;
1578 let provider: &dyn DemandSource = &source;
1579 let reference = provider.reference();
1580 let cancellation = CancellationToken::new();
1581 let node = provider
1582 .lookup(reference, &path("/seen")?, &cancellation)
1583 .await?;
1584 assert_eq!(node.work.source_path_components, 1);
1585 assert_eq!(node.work.source_entries_visited, 0);
1586 assert_eq!(node.work.source_bytes_read, 0);
1587 let version = node.value.ok_or("seen path was absent")?.version;
1588
1589 let bytes = provider
1590 .read_range(reference, &path("/seen")?, version, 3, 4, &cancellation)
1591 .await?;
1592 assert_eq!(bytes.value.as_ref(), b"3456");
1593 assert_eq!(bytes.work.source_bytes_read, 4);
1594 assert_eq!(bytes.work.source_entries_visited, 0);
1595
1596 let first = provider
1597 .list_page(reference, &path("/")?, None, 1, &cancellation)
1598 .await?;
1599 assert_eq!(first.value.entries.len(), 1);
1600 assert!(first.work.source_entries_visited <= 2);
1601 let continuation = first.value.next.ok_or("second root entry missing")?;
1602 let second = provider
1603 .list_page(reference, &path("/")?, Some(continuation), 1, &cancellation)
1604 .await?;
1605 assert_eq!(second.value.entries.len(), 1);
1606 assert!(second.work.source_entries_visited <= 1);
1607 Ok(())
1608 }
1609
1610 #[tokio::test]
1611 async fn native_requests_wait_for_capacity_and_cancel_before_work() -> Result<(), Box<dyn Error>>
1612 {
1613 let root = tempfile::tempdir()?;
1614 let source = NativeDemandSource::open(
1615 root.path(),
1616 FilesystemProfile::Portable,
1617 VolumeLimits::default(),
1618 )
1619 .await?;
1620 let permits = source.occupy_all_requests().await?;
1621 let cancellation = CancellationToken::new();
1622 let root_path = path("/")?;
1623 let query = source.lookup(source.reference(), &root_path, &cancellation);
1624 tokio::pin!(query);
1625 assert!(
1626 tokio::time::timeout(std::time::Duration::from_millis(50), &mut query)
1627 .await
1628 .is_err()
1629 );
1630 cancellation.cancel();
1631 let Err(failure) =
1632 tokio::time::timeout(std::time::Duration::from_secs(1), &mut query).await?
1633 else {
1634 return Err("cancelled request entered the worker".into());
1635 };
1636 assert!(matches!(failure.error, DemandError::Cancelled));
1637 assert_eq!(*failure.work, WorkCounters::default());
1638 drop(permits);
1639 Ok(())
1640 }
1641
1642 #[tokio::test]
1643 async fn post_read_validation_cancels_with_exact_prior_work() -> Result<(), Box<dyn Error>> {
1644 let root = tempfile::tempdir()?;
1645 let source = NativeDemandSource::open(
1646 root.path(),
1647 FilesystemProfile::Portable,
1648 VolumeLimits::default(),
1649 )
1650 .await?;
1651 let permits = source.occupy_all_requests().await?;
1652 let cancellation = CancellationToken::new();
1653 let work = WorkCounters {
1654 source_path_components: 2,
1655 source_bytes_read: 17,
1656 ..WorkCounters::default()
1657 };
1658 let validation = source.validate_after_work_for_test(work, &cancellation);
1659 tokio::pin!(validation);
1660 assert!(
1661 tokio::time::timeout(std::time::Duration::from_millis(50), &mut validation)
1662 .await
1663 .is_err()
1664 );
1665 cancellation.cancel();
1666 let failure = tokio::time::timeout(std::time::Duration::from_secs(1), &mut validation)
1667 .await?
1668 .err()
1669 .ok_or("cancelled validation unexpectedly succeeded")?;
1670 assert!(matches!(failure.error, DemandError::Cancelled));
1671 assert_eq!(*failure.work, work);
1672 drop(permits);
1673 Ok(())
1674 }
1675
1676 #[tokio::test]
1677 async fn dropping_request_cancels_admitted_worker() -> Result<(), Box<dyn Error>> {
1678 let root = tempfile::tempdir()?;
1679 let source = NativeDemandSource::open(
1680 root.path(),
1681 FilesystemProfile::Portable,
1682 VolumeLimits::default(),
1683 )
1684 .await?;
1685 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1686 let (stopped_tx, stopped_rx) = tokio::sync::oneshot::channel();
1687 let request = tokio::spawn(async move {
1688 source
1689 .wait_for_cancelled_worker(started_tx, stopped_tx)
1690 .await;
1691 });
1692 tokio::time::timeout(std::time::Duration::from_secs(1), started_rx).await??;
1693 request.abort();
1694 let _ = request.await;
1695 tokio::time::timeout(std::time::Duration::from_secs(1), stopped_rx).await??;
1696 Ok(())
1697 }
1698
1699 #[tokio::test]
1700 async fn cancelled_directory_page_retries_without_skipping_entries()
1701 -> Result<(), Box<dyn Error>> {
1702 let root = tempfile::tempdir()?;
1703 for name in ["a", "b", "c", "d", "e", "f"] {
1704 std::fs::write(root.path().join(name), b"x")?;
1705 }
1706 let source = NativeDemandSource::open(
1707 root.path(),
1708 FilesystemProfile::Portable,
1709 VolumeLimits::default(),
1710 )
1711 .await?;
1712 let reference = source.reference();
1713 let directory = path("/")?;
1714 let live = CancellationToken::new();
1715 let first = source
1716 .list_page(reference, &directory, None, 1, &live)
1717 .await?;
1718 let cursor = first.value.next.ok_or("expected continuation")?;
1719 let interrupted = CancellationToken::new();
1720 source.cancel_during_page(reference, &directory, cursor.clone(), &interrupted)?;
1721 let mut found = first.value.entries;
1722 let mut next = Some(cursor);
1723 while let Some(cursor) = next {
1724 let page = source
1725 .list_page(reference, &directory, Some(cursor), 2, &live)
1726 .await?;
1727 found.extend(page.value.entries);
1728 next = page.value.next;
1729 }
1730 let mut names: Vec<_> = found
1731 .into_iter()
1732 .map(|entry| entry.name.as_bytes().to_vec())
1733 .collect();
1734 names.sort();
1735 assert_eq!(names.len(), 6);
1736 assert_eq!(names.first().map(Vec::as_slice), Some(&b"a"[..]));
1737 assert_eq!(names.last().map(Vec::as_slice), Some(&b"f"[..]));
1738 Ok(())
1739 }
1740
1741 #[tokio::test]
1742 async fn concurrent_use_reports_busy_without_losing_cursor() -> Result<(), Box<dyn Error>> {
1743 let root = tempfile::tempdir()?;
1744 for name in ["a", "b", "c"] {
1745 std::fs::write(root.path().join(name), b"x")?;
1746 }
1747 let source = NativeDemandSource::open(
1748 root.path(),
1749 FilesystemProfile::Portable,
1750 VolumeLimits::default(),
1751 )
1752 .await?;
1753 let reference = source.reference();
1754 let directory = path("/")?;
1755 let cancellation = CancellationToken::new();
1756 let first = source
1757 .list_page(reference, &directory, None, 1, &cancellation)
1758 .await?;
1759 let cursor = first.value.next.ok_or("expected continuation")?;
1760 let lease = source.cursor_lease(
1761 reference,
1762 &directory,
1763 first.value.version,
1764 Some(cursor.clone()),
1765 )?;
1766 assert!(matches!(
1767 source.cursor_lease(
1768 reference,
1769 &directory,
1770 first.value.version,
1771 Some(cursor.clone())
1772 ),
1773 Err(DemandError::CursorBusy)
1774 ));
1775 drop(lease);
1776 let second = source
1777 .list_page(reference, &directory, Some(cursor), 1, &cancellation)
1778 .await?;
1779 assert_eq!(second.value.entries.len(), 1);
1780 Ok(())
1781 }
1782
1783 #[tokio::test]
1784 async fn version_and_cursor_evidence_fail_closed() -> Result<(), Box<dyn Error>> {
1785 let root = tempfile::tempdir()?;
1786 std::fs::create_dir(root.path().join("directory"))?;
1787 std::fs::write(root.path().join("file"), b"old")?;
1788 let source = NativeDemandSource::open(
1789 root.path(),
1790 FilesystemProfile::Portable,
1791 VolumeLimits::default(),
1792 )
1793 .await?;
1794 let reference = source.reference();
1795 let cancellation = CancellationToken::new();
1796 let node = source
1797 .lookup(reference, &path("/file")?, &cancellation)
1798 .await?
1799 .value
1800 .ok_or("file absent")?;
1801 std::fs::write(root.path().join("file"), b"newer contents")?;
1802 let Err(stale) = source
1803 .read_range(
1804 reference,
1805 &path("/file")?,
1806 node.version,
1807 0,
1808 3,
1809 &cancellation,
1810 )
1811 .await
1812 else {
1813 return Err("stale source content was accepted".into());
1814 };
1815 assert!(matches!(stale.error, DemandError::StaleVersion));
1816 assert_eq!(stale.work.source_path_components, 1);
1817 assert_eq!(stale.work.source_bytes_read, 0);
1818 assert!(matches!(
1819 source
1820 .read_metadata(reference, &path("/file")?, Some(node.version), &cancellation)
1821 .await,
1822 Err(failure) if matches!(failure.error, DemandError::StaleVersion)
1823 ));
1824
1825 let first = source
1826 .list_page(reference, &path("/")?, None, 1, &cancellation)
1827 .await?;
1828 let cursor = first.value.next.ok_or("second root entry missing")?;
1829 assert!(matches!(
1830 source
1831 .list_page(
1832 reference,
1833 &path("/directory")?,
1834 Some(cursor),
1835 1,
1836 &cancellation
1837 )
1838 .await,
1839 Err(failure) if matches!(failure.error, DemandError::StaleCursor)
1840 ));
1841 source.invalidate();
1842 assert!(matches!(
1843 source
1844 .lookup(reference, &path("/file")?, &cancellation)
1845 .await,
1846 Err(failure) if matches!(failure.error, DemandError::StaleSource)
1847 ));
1848 Ok(())
1849 }
1850
1851 #[cfg(target_os = "linux")]
1852 #[tokio::test]
1853 async fn native_lookup_preserves_non_utf8_posix_names() -> Result<(), Box<dyn Error>> {
1854 use crate::kernel::{LogicalName, NameEncoding};
1855 use std::os::unix::ffi::OsStringExt;
1856
1857 let raw = vec![b'n', 0xff];
1858 let name = LogicalName::new(
1859 NameEncoding::PosixBytes,
1860 raw.clone(),
1861 VolumeLimits::default().maximum_component_bytes,
1862 )?;
1863 assert_lossless_name(
1864 FilesystemProfile::Posix,
1865 std::ffi::OsString::from_vec(raw),
1866 name,
1867 )
1868 .await
1869 }
1870
1871 #[cfg(windows)]
1872 #[tokio::test]
1873 async fn native_lookup_preserves_utf16_windows_names() -> Result<(), Box<dyn Error>> {
1874 use crate::kernel::{LogicalName, NameEncoding};
1875
1876 let raw = "snowman-☃"
1877 .encode_utf16()
1878 .flat_map(u16::to_le_bytes)
1879 .collect();
1880 let name = LogicalName::new(
1881 NameEncoding::WindowsUtf16Le,
1882 raw,
1883 VolumeLimits::default().maximum_component_bytes,
1884 )?;
1885 assert_lossless_name(
1886 FilesystemProfile::Windows,
1887 std::ffi::OsString::from("snowman-☃"),
1888 name,
1889 )
1890 .await
1891 }
1892
1893 #[cfg(windows)]
1894 #[tokio::test]
1895 async fn native_lookup_accepts_windows_names_longer_than_127_code_units()
1896 -> Result<(), Box<dyn Error>> {
1897 use crate::kernel::{LogicalName, NameEncoding};
1898
1899 let host_name = "x".repeat(200);
1900 let limits = crate::model::VolumeConfig::native(crate::model::Lifecycle::Durable).limits;
1901 let raw = host_name
1902 .encode_utf16()
1903 .flat_map(u16::to_le_bytes)
1904 .collect();
1905 let name = LogicalName::new(
1906 NameEncoding::WindowsUtf16Le,
1907 raw,
1908 limits.maximum_component_bytes,
1909 )?;
1910 let root = tempfile::tempdir()?;
1911 std::fs::write(root.path().join(&host_name), b"data")?;
1912 let source =
1913 NativeDemandSource::open(root.path(), FilesystemProfile::Windows, limits).await?;
1914 let exact = NamespacePath::new(vec![name], limits)?;
1915 assert!(
1916 source
1917 .lookup(source.reference(), &exact, &CancellationToken::new())
1918 .await?
1919 .value
1920 .is_some()
1921 );
1922 Ok(())
1923 }
1924
1925 #[cfg(any(target_os = "linux", windows))]
1926 async fn assert_lossless_name(
1927 profile: FilesystemProfile,
1928 host_name: std::ffi::OsString,
1929 name: crate::kernel::LogicalName,
1930 ) -> Result<(), Box<dyn Error>> {
1931 let root = tempfile::tempdir()?;
1932 std::fs::write(root.path().join(host_name), b"data")?;
1933 let source =
1934 NativeDemandSource::open(root.path(), profile, VolumeLimits::default()).await?;
1935 let exact = NamespacePath::new(vec![name], VolumeLimits::default())?;
1936 let node = source
1937 .lookup(source.reference(), &exact, &CancellationToken::new())
1938 .await?
1939 .value
1940 .ok_or("lossless file absent")?;
1941 let answer = source
1942 .read_range(
1943 source.reference(),
1944 &exact,
1945 node.version,
1946 0,
1947 4,
1948 &CancellationToken::new(),
1949 )
1950 .await?;
1951 assert_eq!(answer.value.as_ref(), b"data");
1952 Ok(())
1953 }
1954}