1use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use ignore::gitignore::{Gitignore, GitignoreBuilder};
7
8use crate::error_display::format_error_list;
9use crate::fs_traversal::{self, TraversalPolicy, UnsupportedEntry};
10use crate::limits::{LimitError, Limits, ResourceKind};
11use crate::pack::names_pack_path;
12use crate::project_snapshot::{ProjectSnapshot, ProjectSnapshotAssembly, ProjectSnapshotError};
13
14pub const IGNORE_FILE: &str = ".typkignore";
16
17pub type FilesystemProjectResource = ResourceKind<0>;
19
20#[allow(non_upper_case_globals)]
21impl ResourceKind<0> {
22 pub const VisitedEntries: Self = Self::new(0);
23 pub const SelectedFiles: Self = Self::new(1);
24 pub const RootPolicyBytes: Self = Self::new(2);
25 pub const SelectedFileBytes: Self = Self::new(3);
26 pub const TotalSelectedBytes: Self = Self::new(4);
27}
28
29pub type FilesystemProjectLimitError = LimitError<FilesystemProjectResource>;
31
32pub type FilesystemProjectLimits = Limits<FilesystemProjectResource>;
34
35impl Limits<FilesystemProjectResource> {
36 #[track_caller]
38 pub fn new(
39 visited_entries: u64,
40 selected_files: u64,
41 root_policy_bytes: u64,
42 selected_file_bytes: u64,
43 total_selected_bytes: u64,
44 ) -> Self {
45 Self::from_ceilings([
46 visited_entries,
47 selected_files,
48 root_policy_bytes,
49 selected_file_bytes,
50 total_selected_bytes,
51 0,
52 0,
53 ])
54 .assert_probe_resources([
55 FilesystemProjectResource::VisitedEntries,
56 FilesystemProjectResource::SelectedFiles,
57 FilesystemProjectResource::RootPolicyBytes,
58 FilesystemProjectResource::SelectedFileBytes,
59 FilesystemProjectResource::TotalSelectedBytes,
60 ])
61 }
62
63 pub const fn reference_v1() -> Self {
65 Self::from_ceilings([
66 1_000_000,
67 100_000,
68 1024 * 1024,
69 256 * 1024 * 1024,
70 2 * 1024 * 1024 * 1024,
71 0,
72 0,
73 ])
74 }
75
76 pub const fn visited_entries(&self) -> u64 {
77 self.ceilings[0]
78 }
79
80 pub const fn selected_files(&self) -> u64 {
81 self.ceilings[1]
82 }
83
84 pub const fn root_policy_bytes(&self) -> u64 {
85 self.ceilings[2]
86 }
87
88 pub const fn selected_file_bytes(&self) -> u64 {
89 self.ceilings[3]
90 }
91
92 pub const fn total_selected_bytes(&self) -> u64 {
93 self.ceilings[4]
94 }
95}
96
97#[derive(Debug, Clone, Copy, Eq, PartialEq)]
99#[non_exhaustive]
100pub enum FilesystemProjectEntryKind {
101 Socket,
102 Fifo,
103 BlockDevice,
104 CharacterDevice,
105 Unknown,
106}
107
108#[derive(Debug, Clone, Copy, Eq, PartialEq)]
110#[non_exhaustive]
111pub enum FilesystemProjectOperation {
112 InspectRootPolicy,
113 ReadRootPolicy,
114 SurveyEntry,
115 InspectSelectedFile,
116 ReadSelectedFile,
117}
118
119impl std::fmt::Display for FilesystemProjectOperation {
120 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 formatter.write_str(match self {
122 Self::InspectRootPolicy => "inspect root Project Ignore Policy",
123 Self::ReadRootPolicy => "read root Project Ignore Policy",
124 Self::SurveyEntry => "survey project entry",
125 Self::InspectSelectedFile => "inspect selected project file",
126 Self::ReadSelectedFile => "read selected project file",
127 })
128 }
129}
130
131#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
133#[non_exhaustive]
134pub enum FilesystemProjectIssue {
135 #[error("unsupported filesystem entry `{}`: aliases cannot become project files", path.display())]
136 Alias { path: PathBuf },
137 #[error("unsupported filesystem entry `{}` in the project", path.display())]
138 UnsupportedEntry {
139 path: PathBuf,
140 kind: FilesystemProjectEntryKind,
141 },
142 #[error("project path `{}` is not valid UTF-8", path.display())]
143 UnrepresentablePath { path: PathBuf },
144}
145
146impl FilesystemProjectIssue {
147 fn path(&self) -> &Path {
148 match self {
149 Self::Alias { path }
150 | Self::UnsupportedEntry { path, .. }
151 | Self::UnrepresentablePath { path } => path,
152 }
153 }
154
155 fn rank(&self) -> u8 {
156 match self {
157 Self::Alias { .. } => 0,
158 Self::UnsupportedEntry { .. } => 1,
159 Self::UnrepresentablePath { .. } => 2,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
166#[error(
167 "filesystem project survey found {} issue(s){}",
168 .issues.len(),
169 format_error_list(.issues.as_slice())
170)]
171pub struct FilesystemProjectSurveyError {
172 issues: Vec<FilesystemProjectIssue>,
173}
174
175impl FilesystemProjectSurveyError {
176 pub fn issues(&self) -> &[FilesystemProjectIssue] {
177 &self.issues
178 }
179}
180
181#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
183#[non_exhaustive]
184pub enum FilesystemProjectPolicyError {
185 #[error("the policy file is not valid UTF-8")]
186 NotUtf8,
187 #[error("line {line}: {message}")]
188 InvalidRule { line: usize, message: String },
189 #[error("{0}")]
190 Invalid(String),
191}
192
193#[derive(Debug, thiserror::Error)]
195#[non_exhaustive]
196pub enum FilesystemProjectReadError {
197 #[error("failed to {operation} `{}`: {source}", path.display())]
198 Io {
199 operation: FilesystemProjectOperation,
200 path: PathBuf,
201 #[source]
202 source: std::io::Error,
203 },
204 #[error("invalid Project Ignore Policy at `{}`: {source}", path.display())]
205 InvalidPolicy {
206 path: PathBuf,
207 source: FilesystemProjectPolicyError,
208 },
209 #[error(transparent)]
210 Survey(FilesystemProjectSurveyError),
211 #[error("filesystem project resource limit at {path:?}: {source}")]
212 Limit {
213 path: PathBuf,
214 source: FilesystemProjectLimitError,
215 },
216 #[error("selected filesystem entries do not form a Project Snapshot: {0}")]
217 Snapshot(#[source] ProjectSnapshotError),
218}
219
220impl FilesystemProjectReadError {
221 fn io(
222 operation: FilesystemProjectOperation,
223 path: impl Into<PathBuf>,
224 source: std::io::Error,
225 ) -> Self {
226 Self::Io {
227 operation,
228 path: path.into(),
229 source,
230 }
231 }
232
233 fn limit(path: impl Into<PathBuf>, source: FilesystemProjectLimitError) -> Self {
234 Self::Limit {
235 path: path.into(),
236 source,
237 }
238 }
239}
240
241pub fn read_filesystem_project(
248 root: impl AsRef<Path>,
249 entrypoint: impl Into<String>,
250 limits: FilesystemProjectLimits,
251) -> Result<ProjectSnapshot, FilesystemProjectReadError> {
252 let root = root.as_ref();
253 let policy_path = root.join(IGNORE_FILE);
254 let policy_file = read_policy(&policy_path, limits)?;
255 let policy = &policy_file.policy;
256
257 let mut visited_entries = 0u64;
258 let mut selected_files = u64::from(policy_file.bytes.is_some());
259 check_limit(
260 FilesystemProjectResource::SelectedFiles,
261 limits.selected_files(),
262 selected_files,
263 )
264 .map_err(|source| FilesystemProjectReadError::limit(&policy_path, source))?;
265 let mut declared_total = policy_file
266 .bytes
267 .as_ref()
268 .map_or(0, |bytes| bytes.len() as u64);
269 let mut selected = Vec::new();
270 let mut issues = Vec::new();
271 let mut deferred_limit = None;
272 let mut walk = walkdir::WalkDir::new(root).follow_links(false).into_iter();
273
274 while let Some(entry) = walk.next() {
275 let entry = entry.map_err(|error| {
276 let path = error.path().unwrap_or(root).to_owned();
277 let source = error
278 .into_io_error()
279 .unwrap_or_else(|| std::io::Error::other("filesystem traversal failed"));
280 FilesystemProjectReadError::io(FilesystemProjectOperation::SurveyEntry, path, source)
281 })?;
282 if entry.depth() == 0 {
283 continue;
284 }
285
286 visited_entries = checked_add(
287 visited_entries,
288 1,
289 FilesystemProjectResource::VisitedEntries,
290 )
291 .map_err(|source| FilesystemProjectReadError::limit(entry.path(), source))?;
292 check_limit(
293 FilesystemProjectResource::VisitedEntries,
294 limits.visited_entries(),
295 visited_entries,
296 )
297 .map_err(|source| FilesystemProjectReadError::limit(entry.path(), source))?;
298
299 let relative = entry
300 .path()
301 .strip_prefix(root)
302 .expect("walk remains beneath root");
303 let Some(path) = slash_path(relative) else {
304 issues.push(FilesystemProjectIssue::UnrepresentablePath {
305 path: entry.path().to_owned(),
306 });
307 if entry.file_type().is_dir() {
308 walk.skip_current_dir();
309 }
310 continue;
311 };
312
313 if path == IGNORE_FILE {
314 if entry.file_type().is_dir() {
315 walk.skip_current_dir();
316 }
317 continue;
318 }
319
320 let file_type = entry.file_type();
321 if file_type.is_dir() {
322 if policy.excludes_directory(&path) {
323 walk.skip_current_dir();
324 }
325 continue;
326 }
327 if policy.excludes_file(&path) {
328 continue;
329 }
330 if file_type.is_symlink() {
331 issues.push(FilesystemProjectIssue::Alias {
332 path: entry.path().to_owned(),
333 });
334 continue;
335 }
336 if !file_type.is_file() {
337 issues.push(FilesystemProjectIssue::UnsupportedEntry {
338 path: entry.path().to_owned(),
339 kind: UnsupportedEntry::of(&file_type).into(),
340 });
341 continue;
342 }
343
344 selected_files =
345 checked_add(selected_files, 1, FilesystemProjectResource::SelectedFiles)
346 .map_err(|source| FilesystemProjectReadError::limit(entry.path(), source))?;
347 if let Err(source) = check_limit(
348 FilesystemProjectResource::SelectedFiles,
349 limits.selected_files(),
350 selected_files,
351 ) {
352 deferred_limit
353 .get_or_insert_with(|| FilesystemProjectReadError::limit(entry.path(), source));
354 continue;
355 }
356
357 let metadata = entry.metadata().map_err(|error| {
358 let path = error.path().unwrap_or(entry.path()).to_owned();
359 let source = error.into_io_error().unwrap_or_else(|| {
360 std::io::Error::other("failed to inspect selected project file")
361 });
362 FilesystemProjectReadError::io(
363 FilesystemProjectOperation::InspectSelectedFile,
364 path,
365 source,
366 )
367 })?;
368 let declared = metadata.len();
369 if let Err(source) = check_limit(
370 FilesystemProjectResource::SelectedFileBytes,
371 limits.selected_file_bytes(),
372 declared,
373 ) {
374 deferred_limit
375 .get_or_insert_with(|| FilesystemProjectReadError::limit(entry.path(), source));
376 continue;
377 }
378 declared_total = checked_add(
379 declared_total,
380 declared,
381 FilesystemProjectResource::TotalSelectedBytes,
382 )
383 .map_err(|source| FilesystemProjectReadError::limit(entry.path(), source))?;
384 if let Err(source) = check_limit(
385 FilesystemProjectResource::TotalSelectedBytes,
386 limits.total_selected_bytes(),
387 declared_total,
388 ) {
389 deferred_limit
390 .get_or_insert_with(|| FilesystemProjectReadError::limit(entry.path(), source));
391 continue;
392 }
393 selected.push((path, entry.path().to_owned()));
394 }
395
396 if !issues.is_empty() {
397 issues.sort_by(|left, right| {
398 left.path()
399 .cmp(right.path())
400 .then_with(|| left.rank().cmp(&right.rank()))
401 });
402 return Err(FilesystemProjectReadError::Survey(
403 FilesystemProjectSurveyError { issues },
404 ));
405 }
406 if let Some(error) = deferred_limit {
407 return Err(error);
408 }
409
410 selected.sort_by(|(left, _), (right, _)| left.cmp(right));
411 let mut entries = Vec::with_capacity(selected.len() + usize::from(policy_file.bytes.is_some()));
412 let mut actual_total = 0u64;
413 if let Some(bytes) = policy_file.bytes {
414 actual_total = bytes.len() as u64;
415 entries.push((IGNORE_FILE.to_owned(), bytes));
416 }
417 for (path, source) in selected {
418 let remaining = limits.total_selected_bytes() - actual_total;
419 let bytes = read_bounded(
420 root,
421 &source,
422 &[
423 (
424 FilesystemProjectResource::SelectedFileBytes,
425 limits.selected_file_bytes(),
426 limits.selected_file_bytes(),
427 0,
428 ),
429 (
430 FilesystemProjectResource::TotalSelectedBytes,
431 remaining,
432 limits.total_selected_bytes(),
433 actual_total,
434 ),
435 ],
436 FilesystemProjectOperation::ReadSelectedFile,
437 )?;
438 actual_total = checked_add(
439 actual_total,
440 bytes.len() as u64,
441 FilesystemProjectResource::TotalSelectedBytes,
442 )
443 .map_err(|source_error| FilesystemProjectReadError::limit(&source, source_error))?;
444 entries.push((path, bytes));
445 }
446
447 ProjectSnapshotAssembly::new(entrypoint)
448 .assemble(entries)
449 .map_err(FilesystemProjectReadError::Snapshot)
450}
451
452struct ReadPolicy {
453 policy: ProjectIgnorePolicy,
454 bytes: Option<Vec<u8>>,
455}
456
457fn read_policy(
458 path: &Path,
459 limits: FilesystemProjectLimits,
460) -> Result<ReadPolicy, FilesystemProjectReadError> {
461 let metadata = match std::fs::symlink_metadata(path) {
462 Ok(metadata) => metadata,
463 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
464 return Ok(ReadPolicy {
465 policy: ProjectIgnorePolicy::built_in(),
466 bytes: None,
467 });
468 }
469 Err(error) => {
470 return Err(FilesystemProjectReadError::io(
471 FilesystemProjectOperation::InspectRootPolicy,
472 path,
473 error,
474 ));
475 }
476 };
477 let file_type = metadata.file_type();
478 if file_type.is_symlink() {
479 return Err(FilesystemProjectReadError::Survey(
480 FilesystemProjectSurveyError {
481 issues: vec![FilesystemProjectIssue::Alias {
482 path: path.to_owned(),
483 }],
484 },
485 ));
486 }
487 if !file_type.is_file() {
488 return Err(FilesystemProjectReadError::Survey(
489 FilesystemProjectSurveyError {
490 issues: vec![FilesystemProjectIssue::UnsupportedEntry {
491 path: path.to_owned(),
492 kind: UnsupportedEntry::of(&file_type).into(),
493 }],
494 },
495 ));
496 }
497
498 check_limit(
499 FilesystemProjectResource::VisitedEntries,
500 limits.visited_entries(),
501 1,
502 )
503 .and_then(|_| {
504 check_limit(
505 FilesystemProjectResource::SelectedFiles,
506 limits.selected_files(),
507 1,
508 )
509 })
510 .map_err(|source| FilesystemProjectReadError::limit(path, source))?;
511 check_limit(
512 FilesystemProjectResource::RootPolicyBytes,
513 limits.root_policy_bytes(),
514 metadata.len(),
515 )
516 .and_then(|_| {
517 check_limit(
518 FilesystemProjectResource::SelectedFileBytes,
519 limits.selected_file_bytes(),
520 metadata.len(),
521 )
522 })
523 .and_then(|_| {
524 check_limit(
525 FilesystemProjectResource::TotalSelectedBytes,
526 limits.total_selected_bytes(),
527 metadata.len(),
528 )
529 })
530 .map_err(|source| FilesystemProjectReadError::limit(path, source))?;
531 let bytes = read_bounded(
532 path.parent().expect("the policy path is beneath a root"),
533 path,
534 &[
535 (
536 FilesystemProjectResource::RootPolicyBytes,
537 limits.root_policy_bytes(),
538 limits.root_policy_bytes(),
539 0,
540 ),
541 (
542 FilesystemProjectResource::SelectedFileBytes,
543 limits.selected_file_bytes(),
544 limits.selected_file_bytes(),
545 0,
546 ),
547 (
548 FilesystemProjectResource::TotalSelectedBytes,
549 limits.total_selected_bytes(),
550 limits.total_selected_bytes(),
551 0,
552 ),
553 ],
554 FilesystemProjectOperation::ReadRootPolicy,
555 )?;
556 let policy = ProjectIgnorePolicy::from_bytes(&bytes).map_err(|source| {
557 FilesystemProjectReadError::InvalidPolicy {
558 path: path.to_owned(),
559 source,
560 }
561 })?;
562 Ok(ReadPolicy {
563 policy,
564 bytes: Some(bytes),
565 })
566}
567
568fn read_bounded(
569 root: &Path,
570 path: &Path,
571 ceilings: &[(FilesystemProjectResource, u64, u64, u64)],
572 operation: FilesystemProjectOperation,
573) -> Result<Vec<u8>, FilesystemProjectReadError> {
574 let probe_ceiling = ceilings
575 .iter()
576 .map(|(_, allowance, _, _)| *allowance)
577 .min()
578 .expect("a bounded read has at least one ceiling");
579 let mut file =
580 fs_traversal::open_without_following(&ProjectTraversal { operation }, root, path)?;
581 let mut bytes = Vec::new();
582 file.by_ref()
583 .take(probe_ceiling + 1)
584 .read_to_end(&mut bytes)
585 .map_err(|error| FilesystemProjectReadError::io(operation, path, error))?;
586 let observed = u64::try_from(bytes.len()).map_err(|_| {
587 FilesystemProjectReadError::limit(
588 path,
589 FilesystemProjectLimitError::AccountingOverflow {
590 resource: ceilings[0].0,
591 },
592 )
593 })?;
594 for (resource, _, ceiling, base) in ceilings {
595 let cumulative = checked_add(*base, observed, *resource)
596 .map_err(|source| FilesystemProjectReadError::limit(path, source))?;
597 check_limit(*resource, *ceiling, cumulative)
598 .map_err(|source| FilesystemProjectReadError::limit(path, source))?;
599 }
600 Ok(bytes)
601}
602
603struct ProjectTraversal {
605 operation: FilesystemProjectOperation,
606}
607
608impl TraversalPolicy for ProjectTraversal {
609 type Error = FilesystemProjectReadError;
610
611 const ROOT_INVARIANT: &'static str = "a selected path remains beneath its project root";
612
613 fn io(&self, path: &Path, source: std::io::Error) -> Self::Error {
614 FilesystemProjectReadError::io(self.operation, path, source)
615 }
616
617 fn alias(&self, path: &Path) -> Self::Error {
618 alias_error(path)
619 }
620
621 fn unsupported_entry(&self, path: &Path, entry: UnsupportedEntry) -> Self::Error {
622 FilesystemProjectReadError::Survey(FilesystemProjectSurveyError {
623 issues: vec![FilesystemProjectIssue::UnsupportedEntry {
624 path: path.to_owned(),
625 kind: entry.into(),
626 }],
627 })
628 }
629}
630
631impl From<UnsupportedEntry> for FilesystemProjectEntryKind {
632 fn from(entry: UnsupportedEntry) -> Self {
633 match entry {
634 UnsupportedEntry::Socket => Self::Socket,
635 UnsupportedEntry::Fifo => Self::Fifo,
636 UnsupportedEntry::BlockDevice => Self::BlockDevice,
637 UnsupportedEntry::CharacterDevice => Self::CharacterDevice,
638 UnsupportedEntry::Unknown => Self::Unknown,
639 }
640 }
641}
642
643fn alias_error(path: &Path) -> FilesystemProjectReadError {
644 FilesystemProjectReadError::Survey(FilesystemProjectSurveyError {
645 issues: vec![FilesystemProjectIssue::Alias {
646 path: path.to_owned(),
647 }],
648 })
649}
650
651fn slash_path(path: &Path) -> Option<String> {
652 path.components()
653 .map(|component| component.as_os_str().to_str())
654 .collect::<Option<Vec<_>>>()
655 .map(|components| components.join("/"))
656}
657
658fn checked_add(
659 total: u64,
660 value: u64,
661 resource: FilesystemProjectResource,
662) -> Result<u64, FilesystemProjectLimitError> {
663 total
664 .checked_add(value)
665 .ok_or(FilesystemProjectLimitError::AccountingOverflow { resource })
666}
667
668fn check_limit(
669 resource: FilesystemProjectResource,
670 ceiling: u64,
671 observed: u64,
672) -> Result<(), FilesystemProjectLimitError> {
673 if observed > ceiling {
674 return Err(FilesystemProjectLimitError::exceeded(resource, ceiling));
675 }
676 Ok(())
677}
678
679struct ProjectIgnorePolicy {
680 rules: Gitignore,
681}
682
683impl ProjectIgnorePolicy {
684 fn built_in() -> Self {
685 Self {
686 rules: Gitignore::empty(),
687 }
688 }
689
690 fn from_bytes(bytes: &[u8]) -> Result<Self, FilesystemProjectPolicyError> {
691 let contents =
692 std::str::from_utf8(bytes).map_err(|_| FilesystemProjectPolicyError::NotUtf8)?;
693 let mut builder = GitignoreBuilder::new(".");
694 for (index, line) in contents.lines().enumerate() {
695 let line = if index == 0 {
696 line.trim_start_matches('\u{feff}')
697 } else {
698 line
699 };
700 builder.add_line(None, line).map_err(|error| {
701 FilesystemProjectPolicyError::InvalidRule {
702 line: index + 1,
703 message: error.to_string(),
704 }
705 })?;
706 }
707 let rules = builder
708 .build()
709 .map_err(|error| FilesystemProjectPolicyError::Invalid(error.to_string()))?;
710 Ok(Self { rules })
711 }
712
713 fn excludes_file(&self, path: &str) -> bool {
714 self.excludes(path, false)
715 }
716
717 fn excludes_directory(&self, path: &str) -> bool {
718 self.excludes(path, true)
719 }
720
721 fn excludes(&self, path: &str, is_directory: bool) -> bool {
722 if path == IGNORE_FILE {
723 return false;
724 }
725 if names_pack_path(path) {
726 return true;
727 }
728 let mut ancestor_end = 0;
729 while let Some(offset) = path[ancestor_end..].find('/') {
730 ancestor_end += offset;
731 if self.rules.matched(&path[..ancestor_end], true).is_ignore() {
732 return true;
733 }
734 ancestor_end += 1;
735 }
736 self.rules.matched(path, is_directory).is_ignore()
737 }
738}
739
740#[cfg(all(test, unix))]
741mod tests {
742 use std::os::unix::fs::symlink;
743
744 use super::*;
745
746 #[test]
747 fn bounded_reads_do_not_follow_an_alias_created_after_survey() {
748 let directory = tempfile::tempdir().unwrap();
749 let outside = directory.path().join("outside");
750 let selected = directory.path().join("selected");
751 std::fs::write(&outside, b"outside").unwrap();
752 symlink(&outside, &selected).unwrap();
753
754 let error = read_bounded(
755 directory.path(),
756 &selected,
757 &[(FilesystemProjectResource::SelectedFileBytes, 16, 16, 0)],
758 FilesystemProjectOperation::ReadSelectedFile,
759 )
760 .unwrap_err();
761
762 assert!(matches!(
763 error,
764 FilesystemProjectReadError::Survey(ref survey)
765 if matches!(survey.issues(), [FilesystemProjectIssue::Alias { path }] if path == &selected)
766 ));
767 }
768
769 #[test]
770 fn bounded_reads_do_not_follow_an_ancestor_alias_created_after_survey() {
771 let directory = tempfile::tempdir().unwrap();
772 let root = directory.path().join("project");
773 let ancestor = root.join("nested");
774 let selected = ancestor.join("selected");
775 let outside = directory.path().join("outside");
776 std::fs::create_dir_all(&ancestor).unwrap();
777 std::fs::create_dir_all(&outside).unwrap();
778 std::fs::write(&selected, b"surveyed").unwrap();
779 std::fs::write(outside.join("selected"), b"outside").unwrap();
780 std::fs::remove_dir_all(&ancestor).unwrap();
781 symlink(&outside, &ancestor).unwrap();
782
783 let error = read_bounded(
784 &root,
785 &selected,
786 &[(FilesystemProjectResource::SelectedFileBytes, 16, 16, 0)],
787 FilesystemProjectOperation::ReadSelectedFile,
788 )
789 .unwrap_err();
790
791 assert!(matches!(
792 error,
793 FilesystemProjectReadError::Survey(ref survey)
794 if matches!(survey.issues(), [FilesystemProjectIssue::Alias { path }] if path == &ancestor)
795 ));
796 }
797
798 #[test]
799 fn bounded_reads_do_not_block_on_a_fifo_created_after_survey() {
800 use std::ffi::CString;
801 use std::os::unix::ffi::OsStrExt;
802
803 let directory = tempfile::tempdir().unwrap();
804 let selected = directory.path().join("selected");
805 std::fs::write(&selected, b"surveyed").unwrap();
806 std::fs::remove_file(&selected).unwrap();
807 let path = CString::new(selected.as_os_str().as_bytes()).unwrap();
808 assert_eq!(unsafe { libc::mkfifo(path.as_ptr(), 0o600) }, 0);
810
811 let error = read_bounded(
812 directory.path(),
813 &selected,
814 &[(FilesystemProjectResource::SelectedFileBytes, 16, 16, 0)],
815 FilesystemProjectOperation::ReadSelectedFile,
816 )
817 .unwrap_err();
818
819 assert!(matches!(
820 error,
821 FilesystemProjectReadError::Survey(ref survey)
822 if matches!(survey.issues(), [FilesystemProjectIssue::UnsupportedEntry {
823 path,
824 kind: FilesystemProjectEntryKind::Fifo,
825 }] if path == &selected)
826 ));
827 }
828}