1use std::collections::BTreeMap;
2use std::fmt;
3use std::io::Read;
4use std::path::{Component, Path, PathBuf};
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use crate::validation::{MAX_FORMAT_ID_BYTES, valid_nonempty_text};
8use crate::{Error, SourceId};
9
10const MAX_REFERENCED_FILES: usize = 4_096;
13
14const MAX_REFERENCED_BYTES: u64 = 64 << 20;
16
17const MAX_REFERENCED_DEPTH: usize = 64;
19
20const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
21
22#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct FormatId(Box<str>);
25
26impl FormatId {
27 pub fn new(id: impl Into<String>) -> Result<Self, Error> {
29 let id = id.into();
30 if !valid_format_id(&id) {
31 return Err(Error::new(
32 &crate::codes::REQUEST_FORMAT_INVALID_ID,
33 "a format ID must be bounded lower case ASCII segments separated by single hyphens",
34 ));
35 }
36 Ok(Self(id.into_boxed_str()))
37 }
38
39 #[must_use]
40 pub fn as_str(&self) -> &str {
41 &self.0
42 }
43}
44
45impl fmt::Display for FormatId {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 formatter.write_str(&self.0)
48 }
49}
50
51fn valid_format_id(id: &str) -> bool {
52 if id.is_empty() || id.len() > MAX_FORMAT_ID_BYTES {
53 return false;
54 }
55 let bytes = id.as_bytes();
56 if !bytes[0].is_ascii_lowercase() || bytes.last() == Some(&b'-') {
57 return false;
58 }
59 let mut previous_hyphen = false;
60 for byte in bytes {
61 if *byte == b'-' {
62 if previous_hyphen {
63 return false;
64 }
65 previous_hyphen = true;
66 } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
67 previous_hyphen = false;
68 } else {
69 return false;
70 }
71 }
72 true
73}
74
75#[derive(Debug)]
76struct SourceBufferData {
77 id: SourceId,
78 name: Box<str>,
79 bytes: Arc<[u8]>,
80 directory: Box<[Box<str>]>,
84}
85
86#[derive(Clone, Debug)]
88pub struct SourceBuffer(Arc<SourceBufferData>);
89
90impl SourceBuffer {
91 fn new(
92 id: SourceId,
93 name: impl Into<String>,
94 bytes: Arc<[u8]>,
95 directory: Vec<String>,
96 ) -> Self {
97 Self(Arc::new(SourceBufferData {
98 id,
99 name: name.into().into_boxed_str(),
100 bytes,
101 directory: directory.into_iter().map(String::into_boxed_str).collect(),
102 }))
103 }
104
105 #[must_use]
106 pub fn id(&self) -> &SourceId {
107 &self.0.id
108 }
109
110 #[must_use]
111 pub fn name(&self) -> &str {
112 &self.0.name
113 }
114
115 #[must_use]
118 pub fn bytes(&self) -> &[u8] {
119 &self.0.bytes
120 }
121
122 #[must_use]
123 pub fn shared_bytes(&self) -> Arc<[u8]> {
124 Arc::clone(&self.0.bytes)
125 }
126
127 #[must_use]
129 pub fn has_utf8_bom(&self) -> bool {
130 self.0.bytes.starts_with(&UTF8_BOM)
131 }
132
133 #[must_use]
137 pub fn content_bytes(&self) -> &[u8] {
138 let bytes: &[u8] = &self.0.bytes;
139 if bytes.starts_with(&UTF8_BOM) {
140 &bytes[UTF8_BOM.len()..]
141 } else {
142 bytes
143 }
144 }
145
146 pub fn directory_segments(&self) -> impl Iterator<Item = &str> {
151 self.0.directory.iter().map(AsRef::as_ref)
152 }
153}
154
155#[derive(Debug)]
168struct FileAcquisition {
169 root_display: PathBuf,
170 selected: bool,
173 state: Mutex<AcquisitionState>,
174}
175
176#[derive(Debug, Default)]
177struct AcquisitionState {
178 root: Option<platform::RootHandle>,
179 cache: BTreeMap<String, SourceBuffer>,
180 listed: Option<Vec<crate::ArtifactPath>>,
185 files: usize,
186 bytes: u64,
187}
188
189impl AcquisitionState {
190 fn pinned_root(&mut self, root_display: &Path) -> Result<&platform::RootHandle, Error> {
193 if self.root.is_none() {
194 let root = platform::open_root(root_display)
195 .map_err(|cause| open_error(&crate::codes::READ_IO_OPEN, root_display, cause))?;
196 self.root = Some(root);
197 }
198 Ok(self.root.as_ref().expect("pinned above"))
199 }
200}
201
202#[derive(Debug)]
203enum SourceProvider {
204 Memory {
205 primary: SourceBuffer,
206 named: BTreeMap<String, SourceBuffer>,
207 },
208 File {
209 primary: SourceBuffer,
210 acquisition: FileAcquisition,
211 },
212 Directory {
213 acquisition: FileAcquisition,
214 },
215}
216
217pub const PRIMARY_SOURCE_ID: &str = "/input";
225
226#[derive(Clone)]
233pub struct Source {
234 name: Arc<str>,
235 provider: Arc<SourceProvider>,
236 declared_format: Option<FormatId>,
237}
238
239impl Source {
240 pub fn open(path: impl Into<PathBuf>) -> Result<Self, Error> {
242 let path = path.into();
243 if path.as_os_str().is_empty() {
244 return Err(Error::new(
245 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
246 "source path cannot be empty",
247 ));
248 }
249 let name: Arc<str> = path.to_string_lossy().into_owned().into();
250
251 let file = match platform::open_no_follow(&path) {
256 Ok(file) => file,
257 Err(error) if platform::is_symlink_refusal(&error) => {
258 return Err(Error::new(
259 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
260 format!("source `{}` is a symbolic link", path.display()),
261 ));
262 }
263 Err(error) if platform::is_directory_open_failure(&error, &path) => {
264 return Self::open_directory(name, &path);
265 }
266 Err(cause) => return Err(open_error(&crate::codes::READ_IO_OPEN, &path, cause)),
267 };
268 let metadata = file
269 .metadata()
270 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, &path, cause))?;
271 if metadata.is_dir() {
272 drop(file);
273 return Self::open_directory(name, &path);
274 }
275 let bytes = read_open_file(file, &name, u64::MAX)?;
276 let root_display = canonical_parent(&path)?;
277 let primary = SourceBuffer::new(
278 SourceId::new(PRIMARY_SOURCE_ID)?,
279 name.to_string(),
280 bytes,
281 Vec::new(),
282 );
283 Ok(Self {
284 name,
285 provider: Arc::new(SourceProvider::File {
286 primary,
287 acquisition: FileAcquisition {
288 root_display,
289 selected: false,
290 state: Mutex::new(AcquisitionState::default()),
291 },
292 }),
293 declared_format: None,
294 })
295 }
296
297 fn open_directory(name: Arc<str>, path: &Path) -> Result<Self, Error> {
298 let root_display = std::fs::canonicalize(path)
299 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, path, cause))?;
300 Ok(Self {
301 name,
302 provider: Arc::new(SourceProvider::Directory {
303 acquisition: FileAcquisition {
304 root_display,
305 selected: false,
306 state: Mutex::new(AcquisitionState::default()),
307 },
308 }),
309 declared_format: None,
310 })
311 }
312
313 pub fn from_bytes(name: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Result<Self, Error> {
317 let name = name.into();
318 if !valid_nonempty_text(&name) {
319 return Err(Error::new(
320 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
321 "an in-memory source requires a nonempty bounded name",
322 ));
323 }
324 let primary = SourceBuffer::new(
325 SourceId::new(PRIMARY_SOURCE_ID)?,
326 name.clone(),
327 bytes.into(),
328 Vec::new(),
329 );
330 Ok(Self {
331 name: name.into(),
332 provider: Arc::new(SourceProvider::Memory {
333 primary,
334 named: BTreeMap::new(),
335 }),
336 declared_format: None,
337 })
338 }
339
340 pub fn with_named_buffer(
345 self,
346 name: impl Into<String>,
347 bytes: impl Into<Arc<[u8]>>,
348 ) -> Result<Self, Error> {
349 let name = name.into();
350 let segments = resolve_segments(&[], &name)?;
351 let key = segments.join("/");
352 let mut provider = Arc::try_unwrap(self.provider).map_err(|_| {
353 Error::new(
354 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
355 "named buffers are supplied while constructing a source, before it is shared",
356 )
357 })?;
358 let SourceProvider::Memory { named, .. } = &mut provider else {
359 return Err(Error::new(
360 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
361 "named buffers belong to in-memory sources; a file source acquires referenced files beneath its root",
362 ));
363 };
364 let directory = segments[..segments.len() - 1].to_vec();
365 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes.into(), directory);
366 named.insert(key, buffer);
367 Ok(Self {
368 name: self.name,
369 provider: Arc::new(provider),
370 declared_format: self.declared_format,
371 })
372 }
373
374 pub fn with_acquisition_root(self, root: impl Into<PathBuf>) -> Result<Self, Error> {
378 let requested = root.into();
379 let SourceProvider::File {
380 primary,
381 acquisition,
382 } = &*self.provider
383 else {
384 return Err(Error::new(
385 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
386 "an acquisition root applies to a file source",
387 ));
388 };
389 let canonical = std::fs::canonicalize(&requested)
390 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, &requested, cause))?;
391 let Ok(remainder) = acquisition.root_display.strip_prefix(&canonical) else {
392 return Err(Error::new(
393 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
394 format!(
395 "the case file directory {} is outside the requested acquisition root {}",
396 acquisition.root_display.display(),
397 canonical.display()
398 ),
399 ));
400 };
401 let mut directory = Vec::new();
402 for component in remainder.components() {
403 let Component::Normal(segment) = component else {
404 return Err(Error::new(
405 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
406 "the acquisition root does not resolve to a plain prefix of the case directory",
407 ));
408 };
409 let Some(segment) = segment.to_str().filter(|text| plain_segment(text)) else {
410 return Err(Error::new(
411 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
412 "the acquisition root does not resolve to a plain prefix of the case directory",
413 ));
414 };
415 directory.push(segment.to_owned());
416 }
417 let primary = SourceBuffer::new(
418 primary.id().clone(),
419 primary.name().to_owned(),
420 primary.shared_bytes(),
421 directory,
422 );
423 Ok(Self {
424 name: self.name,
425 provider: Arc::new(SourceProvider::File {
426 primary,
427 acquisition: FileAcquisition {
428 root_display: canonical,
429 selected: true,
430 state: Mutex::new(AcquisitionState::default()),
431 },
432 }),
433 declared_format: self.declared_format,
434 })
435 }
436
437 #[must_use]
439 pub fn with_format(mut self, format: FormatId) -> Self {
440 self.declared_format = Some(format);
441 self
442 }
443
444 #[must_use]
445 pub fn name(&self) -> &str {
446 &self.name
447 }
448
449 #[must_use]
450 pub const fn format(&self) -> Option<&FormatId> {
451 self.declared_format.as_ref()
452 }
453
454 #[must_use]
455 pub fn is_directory(&self) -> bool {
456 matches!(&*self.provider, SourceProvider::Directory { .. })
457 }
458
459 pub fn primary_buffer(&self) -> Result<SourceBuffer, Error> {
461 match &*self.provider {
462 SourceProvider::Memory { primary, .. } | SourceProvider::File { primary, .. } => {
463 Ok(primary.clone())
464 }
465 SourceProvider::Directory { .. } => Err(Error::new(
466 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
467 "a directory source has no implicit primary buffer",
468 )
469 .with_source(self.clone())),
470 }
471 }
472
473 pub fn buffer(&self, name: &crate::ArtifactPath) -> Result<SourceBuffer, Error> {
476 let SourceProvider::Directory { acquisition } = &*self.provider else {
477 return Err(Error::new(
478 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
479 "named child buffers require a directory source",
480 )
481 .with_source(self.clone()));
482 };
483 let segments = resolve_segments(&[], name.as_str())
484 .map_err(|error| error.with_source(self.clone()))?;
485 acquisition
486 .acquire(&segments)
487 .map_err(|error| error.with_source(self.clone()))
488 }
489
490 pub fn root_buffer(&self, name: &str) -> Result<SourceBuffer, Error> {
495 match &*self.provider {
496 SourceProvider::Memory { named, .. } => {
497 let segments = resolve_segments(&[], name)?;
498 let key = segments.join("/");
499 named.get(&key).cloned().ok_or_else(|| {
500 Error::new(
501 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
502 format!(
503 "referenced buffer `{key}` was not supplied to this in-memory source"
504 ),
505 )
506 .with_source(self.clone())
507 })
508 }
509 SourceProvider::File { acquisition, .. }
510 | SourceProvider::Directory { acquisition } => {
511 let segments = resolve_segments(&[], name)?;
512 acquisition
513 .acquire(&segments)
514 .map_err(|error| error.with_source(self.clone()))
515 }
516 }
517 }
518
519 #[must_use]
525 pub fn selected_acquisition_root(&self) -> Option<&Path> {
526 match &*self.provider {
527 SourceProvider::Memory { .. } | SourceProvider::Directory { .. } => None,
528 SourceProvider::File { acquisition, .. } => acquisition
529 .selected
530 .then_some(acquisition.root_display.as_path()),
531 }
532 }
533
534 pub fn referenced_buffer(
539 &self,
540 referrer: &SourceBuffer,
541 name: &str,
542 ) -> Result<SourceBuffer, Error> {
543 let referrer_directory: Vec<&str> = referrer.directory_segments().collect();
544 match &*self.provider {
545 SourceProvider::Memory { named, .. } => {
546 let segments = resolve_segments(&referrer_directory, name)?;
547 let key = segments.join("/");
548 named.get(&key).cloned().ok_or_else(|| {
549 Error::new(
550 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
551 format!(
552 "referenced buffer `{key}` was not supplied to this in-memory source"
553 ),
554 )
555 .with_source(self.clone())
556 })
557 }
558 SourceProvider::File { acquisition, .. }
559 | SourceProvider::Directory { acquisition } => {
560 let segments = match absolute_to_root_relative(&acquisition.root_display, name) {
561 Some(root_relative) => root_relative?,
562 None => resolve_segments(&referrer_directory, name)?,
563 };
564 acquisition
565 .acquire(&segments)
566 .map_err(|error| error.with_source(self.clone()))
567 }
568 }
569 }
570
571 #[allow(clippy::too_many_lines)] pub fn entry_names(&self) -> Result<Vec<crate::ArtifactPath>, Error> {
578 match &*self.provider {
579 SourceProvider::Memory { named, .. } => named
580 .keys()
581 .map(|name| crate::ArtifactPath::new(name.clone()))
582 .collect(),
583 SourceProvider::File { .. } => Err(Error::new(
584 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
585 "entry listing requires a directory source",
586 )
587 .with_source(self.clone())),
588 SourceProvider::Directory { acquisition } => {
589 struct Frame<Handle> {
603 prefix: Vec<String>,
604 directory: Handle,
605 subdirectories: Vec<String>,
607 }
608
609 let mut state = acquisition.lock();
610 if let Some(listed) = &state.listed {
611 return Ok(listed.clone());
612 }
613 let root = state.pinned_root(&acquisition.root_display)?;
614 let budget_refusal = || {
615 Error::new(
616 &crate::codes::READ_IO_REFERENCE_BUDGET,
617 format!(
618 "the source directory holds more than {MAX_REFERENCED_FILES} entries"
619 ),
620 )
621 };
622 let root_handle = root.duplicate_handle().map_err(|cause| {
623 Error::new(
624 &crate::codes::READ_IO_METADATA,
625 "cannot list the source directory root",
626 )
627 .with_cause(cause)
628 })?;
629 let mut names = Vec::new();
630 let mut undescended = 0usize;
633 let mut frames = Vec::new();
634 let mut arriving = Some((Vec::<String>::new(), root_handle));
635 while let Some((prefix, directory)) = arriving.take() {
636 let allowance = MAX_REFERENCED_FILES
637 .checked_sub(names.len() + undescended)
638 .filter(|allowance| *allowance > 0)
639 .ok_or_else(budget_refusal)?;
640 let entries =
641 platform::list_entries(&directory, allowance).map_err(|cause| {
642 if platform::is_entry_budget(&cause) {
643 budget_refusal()
644 } else {
645 listing_error(&prefix.join("/"), cause)
646 }
647 })?;
648 let mut subdirectories = Vec::new();
649 for (name, is_directory) in entries {
650 if names.len() + undescended + subdirectories.len() >= MAX_REFERENCED_FILES
651 {
652 return Err(budget_refusal());
653 }
654 if is_directory {
655 if prefix.len() + 1 >= MAX_REFERENCED_DEPTH {
656 return Err(Error::new(
657 &crate::codes::READ_IO_REFERENCE_BUDGET,
658 format!(
659 "the source directory nests more than {MAX_REFERENCED_DEPTH} levels deep"
660 ),
661 ));
662 }
663 subdirectories.push(name);
664 } else {
665 let mut child = prefix.clone();
666 child.push(name);
667 names.push(crate::ArtifactPath::new(child.join("/"))?);
668 }
669 }
670 undescended += subdirectories.len();
671 frames.push(Frame {
672 prefix,
673 directory,
674 subdirectories,
675 });
676
677 while let Some(frame) = frames.last_mut() {
682 let Some(name) = frame.subdirectories.pop() else {
683 frames.pop();
684 continue;
685 };
686 undescended -= 1;
687 let mut child = frame.prefix.clone();
688 let handle = platform::open_child_directory(&frame.directory, &name)
689 .map_err(|cause| {
690 child.push(name.clone());
691 listing_error(&child.join("/"), cause)
692 })?;
693 child.push(name);
694 arriving = Some((child, handle));
695 break;
696 }
697 }
698 names.sort();
699 state.listed = Some(names.clone());
700 Ok(names)
701 }
702 }
703 }
704
705 #[must_use]
707 pub fn acquired_buffers(&self) -> Vec<SourceBuffer> {
708 match &*self.provider {
709 SourceProvider::Memory { primary, named } => {
710 let mut buffers = vec![primary.clone()];
711 buffers.extend(named.values().cloned());
712 buffers
713 }
714 SourceProvider::File {
715 primary,
716 acquisition,
717 } => {
718 let mut buffers = vec![primary.clone()];
719 buffers.extend(acquisition.lock().cache.values().cloned());
720 buffers
721 }
722 SourceProvider::Directory { acquisition } => {
723 acquisition.lock().cache.values().cloned().collect()
724 }
725 }
726 }
727}
728
729impl fmt::Debug for Source {
730 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
731 formatter
732 .debug_struct("Source")
733 .field("name", &self.name)
734 .field("is_directory", &self.is_directory())
735 .field("declared_format", &self.declared_format)
736 .field("acquired_buffer_count", &self.acquired_buffers().len())
737 .finish_non_exhaustive()
738 }
739}
740
741impl FileAcquisition {
742 fn lock(&self) -> MutexGuard<'_, AcquisitionState> {
743 self.state
744 .lock()
745 .unwrap_or_else(std::sync::PoisonError::into_inner)
746 }
747
748 fn acquire(&self, segments: &[String]) -> Result<SourceBuffer, Error> {
755 let key = segments.join("/");
756 let mut state = self.lock();
757 if let Some(buffer) = state.cache.get(&key) {
758 return Ok(buffer.clone());
759 }
760 if state.files >= MAX_REFERENCED_FILES {
761 return Err(Error::new(
762 &crate::codes::READ_IO_REFERENCE_BUDGET,
763 format!("this source already acquired {MAX_REFERENCED_FILES} referenced files"),
764 ));
765 }
766 let remaining = MAX_REFERENCED_BYTES.saturating_sub(state.bytes);
767 let root = state.pinned_root(&self.root_display)?;
768 let file = root.open_beneath(segments).map_err(|error| {
769 if platform::is_symlink_refusal(&error) {
770 Error::new(
771 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
772 format!("referenced file `{key}` crosses a symbolic link"),
773 )
774 } else {
775 Error::new(
776 &crate::codes::READ_IO_OPEN,
777 format!("cannot open referenced file `{key}`"),
778 )
779 .with_cause(error)
780 }
781 })?;
782 let bytes = read_open_file(file, &key, remaining)?;
783 let directory = segments[..segments.len() - 1].to_vec();
784 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes, directory);
785 state.files += 1;
786 state.bytes += buffer.bytes().len() as u64;
787 state.cache.insert(key, buffer.clone());
788 Ok(buffer)
789 }
790}
791
792fn plain_segment(segment: &str) -> bool {
798 if segment.is_empty()
799 || segment == "."
800 || segment == ".."
801 || segment.contains(['/', '\\', '\0', ':'])
802 {
803 return false;
804 }
805 let mut components = Path::new(segment).components();
806 matches!(components.next(), Some(Component::Normal(text)) if text == std::ffi::OsStr::new(segment))
807 && components.next().is_none()
808}
809
810fn resolve_segments(referrer_directory: &[&str], name: &str) -> Result<Vec<String>, Error> {
817 if name.is_empty()
818 || name.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
819 || name.contains('\0')
820 || name.starts_with(['/', '\\'])
821 {
822 return Err(Error::new(
823 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
824 "a referenced name must be a nonempty bounded relative path",
825 ));
826 }
827 let mut segments: Vec<String> = referrer_directory
828 .iter()
829 .map(|segment| (*segment).to_owned())
830 .collect();
831 for raw in name.split(['/', '\\']) {
832 match raw {
833 "" | "." => {}
834 ".." => {
835 if segments.pop().is_none() {
836 return Err(Error::new(
837 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
838 format!("referenced name `{name}` resolves outside the acquisition root"),
839 ));
840 }
841 }
842 segment => {
843 if segment.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
844 || !plain_segment(segment)
845 {
846 return Err(Error::new(
847 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
848 format!("referenced name `{name}` is not a portable relative path"),
849 ));
850 }
851 segments.push(segment.to_owned());
852 }
853 }
854 }
855 if segments.is_empty() {
856 return Err(Error::new(
857 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
858 format!("referenced name `{name}` does not name a file"),
859 ));
860 }
861 if segments.len() > MAX_REFERENCED_DEPTH {
862 return Err(Error::new(
863 &crate::codes::READ_IO_REFERENCE_BUDGET,
864 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
865 ));
866 }
867 Ok(segments)
868}
869
870fn absolute_to_root_relative(root: &Path, name: &str) -> Option<Result<Vec<String>, Error>> {
876 let path = Path::new(name);
877 if !path.is_absolute() {
878 return None;
879 }
880 let Ok(remainder) = path.strip_prefix(root) else {
881 return Some(Err(Error::new(
882 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
883 format!("referenced name `{name}` resolves outside the acquisition root"),
884 )));
885 };
886 let mut segments = Vec::new();
887 for component in remainder.components() {
888 let text = match component {
889 Component::Normal(text) => text.to_str(),
890 _ => None,
891 };
892 let Some(text) = text.filter(|text| plain_segment(text)) else {
893 return Some(Err(Error::new(
894 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
895 format!("referenced name `{name}` is not a portable path beneath the root"),
896 )));
897 };
898 segments.push(text.to_owned());
899 }
900 if segments.is_empty() {
901 return Some(Err(Error::new(
902 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
903 format!("referenced name `{name}` does not name a file"),
904 )));
905 }
906 if segments.len() > MAX_REFERENCED_DEPTH {
907 return Some(Err(Error::new(
908 &crate::codes::READ_IO_REFERENCE_BUDGET,
909 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
910 )));
911 }
912 Some(Ok(segments))
913}
914
915fn canonical_parent(path: &Path) -> Result<PathBuf, Error> {
916 let parent = match path.parent() {
917 Some(parent) if !parent.as_os_str().is_empty() => parent,
918 _ => Path::new("."),
919 };
920 std::fs::canonicalize(parent)
921 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, parent, cause))
922}
923
924fn open_error(info: &'static crate::DiagnosticInfo, path: &Path, cause: std::io::Error) -> Error {
925 Error::new(info, format!("cannot open source `{}`", path.display())).with_cause(cause)
926}
927
928fn listing_error(display: &str, cause: std::io::Error) -> Error {
931 if platform::is_symlink_refusal(&cause) {
932 Error::new(
933 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
934 format!("source directory `{display}` crosses a symbolic link"),
935 )
936 } else {
937 Error::new(
938 &crate::codes::READ_IO_METADATA,
939 format!("cannot list source directory `{display}`"),
940 )
941 .with_cause(cause)
942 }
943}
944
945fn read_open_file(file: std::fs::File, name: &str, max_bytes: u64) -> Result<Arc<[u8]>, Error> {
952 let metadata = file.metadata().map_err(|cause| {
953 Error::new(
954 &crate::codes::READ_IO_METADATA,
955 format!("cannot inspect source buffer `{name}`"),
956 )
957 .with_cause(cause)
958 })?;
959 if !metadata.is_file() {
960 return Err(Error::new(
961 &crate::codes::REQUEST_SOURCE_NOT_A_FILE,
962 format!("source buffer `{name}` is not a regular file"),
963 ));
964 }
965 let declared_length = metadata.len();
966 if declared_length > max_bytes {
967 return Err(Error::new(
968 &crate::codes::READ_IO_REFERENCE_BUDGET,
969 format!(
970 "referenced file `{name}` would take this source past its {MAX_REFERENCED_BYTES} byte acquisition budget"
971 ),
972 ));
973 }
974 let capacity = usize::try_from(declared_length).map_err(|cause| {
975 Error::new(
976 &crate::codes::READ_IO_ALLOCATION_REFUSED,
977 format!("source buffer `{name}` is too large for this platform"),
978 )
979 .with_cause(cause)
980 })?;
981 let mut bytes = Vec::new();
982 bytes.try_reserve_exact(capacity).map_err(|cause| {
983 Error::new(
984 &crate::codes::READ_IO_ALLOCATION_REFUSED,
985 format!("cannot reserve {declared_length} bytes for source buffer `{name}`"),
986 )
987 .with_cause(cause)
988 })?;
989 let read_limit = declared_length
990 .checked_add(1)
991 .ok_or_else(|| {
992 Error::new(
993 &crate::codes::READ_IO_ALLOCATION_REFUSED,
994 format!("source buffer `{name}` is too large to read safely"),
995 )
996 })?
997 .min(max_bytes.saturating_add(1));
998 let mut file = file;
999 file.by_ref()
1000 .take(read_limit)
1001 .read_to_end(&mut bytes)
1002 .map_err(|cause| {
1003 Error::new(
1004 &crate::codes::READ_IO_READ,
1005 format!("cannot read source buffer `{name}`"),
1006 )
1007 .with_cause(cause)
1008 })?;
1009 if bytes.len() != capacity {
1010 return Err(Error::new(
1011 &crate::codes::READ_IO_SOURCE_CHANGED,
1012 format!("source buffer `{name}` changed length while it was read"),
1013 ));
1014 }
1015 Ok(bytes.into())
1016}
1017
1018#[cfg(unix)]
1019mod platform {
1020 use std::ffi::CString;
1030 use std::fs::File;
1031 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
1032 use std::os::unix::ffi::OsStrExt;
1033 use std::path::Path;
1034
1035 #[derive(Debug)]
1036 pub(super) struct RootHandle(OwnedFd);
1037
1038 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1041 let path = c_string(path.as_os_str().as_bytes())?;
1042 let fd = unsafe {
1046 libc::open(
1047 path.as_ptr(),
1048 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1049 )
1050 };
1051 if fd < 0 {
1052 return Err(std::io::Error::last_os_error());
1053 }
1054 let file = unsafe { File::from_raw_fd(fd) };
1056 clear_nonblock(&file)?;
1057 Ok(file)
1058 }
1059
1060 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1065 let path = c_string(path.as_os_str().as_bytes())?;
1066 let fd = unsafe {
1068 libc::open(
1069 path.as_ptr(),
1070 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1071 )
1072 };
1073 if fd < 0 {
1074 return Err(std::io::Error::last_os_error());
1075 }
1076 let file = unsafe { File::from_raw_fd(fd) };
1078 if !file.metadata()?.is_dir() {
1079 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1080 }
1081 clear_nonblock(&file)?;
1082 Ok(RootHandle(file.into()))
1083 }
1084
1085 impl RootHandle {
1086 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1087 let mut directory: Option<OwnedFd> = None;
1088 let (file_segment, directories) =
1089 segments.split_last().expect("resolution yields a file");
1090 for segment in directories {
1091 let next = self.open_at(
1096 directory.as_ref(),
1097 segment,
1098 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1099 )?;
1100 let next = File::from(next);
1101 if !next.metadata()?.is_dir() {
1102 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1103 }
1104 directory = Some(next.into());
1105 }
1106 let fd = self.open_at(
1107 directory.as_ref(),
1108 file_segment,
1109 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1110 )?;
1111 let file = File::from(fd);
1112 clear_nonblock(&file)?;
1113 Ok(file)
1114 }
1115
1116 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1124 let segment = c_string(b".")?;
1125 let fd = unsafe {
1128 libc::openat(
1129 self.0.as_raw_fd(),
1130 segment.as_ptr(),
1131 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY,
1132 )
1133 };
1134 if fd < 0 {
1135 return Err(std::io::Error::last_os_error());
1136 }
1137 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1139 }
1140
1141 fn open_at(
1142 &self,
1143 directory: Option<&OwnedFd>,
1144 segment: &str,
1145 flags: libc::c_int,
1146 ) -> std::io::Result<OwnedFd> {
1147 if !super::plain_segment(segment) {
1148 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1149 }
1150 let at = directory.map_or_else(|| self.0.as_raw_fd(), AsRawFd::as_raw_fd);
1151 let segment = c_string(segment.as_bytes())?;
1152 let fd = unsafe { libc::openat(at, segment.as_ptr(), flags) };
1156 if fd < 0 {
1157 return Err(std::io::Error::last_os_error());
1158 }
1159 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1161 }
1162 }
1163
1164 pub(super) type DirectoryHandle = OwnedFd;
1167
1168 pub(super) fn open_child_directory(
1172 parent: &DirectoryHandle,
1173 name: &str,
1174 ) -> std::io::Result<DirectoryHandle> {
1175 if !super::plain_segment(name) {
1176 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1177 }
1178 let segment = c_string(name.as_bytes())?;
1179 let fd = unsafe {
1183 libc::openat(
1184 parent.as_raw_fd(),
1185 segment.as_ptr(),
1186 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1187 )
1188 };
1189 if fd < 0 {
1190 return Err(std::io::Error::last_os_error());
1191 }
1192 let file = unsafe { File::from_raw_fd(fd) };
1194 if !file.metadata()?.is_dir() {
1195 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1196 }
1197 Ok(file.into())
1198 }
1199
1200 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1201
1202 fn entry_budget_error() -> std::io::Error {
1203 std::io::Error::other(ENTRY_BUDGET_MARKER)
1204 }
1205
1206 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1209 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1210 }
1211
1212 pub(super) fn list_entries(
1219 directory: &DirectoryHandle,
1220 max: usize,
1221 ) -> std::io::Result<Vec<(String, bool)>> {
1222 use std::os::fd::IntoRawFd;
1223
1224 let raw = directory.try_clone()?.into_raw_fd();
1227 let stream = unsafe { libc::fdopendir(raw) };
1230 if stream.is_null() {
1231 let error = std::io::Error::last_os_error();
1232 unsafe { libc::close(raw) };
1235 return Err(error);
1236 }
1237 let mut entries = Vec::new();
1238 loop {
1239 errno_clear();
1241 let entry = unsafe { libc::readdir(stream) };
1242 if entry.is_null() {
1243 let error = std::io::Error::last_os_error();
1244 unsafe { libc::closedir(stream) };
1247 if error.raw_os_error().is_some_and(|code| code != 0) {
1248 return Err(error);
1249 }
1250 break;
1251 }
1252 let name_bytes = unsafe {
1257 std::ffi::CStr::from_ptr((&raw const (*entry).d_name).cast::<libc::c_char>())
1258 };
1259 let Ok(name) = name_bytes.to_str() else {
1260 continue;
1261 };
1262 if name == "." || name == ".." {
1263 continue;
1264 }
1265 if entries.len() == max {
1266 unsafe { libc::closedir(stream) };
1268 return Err(entry_budget_error());
1269 }
1270 let kind = unsafe { (*entry).d_type };
1272 let is_directory = match kind {
1273 libc::DT_DIR => true,
1274 libc::DT_UNKNOWN => {
1275 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1278 let segment =
1279 c_string(name.as_bytes()).expect("directory entry names carry no NUL");
1280 let status = unsafe {
1284 libc::fstatat(
1285 libc::dirfd(stream),
1286 segment.as_ptr(),
1287 &raw mut stat,
1288 libc::AT_SYMLINK_NOFOLLOW,
1289 )
1290 };
1291 status == 0 && stat.st_mode & libc::S_IFMT == libc::S_IFDIR
1292 }
1293 _ => false,
1294 };
1295 entries.push((name.to_owned(), is_directory));
1296 }
1297 Ok(entries)
1298 }
1299
1300 fn errno_clear() {
1301 unsafe {
1303 *errno_location() = 0;
1304 }
1305 }
1306
1307 #[cfg(target_os = "macos")]
1308 fn errno_location() -> *mut libc::c_int {
1309 unsafe { libc::__error() }
1311 }
1312
1313 #[cfg(not(target_os = "macos"))]
1314 fn errno_location() -> *mut libc::c_int {
1315 unsafe { libc::__errno_location() }
1318 }
1319
1320 fn clear_nonblock(file: &File) -> std::io::Result<()> {
1322 let fd = file.as_raw_fd();
1323 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1326 if flags < 0 {
1327 return Err(std::io::Error::last_os_error());
1328 }
1329 let status = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1331 if status < 0 {
1332 return Err(std::io::Error::last_os_error());
1333 }
1334 Ok(())
1335 }
1336
1337 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1338 matches!(error.raw_os_error(), Some(libc::ELOOP | libc::EMLINK))
1339 }
1340
1341 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1342 let _ = path;
1346 error.raw_os_error() == Some(libc::EISDIR)
1347 }
1348
1349 fn c_string(bytes: &[u8]) -> std::io::Result<CString> {
1350 CString::new(bytes).map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))
1351 }
1352
1353 #[cfg(test)]
1354 mod tests {
1355 use super::*;
1356
1357 #[test]
1358 fn open_root_refuses_a_symbolic_link_to_a_directory() {
1359 let base = std::env::temp_dir().join(format!(
1360 "powerio-core-open-root-{}-{}",
1361 std::process::id(),
1362 std::time::SystemTime::now()
1363 .duration_since(std::time::UNIX_EPOCH)
1364 .unwrap()
1365 .as_nanos()
1366 ));
1367 std::fs::create_dir_all(&base).unwrap();
1368 assert!(open_root(&base).is_ok());
1369 let link = base.join("link");
1370 std::os::unix::fs::symlink(&base, &link).unwrap();
1371 let error = open_root(&link).unwrap_err();
1372 assert!(
1373 is_symlink_refusal(&error) || error.kind() == std::io::ErrorKind::NotADirectory,
1374 "{error:?}"
1375 );
1376 std::fs::remove_dir_all(&base).unwrap();
1377 }
1378 }
1379}
1380
1381#[cfg(not(unix))]
1382mod platform {
1383 use std::fs::File;
1391 use std::path::{Path, PathBuf};
1392
1393 #[derive(Debug)]
1394 pub(super) struct RootHandle {
1395 root: PathBuf,
1396 }
1397
1398 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1399 let file = open_reparse_refused(path)?;
1400 Ok(file)
1401 }
1402
1403 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1404 drop(open_directory_pinned(path)?);
1408 Ok(RootHandle {
1409 root: path.to_path_buf(),
1410 })
1411 }
1412
1413 impl RootHandle {
1414 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1415 let mut path = self.root.clone();
1416 let (file_segment, directories) =
1417 segments.split_last().expect("resolution yields a file");
1418 let mut held = Vec::with_capacity(directories.len() + 1);
1422 held.push(open_directory_pinned(&self.root)?);
1423 for segment in directories {
1424 push_plain_segment(&mut path, segment)?;
1425 held.push(open_directory_pinned(&path)?);
1426 }
1427 push_plain_segment(&mut path, file_segment)?;
1428 let file = open_reparse_refused(&path)?;
1429 drop(held);
1430 Ok(file)
1431 }
1432
1433 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1436 let handle = open_directory_pinned(&self.root)?;
1437 Ok(DirectoryHandle {
1438 path: self.root.clone(),
1439 _handle: handle,
1440 })
1441 }
1442 }
1443
1444 #[derive(Debug)]
1448 pub(super) struct DirectoryHandle {
1449 path: PathBuf,
1450 _handle: File,
1451 }
1452
1453 pub(super) fn open_child_directory(
1457 parent: &DirectoryHandle,
1458 name: &str,
1459 ) -> std::io::Result<DirectoryHandle> {
1460 let mut path = parent.path.clone();
1461 push_plain_segment(&mut path, name)?;
1462 let handle = open_directory_pinned(&path)?;
1463 Ok(DirectoryHandle {
1464 path,
1465 _handle: handle,
1466 })
1467 }
1468
1469 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1470
1471 fn entry_budget_error() -> std::io::Error {
1472 std::io::Error::other(ENTRY_BUDGET_MARKER)
1473 }
1474
1475 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1478 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1479 }
1480
1481 pub(super) fn list_entries(
1486 directory: &DirectoryHandle,
1487 max: usize,
1488 ) -> std::io::Result<Vec<(String, bool)>> {
1489 let mut entries = Vec::new();
1490 for entry in std::fs::read_dir(&directory.path)? {
1491 let entry = entry?;
1492 let Ok(name) = entry.file_name().into_string() else {
1493 continue;
1494 };
1495 if entries.len() == max {
1496 return Err(entry_budget_error());
1497 }
1498 let is_directory = entry.file_type()?.is_dir();
1499 entries.push((name, is_directory));
1500 }
1501 Ok(entries)
1502 }
1503
1504 fn push_plain_segment(path: &mut PathBuf, segment: &str) -> std::io::Result<()> {
1507 if !super::plain_segment(segment) {
1508 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1509 }
1510 path.push(segment);
1511 Ok(())
1512 }
1513
1514 #[cfg(windows)]
1520 fn open_directory_pinned(path: &Path) -> std::io::Result<File> {
1521 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1522
1523 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1524 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1525 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1526 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
1527 const FILE_SHARE_READ: u32 = 0x0000_0001;
1528 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
1529
1530 let file = std::fs::OpenOptions::new()
1531 .read(true)
1532 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1533 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
1534 .open(path)?;
1535 let attributes = file.metadata()?.file_attributes();
1536 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1537 return Err(symlink_error());
1538 }
1539 if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1540 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1541 }
1542 Ok(file)
1543 }
1544
1545 #[cfg(not(windows))]
1546 fn open_directory_pinned(path: &Path) -> std::io::Result<File> {
1547 let metadata = std::fs::symlink_metadata(path)?;
1548 if metadata.file_type().is_symlink() {
1549 return Err(symlink_error());
1550 }
1551 if !metadata.is_dir() {
1552 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1553 }
1554 File::open(path)
1555 }
1556
1557 #[cfg(windows)]
1558 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1559 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1560
1561 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1562 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1563
1564 let file = std::fs::OpenOptions::new()
1565 .read(true)
1566 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
1567 .open(path)?;
1568 let attributes = file.metadata()?.file_attributes();
1569 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1570 return Err(symlink_error());
1571 }
1572 Ok(file)
1573 }
1574
1575 #[cfg(not(windows))]
1576 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1577 let metadata = std::fs::symlink_metadata(path)?;
1578 if metadata.file_type().is_symlink() {
1579 return Err(symlink_error());
1580 }
1581 File::open(path)
1582 }
1583
1584 fn symlink_error() -> std::io::Error {
1585 std::io::Error::new(std::io::ErrorKind::InvalidData, "symbolic link refused")
1586 }
1587
1588 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1589 error.kind() == std::io::ErrorKind::InvalidData
1590 && error.to_string().contains("symbolic link refused")
1591 }
1592
1593 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1594 let _ = error;
1595 std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
1596 }
1597}
1598
1599#[cfg(test)]
1600mod tests {
1601 use std::time::{SystemTime, UNIX_EPOCH};
1602
1603 use super::*;
1604 use crate::ArtifactPath;
1605
1606 static PROCESS_RESOURCE_TESTS: Mutex<()> = Mutex::new(());
1611
1612 fn process_resource_guard() -> MutexGuard<'static, ()> {
1613 PROCESS_RESOURCE_TESTS
1614 .lock()
1615 .unwrap_or_else(std::sync::PoisonError::into_inner)
1616 }
1617
1618 struct CountingAllocator;
1621
1622 static ALLOCATED_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1623
1624 thread_local! {
1625 static MEASURING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1626 }
1627
1628 fn measured_bytes<T>(work: impl FnOnce() -> T) -> (T, usize) {
1629 let _ = MEASURING.try_with(|flag| flag.set(true));
1630 let before = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1631 let value = work();
1632 let after = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1633 let _ = MEASURING.try_with(|flag| flag.set(false));
1634 (value, after.saturating_sub(before))
1635 }
1636
1637 unsafe impl std::alloc::GlobalAlloc for CountingAllocator {
1640 unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
1641 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1642 ALLOCATED_BYTES.fetch_add(layout.size(), std::sync::atomic::Ordering::Relaxed);
1643 }
1644 unsafe { std::alloc::System.alloc(layout) }
1645 }
1646
1647 unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
1648 unsafe { std::alloc::System.dealloc(ptr, layout) }
1649 }
1650
1651 unsafe fn realloc(
1652 &self,
1653 ptr: *mut u8,
1654 layout: std::alloc::Layout,
1655 new_size: usize,
1656 ) -> *mut u8 {
1657 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1658 ALLOCATED_BYTES.fetch_add(new_size, std::sync::atomic::Ordering::Relaxed);
1659 }
1660 unsafe { std::alloc::System.realloc(ptr, layout, new_size) }
1661 }
1662 }
1663
1664 #[global_allocator]
1665 static COUNTING: CountingAllocator = CountingAllocator;
1666
1667 fn test_root(name: &str) -> PathBuf {
1668 let nonce = SystemTime::now()
1669 .duration_since(UNIX_EPOCH)
1670 .unwrap()
1671 .as_nanos();
1672 std::env::temp_dir().join(format!(
1673 "powerio-core-{name}-{}-{nonce}",
1674 std::process::id()
1675 ))
1676 }
1677
1678 #[test]
1679 fn format_ids_use_the_exact_open_grammar() {
1680 for id in ["matpower", "psse-raw", "doe-go-3", "x1"] {
1681 assert_eq!(FormatId::new(id).unwrap().as_str(), id);
1682 }
1683 for id in ["", "1matpower", "MATPOWER", "psse--raw", "psse-", "p_sse"] {
1684 assert!(FormatId::new(id).is_err(), "{id}");
1685 }
1686 assert!(FormatId::new("a".repeat(MAX_FORMAT_ID_BYTES + 1)).is_err());
1687 }
1688
1689 #[test]
1690 fn memory_sources_retain_arbitrary_binary_bytes_without_copying() {
1691 let bytes: Arc<[u8]> = vec![0, 255, 0, 128].into();
1692 let pointer = bytes.as_ptr();
1693 let source = Source::from_bytes("input.bin", Arc::clone(&bytes))
1694 .unwrap()
1695 .with_format(FormatId::new("pwb").unwrap());
1696 let buffer = source.primary_buffer().unwrap();
1697 assert_eq!(buffer.bytes(), [0, 255, 0, 128]);
1698 assert_eq!(buffer.bytes().as_ptr(), pointer);
1699 assert_eq!(source.format().unwrap().as_str(), "pwb");
1700 assert!(Source::from_bytes("", Vec::new()).is_err());
1701 assert!(Source::from_bytes("x\0y", Vec::new()).is_err());
1702 }
1703
1704 #[test]
1705 fn a_bom_is_retained_and_skipped_for_the_parser_without_a_second_buffer() {
1706 let bytes: Vec<u8> = [0xEF, 0xBB, 0xBF, b'm', b'p', b'c'].to_vec();
1707 let source = Source::from_bytes("case.m", bytes).unwrap();
1708 let buffer = source.primary_buffer().unwrap();
1709 assert!(buffer.has_utf8_bom());
1710 assert_eq!(buffer.bytes().len(), 6);
1711 assert_eq!(buffer.content_bytes(), b"mpc");
1712 assert_eq!(
1714 buffer.content_bytes().as_ptr(),
1715 buffer.bytes()[3..].as_ptr()
1716 );
1717
1718 let plain = Source::from_bytes("case.m", b"mpc".to_vec()).unwrap();
1719 let plain = plain.primary_buffer().unwrap();
1720 assert!(!plain.has_utf8_bom());
1721 assert_eq!(plain.content_bytes(), plain.bytes());
1722 }
1723
1724 #[test]
1725 fn a_memory_source_resolves_named_buffers_and_never_the_filesystem() {
1726 let source = Source::from_bytes("master.dss", b"redirect sub/feeder.dss".to_vec())
1727 .unwrap()
1728 .with_named_buffer("sub/feeder.dss", b"feeder".to_vec())
1729 .unwrap();
1730 let primary = source.primary_buffer().unwrap();
1731 let feeder = source
1732 .referenced_buffer(&primary, "sub/feeder.dss")
1733 .unwrap();
1734 assert_eq!(feeder.bytes(), b"feeder");
1735
1736 let sibling = source
1739 .referenced_buffer(&feeder, "../sub/feeder.dss")
1740 .unwrap();
1741 assert_eq!(sibling.bytes(), b"feeder");
1742
1743 assert!(source.referenced_buffer(&primary, "missing.dss").is_err());
1744 let escape = source.referenced_buffer(&primary, "../outside.dss");
1745 assert_eq!(
1746 escape.unwrap_err().category(),
1747 crate::ErrorCategory::Request
1748 );
1749 }
1750
1751 #[test]
1752 fn a_file_source_acquires_referenced_files_beneath_its_containing_directory() {
1753 let root = test_root("file-refs");
1754 std::fs::create_dir_all(root.join("sub")).unwrap();
1755 std::fs::write(root.join("master.dss"), b"master").unwrap();
1756 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
1757 std::fs::write(root.join("sub/coords.csv"), b"coords").unwrap();
1758
1759 let source = Source::open(root.join("master.dss")).unwrap();
1760 let primary = source.primary_buffer().unwrap();
1761 assert_eq!(primary.bytes(), b"master");
1762
1763 let feeder = source
1764 .referenced_buffer(&primary, "sub/feeder.dss")
1765 .unwrap();
1766 assert_eq!(feeder.bytes(), b"feeder");
1767 let coords = source.referenced_buffer(&feeder, "coords.csv").unwrap();
1770 assert_eq!(coords.bytes(), b"coords");
1771 let master_again = source.referenced_buffer(&feeder, "../master.dss").unwrap();
1773 assert_eq!(master_again.bytes(), b"master");
1774 let escape = source.referenced_buffer(&primary, "../escape.dss");
1775 assert_eq!(
1776 escape.unwrap_err().category(),
1777 crate::ErrorCategory::Request
1778 );
1779
1780 let again = source
1782 .referenced_buffer(&primary, "sub/feeder.dss")
1783 .unwrap();
1784 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
1785 assert_eq!(source.acquired_buffers().len(), 4);
1786 std::fs::remove_dir_all(root).unwrap();
1787 }
1788
1789 #[test]
1790 fn an_explicitly_wider_root_admits_shared_files_and_still_confines() {
1791 let root = test_root("wider-root");
1792 std::fs::create_dir_all(root.join("cases")).unwrap();
1793 std::fs::create_dir_all(root.join("shared")).unwrap();
1794 std::fs::write(root.join("cases/master.dss"), b"master").unwrap();
1795 std::fs::write(root.join("shared/wires.dss"), b"wires").unwrap();
1796
1797 let narrow = Source::open(root.join("cases/master.dss")).unwrap();
1799 let primary = narrow.primary_buffer().unwrap();
1800 assert!(
1801 narrow
1802 .referenced_buffer(&primary, "../shared/wires.dss")
1803 .is_err()
1804 );
1805
1806 let wide = Source::open(root.join("cases/master.dss"))
1808 .unwrap()
1809 .with_acquisition_root(&root)
1810 .unwrap();
1811 let primary = wide.primary_buffer().unwrap();
1812 let wires = wide
1813 .referenced_buffer(&primary, "../shared/wires.dss")
1814 .unwrap();
1815 assert_eq!(wires.bytes(), b"wires");
1816 assert!(
1817 wide.referenced_buffer(&primary, "../../etc/passwd")
1818 .is_err()
1819 );
1820
1821 let outside = test_root("wider-root-outside");
1823 std::fs::create_dir_all(&outside).unwrap();
1824 assert!(
1825 Source::open(root.join("cases/master.dss"))
1826 .unwrap()
1827 .with_acquisition_root(&outside)
1828 .is_err()
1829 );
1830 std::fs::remove_dir_all(outside).ok();
1831 std::fs::remove_dir_all(root).unwrap();
1832 }
1833
1834 #[test]
1835 fn directory_buffers_are_lazy_cached_and_binary_safe() {
1836 let root = test_root("directory");
1837 std::fs::create_dir_all(root.join("nested")).unwrap();
1838 std::fs::write(root.join("nested/data.bin"), [0, 255, 7]).unwrap();
1839 let source = Source::open(&root).unwrap();
1840 assert!(source.is_directory());
1841 assert!(source.acquired_buffers().is_empty());
1842 let name = ArtifactPath::new("nested/data.bin").unwrap();
1843 let first = source.buffer(&name).unwrap();
1844 let second = source.buffer(&name).unwrap();
1845 assert_eq!(first.bytes(), [0, 255, 7]);
1846 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
1847 assert_eq!(source.acquired_buffers().len(), 1);
1848 std::fs::remove_dir_all(root).unwrap();
1849 }
1850
1851 #[cfg(unix)]
1852 #[test]
1853 fn source_acquisition_refuses_root_and_child_symlinks() {
1854 use std::os::unix::fs::symlink;
1855
1856 let root = test_root("symlink");
1857 std::fs::create_dir_all(&root).unwrap();
1858 std::fs::write(root.join("real.bin"), b"real").unwrap();
1859 symlink(root.join("real.bin"), root.join("link.bin")).unwrap();
1860 let source = Source::open(&root).unwrap();
1861 let error = source
1862 .buffer(&ArtifactPath::new("link.bin").unwrap())
1863 .unwrap_err();
1864 assert_eq!(error.category(), crate::ErrorCategory::Request);
1865
1866 std::fs::create_dir_all(root.join("real-dir")).unwrap();
1868 std::fs::write(root.join("real-dir/inner.bin"), b"inner").unwrap();
1869 symlink(root.join("real-dir"), root.join("link-dir")).unwrap();
1870 let error = source
1871 .buffer(&ArtifactPath::new("link-dir/inner.bin").unwrap())
1872 .unwrap_err();
1873 assert_eq!(error.category(), crate::ErrorCategory::Request);
1874
1875 let root_link = root.with_extension("link");
1876 symlink(&root, &root_link).unwrap();
1877 assert!(Source::open(&root_link).is_err());
1878 std::fs::remove_file(root_link).unwrap();
1879 std::fs::remove_dir_all(root).unwrap();
1880 }
1881
1882 #[cfg(unix)]
1883 #[test]
1884 fn a_named_pipe_is_refused_promptly_and_siblings_still_acquire() {
1885 use std::os::unix::ffi::OsStrExt;
1886
1887 let root = test_root("fifo");
1888 std::fs::create_dir_all(&root).unwrap();
1889 std::fs::write(root.join("real.csv"), b"real").unwrap();
1890 let fifo = root.join("pipe.dat");
1891 let c_path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
1892 assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
1895
1896 let (sender, receiver) = std::sync::mpsc::channel();
1900 let opened_root = root.clone();
1901 let worker = std::thread::spawn(move || {
1902 let open_error = Source::open(opened_root.join("pipe.dat")).map(|_| ());
1903 let directory = Source::open(&opened_root).unwrap();
1904 let buffer_error = directory
1905 .buffer(&ArtifactPath::new("pipe.dat").unwrap())
1906 .map(|_| ());
1907 let sibling = directory
1908 .buffer(&ArtifactPath::new("real.csv").unwrap())
1909 .map(|buffer| buffer.bytes().to_vec());
1910 sender.send((open_error, buffer_error, sibling)).unwrap();
1911 });
1912 let (open_error, buffer_error, sibling) = receiver
1913 .recv_timeout(std::time::Duration::from_secs(10))
1914 .expect("acquisition on a writerless pipe completes promptly");
1915 worker.join().unwrap();
1916 assert_eq!(
1917 open_error.unwrap_err().category(),
1918 crate::ErrorCategory::Request
1919 );
1920 assert_eq!(
1921 buffer_error.unwrap_err().category(),
1922 crate::ErrorCategory::Request
1923 );
1924 assert_eq!(sibling.unwrap(), b"real");
1925 std::fs::remove_dir_all(root).unwrap();
1926 }
1927
1928 #[test]
1929 fn an_over_budget_referenced_file_is_refused_before_allocation() {
1930 let root = test_root("budget");
1931 std::fs::create_dir_all(&root).unwrap();
1932 std::fs::write(root.join("master.dss"), b"master").unwrap();
1933 let big = std::fs::File::create(root.join("big.dat")).unwrap();
1937 big.set_len(MAX_REFERENCED_BYTES * 4).unwrap();
1938 drop(big);
1939
1940 let source = Source::open(root.join("master.dss")).unwrap();
1941 let primary = source.primary_buffer().unwrap();
1942 let (error, allocated) =
1947 measured_bytes(|| source.referenced_buffer(&primary, "big.dat").unwrap_err());
1948 assert!(error.to_string().contains("acquisition budget"), "{error}");
1949 assert!(
1950 (allocated as u64) < MAX_REFERENCED_BYTES / 16,
1951 "the refused acquisition allocated {allocated} bytes"
1952 );
1953 std::fs::remove_dir_all(root).unwrap();
1954 }
1955
1956 #[cfg(unix)]
1957 #[test]
1958 fn racing_entry_listing_never_names_files_outside_the_root() {
1959 use std::os::unix::fs::symlink;
1960
1961 let root = test_root("race-list");
1962 std::fs::create_dir_all(root.join("sub")).unwrap();
1963 std::fs::write(root.join("sub/inside.txt"), b"inside").unwrap();
1964 let outside = test_root("race-list-outside");
1965 std::fs::create_dir_all(&outside).unwrap();
1966 std::fs::write(outside.join("outside-only.txt"), b"outside").unwrap();
1967
1968 let source = Source::open(&root).unwrap();
1969 let stop = std::sync::atomic::AtomicBool::new(false);
1970 std::thread::scope(|scope| {
1971 let flipper = scope.spawn(|| {
1972 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
1973 let _ = std::fs::remove_dir_all(root.join("sub"));
1974 let _ = symlink(&outside, root.join("sub"));
1975 let _ = std::fs::remove_file(root.join("sub"));
1976 let _ = std::fs::create_dir(root.join("sub"));
1977 let _ = std::fs::write(root.join("sub/inside.txt"), b"inside");
1978 }
1979 });
1980 for _ in 0..50 {
1981 if let Ok(names) = source.entry_names() {
1984 assert!(
1985 names
1986 .iter()
1987 .all(|name| !name.as_str().contains("outside-only")),
1988 "{names:?}"
1989 );
1990 }
1991 }
1992 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1993 flipper.join().unwrap();
1994 });
1995 std::fs::remove_dir_all(&root).ok();
1996 std::fs::remove_dir_all(&outside).ok();
1997 }
1998
1999 #[test]
2000 fn referenced_names_must_be_portable_relative_paths() {
2001 let root = test_root("portable-names");
2002 std::fs::create_dir_all(root.join("sub")).unwrap();
2003 std::fs::write(root.join("master.dss"), b"master").unwrap();
2004 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
2005 let source = Source::open(root.join("master.dss")).unwrap();
2006 let primary = source.primary_buffer().unwrap();
2007
2008 for name in ["..\\escape.dss", "\\escape.dss", "C:\\escape.dss", "C:x"] {
2011 let error = source.referenced_buffer(&primary, name).unwrap_err();
2012 assert_eq!(
2013 error.category(),
2014 crate::ErrorCategory::Request,
2015 "{name}: {error}"
2016 );
2017 }
2018 assert!(source.root_buffer("..\\master.dss").is_err());
2019 assert!(source.root_buffer("\\master.dss").is_err());
2020
2021 let feeder = source
2023 .referenced_buffer(&primary, "sub/feeder.dss")
2024 .unwrap();
2025 assert_eq!(feeder.bytes(), b"feeder");
2026
2027 let absolute = root.canonicalize().unwrap().join("sub").join("feeder.dss");
2033 let again = source
2034 .referenced_buffer(&primary, absolute.to_str().unwrap())
2035 .unwrap();
2036 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
2037 assert!(
2039 source
2040 .acquired_buffers()
2041 .iter()
2042 .all(|buffer| !buffer.name().contains("escape"))
2043 );
2044 std::fs::remove_dir_all(root).unwrap();
2045 }
2046
2047 #[cfg(unix)]
2048 #[test]
2049 fn live_sources_hold_no_directory_descriptors_before_acquisition() {
2050 fn open_descriptor_count() -> usize {
2051 let table = if cfg!(target_os = "macos") {
2052 "/dev/fd"
2053 } else {
2054 "/proc/self/fd"
2055 };
2056 std::fs::read_dir(table).unwrap().count()
2057 }
2058
2059 let _guard = process_resource_guard();
2060 let root = test_root("fd-count");
2061 std::fs::create_dir_all(&root).unwrap();
2062 std::fs::write(root.join("case.m"), b"case").unwrap();
2063 std::fs::write(root.join("ref.csv"), b"ref").unwrap();
2064
2065 let before = open_descriptor_count();
2066 let sources: Vec<Source> = (0..300)
2067 .map(|_| Source::open(root.join("case.m")).unwrap())
2068 .collect();
2069 let held = open_descriptor_count();
2070 assert!(
2071 held <= before + 4,
2072 "{} sources hold {} descriptors over the baseline {}",
2073 sources.len(),
2074 held - before,
2075 before
2076 );
2077
2078 for source in sources.iter().take(32) {
2083 let primary = source.primary_buffer().unwrap();
2084 let first = source.referenced_buffer(&primary, "ref.csv").unwrap();
2085 let second = source.referenced_buffer(&primary, "ref.csv").unwrap();
2086 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
2087 }
2088 drop(sources);
2089 std::fs::remove_dir_all(root).unwrap();
2090 }
2091
2092 #[test]
2093 fn entry_listing_returns_names_of_every_length_exactly() {
2094 let root = test_root("name-lengths");
2097 std::fs::create_dir_all(&root).unwrap();
2098 let long = "n".repeat(200);
2099 for name in ["a", "medium-name.csv", long.as_str()] {
2100 std::fs::write(root.join(name), b"x").unwrap();
2101 }
2102 let source = Source::open(&root).unwrap();
2103 let mut names: Vec<String> = source
2104 .entry_names()
2105 .unwrap()
2106 .iter()
2107 .map(|name| name.as_str().to_owned())
2108 .collect();
2109 names.sort();
2110 let mut expected = vec!["a".to_owned(), "medium-name.csv".to_owned(), long];
2111 expected.sort();
2112 assert_eq!(names, expected);
2113 std::fs::remove_dir_all(root).unwrap();
2114 }
2115
2116 #[cfg(unix)]
2117 #[test]
2118 fn a_directory_nested_past_the_depth_bound_is_refused_promptly() {
2119 use std::os::fd::{AsRawFd, FromRawFd};
2120
2121 let _guard = process_resource_guard();
2124
2125 let root = test_root("deep-chain");
2126 std::fs::create_dir_all(&root).unwrap();
2127 let name = std::ffi::CString::new("d").unwrap();
2130 let mut level = std::fs::File::open(&root).unwrap();
2131 for _ in 0..(MAX_REFERENCED_DEPTH + 40) {
2132 unsafe {
2136 assert_eq!(libc::mkdirat(level.as_raw_fd(), name.as_ptr(), 0o755), 0);
2137 let fd = libc::openat(
2138 level.as_raw_fd(),
2139 name.as_ptr(),
2140 libc::O_RDONLY | libc::O_CLOEXEC,
2141 );
2142 assert!(fd >= 0);
2143 level = std::fs::File::from_raw_fd(fd);
2144 }
2145 }
2146 drop(level);
2147
2148 let (sender, receiver) = std::sync::mpsc::channel();
2149 let listed_root = root.clone();
2150 let worker = std::thread::spawn(move || {
2151 let source = Source::open(&listed_root).unwrap();
2152 sender.send(source.entry_names().map(|_| ())).unwrap();
2153 });
2154 let outcome = receiver
2155 .recv_timeout(std::time::Duration::from_secs(10))
2156 .expect("the depth refusal returns promptly");
2157 worker.join().unwrap();
2158 let error = outcome.expect_err("a chain past the depth bound refuses");
2159 assert!(error.to_string().contains("levels deep"), "{error}");
2160
2161 let mut fds = vec![std::fs::File::open(&root).unwrap()];
2164 loop {
2165 let last = fds.last().unwrap();
2166 let fd = unsafe {
2168 libc::openat(
2169 last.as_raw_fd(),
2170 name.as_ptr(),
2171 libc::O_RDONLY | libc::O_CLOEXEC,
2172 )
2173 };
2174 if fd < 0 {
2175 break;
2176 }
2177 fds.push(unsafe { std::fs::File::from_raw_fd(fd) });
2179 }
2180 while fds.len() > 1 {
2181 let parent = &fds[fds.len() - 2];
2182 unsafe {
2184 libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR);
2185 }
2186 fds.pop();
2187 }
2188 drop(fds);
2189 std::fs::remove_dir_all(&root).unwrap();
2190 }
2191
2192 #[test]
2193 fn a_directory_past_the_entry_budget_is_refused_with_bounded_memory() {
2194 let root = test_root("entry-budget");
2195 std::fs::create_dir_all(&root).unwrap();
2196 let excess = MAX_REFERENCED_FILES * 4;
2200 for index in 0..excess {
2201 std::fs::write(root.join(format!("f{index:05}.csv")), b"").unwrap();
2202 }
2203 let source = Source::open(&root).unwrap();
2204 let (error, allocated) = measured_bytes(|| source.entry_names().unwrap_err());
2205 assert!(error.to_string().contains("entries"), "{error}");
2206 assert!(
2211 allocated < MAX_REFERENCED_FILES * 192,
2212 "the refused listing allocated {allocated} bytes"
2213 );
2214 std::fs::remove_dir_all(root).unwrap();
2215 }
2216
2217 #[cfg(unix)]
2218 #[test]
2219 fn listing_breadth_never_scales_open_descriptors() {
2220 fn open_descriptor_count() -> usize {
2221 let table = if cfg!(target_os = "macos") {
2222 "/dev/fd"
2223 } else {
2224 "/proc/self/fd"
2225 };
2226 std::fs::read_dir(table).unwrap().count()
2227 }
2228
2229 let _guard = process_resource_guard();
2230 let root = test_root("breadth");
2231 let breadth = 400usize;
2235 for index in 0..breadth {
2236 let sub = root.join(format!("s{index:03}"));
2237 std::fs::create_dir_all(&sub).unwrap();
2238 std::fs::write(sub.join("data.csv"), b"x").unwrap();
2239 }
2240 std::fs::create_dir_all(root.join("nested/a/b/c")).unwrap();
2241 std::fs::write(root.join("nested/a/b/c/deep.csv"), b"x").unwrap();
2242
2243 let mut original: libc::rlimit = unsafe { std::mem::zeroed() };
2249 assert_eq!(
2250 unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut original) },
2252 0
2253 );
2254 let lowered = libc::rlimit {
2255 rlim_cur: 256,
2256 rlim_max: original.rlim_max,
2257 };
2258 assert_eq!(
2260 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const lowered) },
2261 0
2262 );
2263
2264 let baseline = open_descriptor_count();
2265 let peak = std::sync::atomic::AtomicUsize::new(0);
2266 let stop = std::sync::atomic::AtomicBool::new(false);
2267 let names = std::thread::scope(|scope| {
2268 let sampler = scope.spawn(|| {
2269 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
2270 let count = open_descriptor_count();
2271 peak.fetch_max(count, std::sync::atomic::Ordering::Relaxed);
2272 }
2273 });
2274 let source = Source::open(&root).unwrap();
2275 let names = source.entry_names().unwrap();
2276 stop.store(true, std::sync::atomic::Ordering::Relaxed);
2277 sampler.join().unwrap();
2278 drop(source);
2279 names
2280 });
2281 assert_eq!(
2283 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const original) },
2284 0
2285 );
2286
2287 assert_eq!(names.len(), breadth + 1, "every file listed");
2288 let sampled_peak = peak.load(std::sync::atomic::Ordering::Relaxed);
2289 assert!(
2290 sampled_peak <= baseline + MAX_REFERENCED_DEPTH + 16,
2291 "the walk held {sampled_peak} descriptors over a baseline of {baseline}"
2292 );
2293 std::fs::remove_dir_all(root).unwrap();
2294 }
2295
2296 #[test]
2297 fn a_directory_listing_still_names_windows_reserved_spellings() {
2298 let root = test_root("reserved-listing");
2301 std::fs::create_dir_all(&root).unwrap();
2302 std::fs::write(root.join("aux.dss"), b"content").unwrap();
2303 let source = Source::open(&root).unwrap();
2304 let names = source.entry_names().unwrap();
2305 assert_eq!(names.len(), 1);
2306 assert_eq!(names[0].as_str(), "aux.dss");
2307 std::fs::remove_dir_all(root).unwrap();
2308 }
2309
2310 #[test]
2311 fn a_directory_listing_repeats_and_survives_acquisition() {
2312 let root = test_root("repeat-listing");
2317 std::fs::create_dir_all(&root).unwrap();
2318 std::fs::write(root.join("network.csv"), b"name\nseq\n").unwrap();
2319 std::fs::write(root.join("buses.csv"), b"name\nB1\n").unwrap();
2320 let source = Source::open(&root).unwrap();
2321 let first = source.entry_names().unwrap();
2322 assert_eq!(first.len(), 2);
2323 let name = ArtifactPath::new("network.csv").unwrap();
2324 source.buffer(&name).unwrap();
2325 let second = source.entry_names().unwrap();
2326 assert_eq!(first, second);
2327 std::fs::remove_dir_all(root).unwrap();
2328 }
2329
2330 #[test]
2331 fn concurrent_acquisition_retains_one_buffer_for_one_name() {
2332 let root = test_root("concurrent");
2333 std::fs::create_dir_all(&root).unwrap();
2334 std::fs::write(root.join("shared.csv"), b"shared").unwrap();
2335 let source = Source::open(&root).unwrap();
2336 let name = ArtifactPath::new("shared.csv").unwrap();
2337 let buffers: Vec<_> = std::thread::scope(|scope| {
2338 (0..8)
2339 .map(|_| {
2340 let source = source.clone();
2341 let name = name.clone();
2342 scope.spawn(move || source.buffer(&name).unwrap())
2343 })
2344 .collect::<Vec<_>>()
2345 .into_iter()
2346 .map(|handle| handle.join().unwrap())
2347 .collect()
2348 });
2349 let pointer = buffers[0].bytes().as_ptr();
2350 assert!(
2351 buffers
2352 .iter()
2353 .all(|buffer| buffer.bytes().as_ptr() == pointer)
2354 );
2355 assert_eq!(source.acquired_buffers().len(), 1);
2356 std::fs::remove_dir_all(root).unwrap();
2357 }
2358 #[cfg(unix)]
2359 #[test]
2360 fn a_refused_listing_never_shortens_the_next_one() {
2361 let root = test_root("refused-listing");
2365 std::fs::create_dir_all(&root).unwrap();
2366 for index in 0..(MAX_REFERENCED_FILES + 5) {
2367 std::fs::write(root.join(format!("f{index}.txt")), b"x").unwrap();
2368 }
2369 let source = Source::open(&root).unwrap();
2370 let first = source.entry_names().unwrap_err();
2371 let second = source.entry_names().unwrap_err();
2372 assert_eq!(
2373 first.diagnostics().first().map(|d| d.code().to_owned()),
2374 second.diagnostics().first().map(|d| d.code().to_owned()),
2375 "the refusal repeats rather than shrinking into a partial listing"
2376 );
2377 drop(source);
2378 let _ = std::fs::remove_dir_all(&root);
2379 }
2380}