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;
16const DEFAULT_PRIMARY_BYTES: u64 = 64 << 20;
17
18fn parse_primary_limit(value: Option<&std::ffi::OsStr>) -> Result<u64, Error> {
19 let Some(value) = value else {
20 return Ok(DEFAULT_PRIMARY_BYTES);
21 };
22 let limit = value
23 .to_str()
24 .filter(|value| !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()))
25 .and_then(|value| value.parse::<u64>().ok())
26 .filter(|&limit| limit > 0 && isize::try_from(limit).is_ok());
27 limit.ok_or_else(|| Error::new(&crate::codes::REQUEST_SOURCE_INVALID_LIMIT,
28 "POWERIO_MAX_PRIMARY_BYTES must be a positive decimal byte count within this platform's allocation limit"))
29}
30
31#[derive(Clone, Copy)]
32enum ReadBudget {
33 Primary(u64),
34 Referenced(u64),
35}
36
37impl ReadBudget {
38 fn bytes(self) -> u64 {
39 match self {
40 Self::Primary(n) | Self::Referenced(n) => n,
41 }
42 }
43 fn exceeded(self, name: &str) -> Error {
44 match self {
45 Self::Primary(limit) => Error::new(
46 &crate::codes::READ_IO_PRIMARY_BUDGET,
47 format!("primary source `{name}` exceeds its {limit} byte limit"),
48 ),
49 Self::Referenced(_) => Error::new(
50 &crate::codes::READ_IO_REFERENCE_BUDGET,
51 format!(
52 "referenced file `{name}` would take this source past its {MAX_REFERENCED_BYTES} byte acquisition budget"
53 ),
54 ),
55 }
56 }
57}
58
59const MAX_REFERENCED_DEPTH: usize = 64;
61
62const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
63
64#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct FormatId(Box<str>);
67
68impl FormatId {
69 pub fn new(id: impl Into<String>) -> Result<Self, Error> {
71 let id = id.into();
72 if !valid_format_id(&id) {
73 return Err(Error::new(
74 &crate::codes::REQUEST_FORMAT_INVALID_ID,
75 "a format ID must be bounded lower case ASCII segments separated by single hyphens",
76 ));
77 }
78 Ok(Self(id.into_boxed_str()))
79 }
80
81 #[must_use]
82 pub fn as_str(&self) -> &str {
83 &self.0
84 }
85}
86
87impl fmt::Display for FormatId {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 formatter.write_str(&self.0)
90 }
91}
92
93fn valid_format_id(id: &str) -> bool {
94 if id.is_empty() || id.len() > MAX_FORMAT_ID_BYTES {
95 return false;
96 }
97 let bytes = id.as_bytes();
98 if !bytes[0].is_ascii_lowercase() || bytes.last() == Some(&b'-') {
99 return false;
100 }
101 let mut previous_hyphen = false;
102 for byte in bytes {
103 if *byte == b'-' {
104 if previous_hyphen {
105 return false;
106 }
107 previous_hyphen = true;
108 } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
109 previous_hyphen = false;
110 } else {
111 return false;
112 }
113 }
114 true
115}
116
117#[derive(Debug)]
118struct SourceBufferData {
119 id: SourceId,
120 name: Box<str>,
121 bytes: Arc<[u8]>,
122 directory: Box<[Box<str>]>,
126}
127
128#[derive(Clone, Debug)]
130pub struct SourceBuffer(Arc<SourceBufferData>);
131
132impl SourceBuffer {
133 fn new(
134 id: SourceId,
135 name: impl Into<String>,
136 bytes: Arc<[u8]>,
137 directory: Vec<String>,
138 ) -> Self {
139 Self(Arc::new(SourceBufferData {
140 id,
141 name: name.into().into_boxed_str(),
142 bytes,
143 directory: directory.into_iter().map(String::into_boxed_str).collect(),
144 }))
145 }
146
147 #[must_use]
148 pub fn id(&self) -> &SourceId {
149 &self.0.id
150 }
151
152 #[must_use]
153 pub fn name(&self) -> &str {
154 &self.0.name
155 }
156
157 #[must_use]
160 pub fn bytes(&self) -> &[u8] {
161 &self.0.bytes
162 }
163
164 #[must_use]
165 pub fn shared_bytes(&self) -> Arc<[u8]> {
166 Arc::clone(&self.0.bytes)
167 }
168
169 #[must_use]
171 pub fn has_utf8_bom(&self) -> bool {
172 self.0.bytes.starts_with(&UTF8_BOM)
173 }
174
175 #[must_use]
179 pub fn content_bytes(&self) -> &[u8] {
180 let bytes: &[u8] = &self.0.bytes;
181 if bytes.starts_with(&UTF8_BOM) {
182 &bytes[UTF8_BOM.len()..]
183 } else {
184 bytes
185 }
186 }
187
188 pub fn directory_segments(&self) -> impl Iterator<Item = &str> {
193 self.0.directory.iter().map(AsRef::as_ref)
194 }
195}
196
197#[derive(Debug)]
210struct FileAcquisition {
211 root_display: PathBuf,
212 selected: bool,
215 state: Mutex<AcquisitionState>,
216}
217
218#[derive(Debug, Default)]
219struct AcquisitionState {
220 root: Option<platform::RootHandle>,
221 cache: BTreeMap<String, SourceBuffer>,
222 listed: Option<Vec<crate::ArtifactPath>>,
227 files: usize,
228 bytes: u64,
229}
230
231impl AcquisitionState {
232 fn pinned_root(&mut self, root_display: &Path) -> Result<&platform::RootHandle, Error> {
235 if self.root.is_none() {
236 let root = platform::open_root(root_display)
237 .map_err(|cause| open_error(&crate::codes::READ_IO_OPEN, root_display, cause))?;
238 self.root = Some(root);
239 }
240 Ok(self.root.as_ref().expect("pinned above"))
241 }
242}
243
244#[derive(Debug)]
245enum SourceProvider {
246 Memory {
247 primary: SourceBuffer,
248 named: BTreeMap<String, SourceBuffer>,
249 },
250 File {
251 primary: SourceBuffer,
252 acquisition: FileAcquisition,
253 },
254 Directory {
255 acquisition: FileAcquisition,
256 },
257}
258
259pub const PRIMARY_SOURCE_ID: &str = "/input";
267
268#[derive(Clone)]
275pub struct Source {
276 name: Arc<str>,
277 provider: Arc<SourceProvider>,
278 declared_format: Option<FormatId>,
279}
280
281impl Source {
282 pub fn open(path: impl Into<PathBuf>) -> Result<Self, Error> {
288 let path = path.into();
289 if path.as_os_str().is_empty() {
290 return Err(Error::new(
291 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
292 "source path cannot be empty",
293 ));
294 }
295 let name: Arc<str> = path.to_string_lossy().into_owned().into();
296
297 let file = match platform::open_no_follow(&path) {
302 Ok(file) => file,
303 Err(error) if platform::is_symlink_refusal(&error) => {
304 return Err(Error::new(
305 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
306 format!("source `{}` is a symbolic link", path.display()),
307 ));
308 }
309 Err(error) if platform::is_directory_open_failure(&error, &path) => {
310 return Self::open_directory(name, &path);
311 }
312 Err(cause) => return Err(open_error(&crate::codes::READ_IO_OPEN, &path, cause)),
313 };
314 let metadata = file
315 .metadata()
316 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, &path, cause))?;
317 if metadata.is_dir() {
318 drop(file);
319 return Self::open_directory(name, &path);
320 }
321 let limit = parse_primary_limit(std::env::var_os("POWERIO_MAX_PRIMARY_BYTES").as_deref())?;
322 let bytes = read_open_file(file, &name, ReadBudget::Primary(limit))?;
323 let root_display = canonical_parent(&path)?;
324 let primary = SourceBuffer::new(
325 SourceId::new(PRIMARY_SOURCE_ID)?,
326 name.to_string(),
327 bytes,
328 Vec::new(),
329 );
330 Ok(Self {
331 name,
332 provider: Arc::new(SourceProvider::File {
333 primary,
334 acquisition: FileAcquisition {
335 root_display,
336 selected: false,
337 state: Mutex::new(AcquisitionState::default()),
338 },
339 }),
340 declared_format: None,
341 })
342 }
343
344 fn open_directory(name: Arc<str>, path: &Path) -> Result<Self, Error> {
345 let root_display = std::fs::canonicalize(path)
346 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, path, cause))?;
347 Ok(Self {
348 name,
349 provider: Arc::new(SourceProvider::Directory {
350 acquisition: FileAcquisition {
351 root_display,
352 selected: false,
353 state: Mutex::new(AcquisitionState::default()),
354 },
355 }),
356 declared_format: None,
357 })
358 }
359
360 pub fn from_memory(
364 name: impl Into<String>,
365 bytes: impl Into<Arc<[u8]>>,
366 ) -> Result<Self, Error> {
367 let name = name.into();
368 if !valid_nonempty_text(&name) {
369 return Err(Error::new(
370 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
371 "an in-memory source requires a nonempty bounded name",
372 ));
373 }
374 let primary = SourceBuffer::new(
375 SourceId::new(PRIMARY_SOURCE_ID)?,
376 name.clone(),
377 bytes.into(),
378 Vec::new(),
379 );
380 Ok(Self {
381 name: name.into(),
382 provider: Arc::new(SourceProvider::Memory {
383 primary,
384 named: BTreeMap::new(),
385 }),
386 declared_format: None,
387 })
388 }
389
390 pub fn with_named_buffer(
395 self,
396 name: impl Into<String>,
397 bytes: impl Into<Arc<[u8]>>,
398 ) -> Result<Self, Error> {
399 let name = name.into();
400 let segments = resolve_segments(&[], &name)?;
401 let key = segments.join("/");
402 let mut provider = Arc::try_unwrap(self.provider).map_err(|_| {
403 Error::new(
404 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
405 "named buffers are supplied while constructing a source, before it is shared",
406 )
407 })?;
408 let SourceProvider::Memory { named, .. } = &mut provider else {
409 return Err(Error::new(
410 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
411 "named buffers belong to in-memory sources; a file source acquires referenced files beneath its root",
412 ));
413 };
414 let directory = segments[..segments.len() - 1].to_vec();
415 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes.into(), directory);
416 named.insert(key, buffer);
417 Ok(Self {
418 name: self.name,
419 provider: Arc::new(provider),
420 declared_format: self.declared_format,
421 })
422 }
423
424 pub fn with_acquisition_root(self, root: impl Into<PathBuf>) -> Result<Self, Error> {
428 let requested = root.into();
429 let SourceProvider::File {
430 primary,
431 acquisition,
432 } = &*self.provider
433 else {
434 return Err(Error::new(
435 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
436 "an acquisition root applies to a file source",
437 ));
438 };
439 let canonical = std::fs::canonicalize(&requested)
440 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, &requested, cause))?;
441 let Ok(remainder) = acquisition.root_display.strip_prefix(&canonical) else {
442 return Err(Error::new(
443 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
444 format!(
445 "the case file directory {} is outside the requested acquisition root {}",
446 acquisition.root_display.display(),
447 canonical.display()
448 ),
449 ));
450 };
451 let mut directory = Vec::new();
452 for component in remainder.components() {
453 let Component::Normal(segment) = component else {
454 return Err(Error::new(
455 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
456 "the acquisition root does not resolve to a plain prefix of the case directory",
457 ));
458 };
459 let Some(segment) = segment.to_str().filter(|text| plain_segment(text)) else {
460 return Err(Error::new(
461 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
462 "the acquisition root does not resolve to a plain prefix of the case directory",
463 ));
464 };
465 directory.push(segment.to_owned());
466 }
467 let primary = SourceBuffer::new(
468 primary.id().clone(),
469 primary.name().to_owned(),
470 primary.shared_bytes(),
471 directory,
472 );
473 Ok(Self {
474 name: self.name,
475 provider: Arc::new(SourceProvider::File {
476 primary,
477 acquisition: FileAcquisition {
478 root_display: canonical,
479 selected: true,
480 state: Mutex::new(AcquisitionState::default()),
481 },
482 }),
483 declared_format: self.declared_format,
484 })
485 }
486
487 #[must_use]
489 pub fn with_format(mut self, format: FormatId) -> Self {
490 self.declared_format = Some(format);
491 self
492 }
493
494 #[must_use]
495 pub fn name(&self) -> &str {
496 &self.name
497 }
498
499 #[must_use]
500 pub const fn format(&self) -> Option<&FormatId> {
501 self.declared_format.as_ref()
502 }
503
504 #[must_use]
505 pub fn is_directory(&self) -> bool {
506 matches!(&*self.provider, SourceProvider::Directory { .. })
507 }
508
509 pub fn primary_buffer(&self) -> Result<SourceBuffer, Error> {
511 match &*self.provider {
512 SourceProvider::Memory { primary, .. } | SourceProvider::File { primary, .. } => {
513 Ok(primary.clone())
514 }
515 SourceProvider::Directory { .. } => Err(Error::new(
516 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
517 "a directory source has no implicit primary buffer",
518 )
519 .with_source(self.clone())),
520 }
521 }
522
523 pub fn buffer(&self, name: &crate::ArtifactPath) -> Result<SourceBuffer, Error> {
526 let SourceProvider::Directory { acquisition } = &*self.provider else {
527 return Err(Error::new(
528 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
529 "named child buffers require a directory source",
530 )
531 .with_source(self.clone()));
532 };
533 let segments = resolve_segments(&[], name.as_str())
534 .map_err(|error| error.with_source(self.clone()))?;
535 acquisition
536 .acquire(&segments)
537 .map_err(|error| error.with_source(self.clone()))
538 }
539
540 pub fn root_buffer(&self, name: &str) -> Result<SourceBuffer, Error> {
545 match &*self.provider {
546 SourceProvider::Memory { named, .. } => {
547 let segments = resolve_segments(&[], name)?;
548 let key = segments.join("/");
549 named.get(&key).cloned().ok_or_else(|| {
550 Error::new(
551 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
552 format!(
553 "referenced buffer `{key}` was not supplied to this in-memory source"
554 ),
555 )
556 .with_source(self.clone())
557 })
558 }
559 SourceProvider::File { acquisition, .. }
560 | SourceProvider::Directory { acquisition } => {
561 let segments = resolve_segments(&[], name)?;
562 acquisition
563 .acquire(&segments)
564 .map_err(|error| error.with_source(self.clone()))
565 }
566 }
567 }
568
569 #[must_use]
575 pub fn selected_acquisition_root(&self) -> Option<&Path> {
576 match &*self.provider {
577 SourceProvider::Memory { .. } | SourceProvider::Directory { .. } => None,
578 SourceProvider::File { acquisition, .. } => acquisition
579 .selected
580 .then_some(acquisition.root_display.as_path()),
581 }
582 }
583
584 pub fn referenced_buffer(
589 &self,
590 referrer: &SourceBuffer,
591 name: &str,
592 ) -> Result<SourceBuffer, Error> {
593 let referrer_directory: Vec<&str> = referrer.directory_segments().collect();
594 match &*self.provider {
595 SourceProvider::Memory { named, .. } => {
596 let segments = resolve_segments(&referrer_directory, name)?;
597 let key = segments.join("/");
598 named.get(&key).cloned().ok_or_else(|| {
599 Error::new(
600 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
601 format!(
602 "referenced buffer `{key}` was not supplied to this in-memory source"
603 ),
604 )
605 .with_source(self.clone())
606 })
607 }
608 SourceProvider::File { acquisition, .. }
609 | SourceProvider::Directory { acquisition } => {
610 let segments = match absolute_to_root_relative(&acquisition.root_display, name) {
611 Some(root_relative) => root_relative?,
612 None => resolve_segments(&referrer_directory, name)?,
613 };
614 acquisition
615 .acquire(&segments)
616 .map_err(|error| error.with_source(self.clone()))
617 }
618 }
619 }
620
621 #[allow(clippy::too_many_lines)] pub fn entry_names(&self) -> Result<Vec<crate::ArtifactPath>, Error> {
628 match &*self.provider {
629 SourceProvider::Memory { named, .. } => named
630 .keys()
631 .map(|name| crate::ArtifactPath::new(name.clone()))
632 .collect(),
633 SourceProvider::File { .. } => Err(Error::new(
634 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
635 "entry listing requires a directory source",
636 )
637 .with_source(self.clone())),
638 SourceProvider::Directory { acquisition } => {
639 struct Frame<Handle> {
653 prefix: Vec<String>,
654 directory: Handle,
655 subdirectories: Vec<String>,
657 }
658
659 let mut state = acquisition.lock();
660 if let Some(listed) = &state.listed {
661 return Ok(listed.clone());
662 }
663 let root = state.pinned_root(&acquisition.root_display)?;
664 let budget_refusal = || {
665 Error::new(
666 &crate::codes::READ_IO_REFERENCE_BUDGET,
667 format!(
668 "the source directory holds more than {MAX_REFERENCED_FILES} entries"
669 ),
670 )
671 };
672 let root_handle = root.duplicate_handle().map_err(|cause| {
673 Error::new(
674 &crate::codes::READ_IO_METADATA,
675 "cannot list the source directory root",
676 )
677 .with_cause(cause)
678 })?;
679 let mut names = Vec::new();
680 let mut undescended = 0usize;
683 let mut frames = Vec::new();
684 let mut arriving = Some((Vec::<String>::new(), root_handle));
685 while let Some((prefix, directory)) = arriving.take() {
686 let allowance = MAX_REFERENCED_FILES
687 .checked_sub(names.len() + undescended)
688 .filter(|allowance| *allowance > 0)
689 .ok_or_else(budget_refusal)?;
690 let entries =
691 platform::list_entries(&directory, allowance).map_err(|cause| {
692 if platform::is_entry_budget(&cause) {
693 budget_refusal()
694 } else {
695 listing_error(&prefix.join("/"), cause)
696 }
697 })?;
698 let mut subdirectories = Vec::new();
699 for (name, is_directory) in entries {
700 if names.len() + undescended + subdirectories.len() >= MAX_REFERENCED_FILES
701 {
702 return Err(budget_refusal());
703 }
704 if is_directory {
705 if prefix.len() + 1 >= MAX_REFERENCED_DEPTH {
706 return Err(Error::new(
707 &crate::codes::READ_IO_REFERENCE_BUDGET,
708 format!(
709 "the source directory nests more than {MAX_REFERENCED_DEPTH} levels deep"
710 ),
711 ));
712 }
713 subdirectories.push(name);
714 } else {
715 let mut child = prefix.clone();
716 child.push(name);
717 names.push(crate::ArtifactPath::new(child.join("/"))?);
718 }
719 }
720 undescended += subdirectories.len();
721 frames.push(Frame {
722 prefix,
723 directory,
724 subdirectories,
725 });
726
727 while let Some(frame) = frames.last_mut() {
732 let Some(name) = frame.subdirectories.pop() else {
733 frames.pop();
734 continue;
735 };
736 undescended -= 1;
737 let mut child = frame.prefix.clone();
738 let handle = platform::open_child_directory(&frame.directory, &name)
739 .map_err(|cause| {
740 child.push(name.clone());
741 listing_error(&child.join("/"), cause)
742 })?;
743 child.push(name);
744 arriving = Some((child, handle));
745 break;
746 }
747 }
748 names.sort();
749 state.listed = Some(names.clone());
750 Ok(names)
751 }
752 }
753 }
754
755 #[must_use]
757 pub fn acquired_buffers(&self) -> Vec<SourceBuffer> {
758 match &*self.provider {
759 SourceProvider::Memory { primary, named } => {
760 let mut buffers = vec![primary.clone()];
761 buffers.extend(named.values().cloned());
762 buffers
763 }
764 SourceProvider::File {
765 primary,
766 acquisition,
767 } => {
768 let mut buffers = vec![primary.clone()];
769 buffers.extend(acquisition.lock().cache.values().cloned());
770 buffers
771 }
772 SourceProvider::Directory { acquisition } => {
773 acquisition.lock().cache.values().cloned().collect()
774 }
775 }
776 }
777}
778
779impl fmt::Debug for Source {
780 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
781 formatter
782 .debug_struct("Source")
783 .field("name", &self.name)
784 .field("is_directory", &self.is_directory())
785 .field("declared_format", &self.declared_format)
786 .field("acquired_buffer_count", &self.acquired_buffers().len())
787 .finish_non_exhaustive()
788 }
789}
790
791impl FileAcquisition {
792 fn lock(&self) -> MutexGuard<'_, AcquisitionState> {
793 self.state
794 .lock()
795 .unwrap_or_else(std::sync::PoisonError::into_inner)
796 }
797
798 fn acquire(&self, segments: &[String]) -> Result<SourceBuffer, Error> {
805 let key = segments.join("/");
806 let mut state = self.lock();
807 if let Some(buffer) = state.cache.get(&key) {
808 return Ok(buffer.clone());
809 }
810 if state.files >= MAX_REFERENCED_FILES {
811 return Err(Error::new(
812 &crate::codes::READ_IO_REFERENCE_BUDGET,
813 format!("this source already acquired {MAX_REFERENCED_FILES} referenced files"),
814 ));
815 }
816 let remaining = MAX_REFERENCED_BYTES.saturating_sub(state.bytes);
817 let root = state.pinned_root(&self.root_display)?;
818 let file = root.open_beneath(segments).map_err(|error| {
819 if platform::is_symlink_refusal(&error) {
820 Error::new(
821 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
822 format!("referenced file `{key}` crosses a symbolic link"),
823 )
824 } else {
825 Error::new(
826 &crate::codes::READ_IO_OPEN,
827 format!("cannot open referenced file `{key}`"),
828 )
829 .with_cause(error)
830 }
831 })?;
832 let bytes = read_open_file(file, &key, ReadBudget::Referenced(remaining))?;
833 let directory = segments[..segments.len() - 1].to_vec();
834 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes, directory);
835 state.files += 1;
836 state.bytes += buffer.bytes().len() as u64;
837 state.cache.insert(key, buffer.clone());
838 Ok(buffer)
839 }
840}
841
842fn plain_segment(segment: &str) -> bool {
848 if segment.is_empty()
849 || segment == "."
850 || segment == ".."
851 || segment.contains(['/', '\\', '\0', ':'])
852 {
853 return false;
854 }
855 let mut components = Path::new(segment).components();
856 matches!(components.next(), Some(Component::Normal(text)) if text == std::ffi::OsStr::new(segment))
857 && components.next().is_none()
858}
859
860fn resolve_segments(referrer_directory: &[&str], name: &str) -> Result<Vec<String>, Error> {
867 if name.is_empty()
868 || name.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
869 || name.contains('\0')
870 || name.starts_with(['/', '\\'])
871 {
872 return Err(Error::new(
873 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
874 "a referenced name must be a nonempty bounded relative path",
875 ));
876 }
877 let mut segments: Vec<String> = referrer_directory
878 .iter()
879 .map(|segment| (*segment).to_owned())
880 .collect();
881 for raw in name.split(['/', '\\']) {
882 match raw {
883 "" | "." => {}
884 ".." => {
885 if segments.pop().is_none() {
886 return Err(Error::new(
887 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
888 format!("referenced name `{name}` resolves outside the acquisition root"),
889 ));
890 }
891 }
892 segment => {
893 if segment.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
894 || !plain_segment(segment)
895 {
896 return Err(Error::new(
897 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
898 format!("referenced name `{name}` is not a portable relative path"),
899 ));
900 }
901 segments.push(segment.to_owned());
902 }
903 }
904 }
905 if segments.is_empty() {
906 return Err(Error::new(
907 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
908 format!("referenced name `{name}` does not name a file"),
909 ));
910 }
911 if segments.len() > MAX_REFERENCED_DEPTH {
912 return Err(Error::new(
913 &crate::codes::READ_IO_REFERENCE_BUDGET,
914 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
915 ));
916 }
917 Ok(segments)
918}
919
920fn absolute_to_root_relative(root: &Path, name: &str) -> Option<Result<Vec<String>, Error>> {
926 let path = Path::new(name);
927 if !path.is_absolute() {
928 return None;
929 }
930 let Ok(remainder) = path.strip_prefix(root) else {
931 return Some(Err(Error::new(
932 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
933 format!("referenced name `{name}` resolves outside the acquisition root"),
934 )));
935 };
936 let mut segments = Vec::new();
937 for component in remainder.components() {
938 let text = match component {
939 Component::Normal(text) => text.to_str(),
940 _ => None,
941 };
942 let Some(text) = text.filter(|text| plain_segment(text)) else {
943 return Some(Err(Error::new(
944 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
945 format!("referenced name `{name}` is not a portable path beneath the root"),
946 )));
947 };
948 segments.push(text.to_owned());
949 }
950 if segments.is_empty() {
951 return Some(Err(Error::new(
952 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
953 format!("referenced name `{name}` does not name a file"),
954 )));
955 }
956 if segments.len() > MAX_REFERENCED_DEPTH {
957 return Some(Err(Error::new(
958 &crate::codes::READ_IO_REFERENCE_BUDGET,
959 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
960 )));
961 }
962 Some(Ok(segments))
963}
964
965fn canonical_parent(path: &Path) -> Result<PathBuf, Error> {
966 let parent = match path.parent() {
967 Some(parent) if !parent.as_os_str().is_empty() => parent,
968 _ => Path::new("."),
969 };
970 std::fs::canonicalize(parent)
971 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, parent, cause))
972}
973
974fn open_error(info: &'static crate::DiagnosticInfo, path: &Path, cause: std::io::Error) -> Error {
975 Error::new(info, format!("cannot open source `{}`", path.display())).with_cause(cause)
976}
977
978fn listing_error(display: &str, cause: std::io::Error) -> Error {
981 if platform::is_symlink_refusal(&cause) {
982 Error::new(
983 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
984 format!("source directory `{display}` crosses a symbolic link"),
985 )
986 } else {
987 Error::new(
988 &crate::codes::READ_IO_METADATA,
989 format!("cannot list source directory `{display}`"),
990 )
991 .with_cause(cause)
992 }
993}
994
995fn read_open_file(file: std::fs::File, name: &str, budget: ReadBudget) -> Result<Arc<[u8]>, Error> {
1002 let max_bytes = budget.bytes();
1003 let metadata = file.metadata().map_err(|cause| {
1004 Error::new(
1005 &crate::codes::READ_IO_METADATA,
1006 format!("cannot inspect source buffer `{name}`"),
1007 )
1008 .with_cause(cause)
1009 })?;
1010 if !metadata.is_file() {
1011 return Err(Error::new(
1012 &crate::codes::REQUEST_SOURCE_NOT_A_FILE,
1013 format!("source buffer `{name}` is not a regular file"),
1014 ));
1015 }
1016 let declared_length = metadata.len();
1017 if declared_length > max_bytes {
1018 return Err(budget.exceeded(name));
1019 }
1020 let capacity = usize::try_from(declared_length).map_err(|cause| {
1021 Error::new(
1022 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1023 format!("source buffer `{name}` is too large for this platform"),
1024 )
1025 .with_cause(cause)
1026 })?;
1027 let mut bytes = Vec::new();
1028 bytes.try_reserve_exact(capacity).map_err(|cause| {
1029 Error::new(
1030 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1031 format!("cannot reserve {declared_length} bytes for source buffer `{name}`"),
1032 )
1033 .with_cause(cause)
1034 })?;
1035 let read_limit = declared_length
1036 .checked_add(1)
1037 .ok_or_else(|| {
1038 Error::new(
1039 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1040 format!("source buffer `{name}` is too large to read safely"),
1041 )
1042 })?
1043 .min(max_bytes.saturating_add(1));
1044 let mut file = file;
1045 file.by_ref()
1046 .take(read_limit)
1047 .read_to_end(&mut bytes)
1048 .map_err(|cause| {
1049 Error::new(
1050 &crate::codes::READ_IO_READ,
1051 format!("cannot read source buffer `{name}`"),
1052 )
1053 .with_cause(cause)
1054 })?;
1055 if bytes.len() != capacity {
1056 return Err(Error::new(
1057 &crate::codes::READ_IO_SOURCE_CHANGED,
1058 format!("source buffer `{name}` changed length while it was read"),
1059 ));
1060 }
1061 Ok(bytes.into())
1062}
1063
1064#[cfg(unix)]
1065mod platform {
1066 use std::ffi::CString;
1076 use std::fs::File;
1077 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
1078 use std::os::unix::ffi::OsStrExt;
1079 use std::path::Path;
1080
1081 #[derive(Debug)]
1082 pub(super) struct RootHandle(OwnedFd);
1083
1084 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1087 let path = c_string(path.as_os_str().as_bytes())?;
1088 let fd = unsafe {
1092 libc::open(
1093 path.as_ptr(),
1094 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1095 )
1096 };
1097 if fd < 0 {
1098 return Err(std::io::Error::last_os_error());
1099 }
1100 let file = unsafe { File::from_raw_fd(fd) };
1102 clear_nonblock(&file)?;
1103 Ok(file)
1104 }
1105
1106 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1111 let path = c_string(path.as_os_str().as_bytes())?;
1112 let fd = unsafe {
1114 libc::open(
1115 path.as_ptr(),
1116 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1117 )
1118 };
1119 if fd < 0 {
1120 return Err(std::io::Error::last_os_error());
1121 }
1122 let file = unsafe { File::from_raw_fd(fd) };
1124 if !file.metadata()?.is_dir() {
1125 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1126 }
1127 clear_nonblock(&file)?;
1128 Ok(RootHandle(file.into()))
1129 }
1130
1131 impl RootHandle {
1132 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1133 let mut directory: Option<OwnedFd> = None;
1134 let (file_segment, directories) =
1135 segments.split_last().expect("resolution yields a file");
1136 for segment in directories {
1137 let next = self.open_at(
1142 directory.as_ref(),
1143 segment,
1144 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1145 )?;
1146 let next = File::from(next);
1147 if !next.metadata()?.is_dir() {
1148 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1149 }
1150 directory = Some(next.into());
1151 }
1152 let fd = self.open_at(
1153 directory.as_ref(),
1154 file_segment,
1155 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1156 )?;
1157 let file = File::from(fd);
1158 clear_nonblock(&file)?;
1159 Ok(file)
1160 }
1161
1162 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1170 let segment = c_string(b".")?;
1171 let fd = unsafe {
1174 libc::openat(
1175 self.0.as_raw_fd(),
1176 segment.as_ptr(),
1177 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY,
1178 )
1179 };
1180 if fd < 0 {
1181 return Err(std::io::Error::last_os_error());
1182 }
1183 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1185 }
1186
1187 fn open_at(
1188 &self,
1189 directory: Option<&OwnedFd>,
1190 segment: &str,
1191 flags: libc::c_int,
1192 ) -> std::io::Result<OwnedFd> {
1193 if !super::plain_segment(segment) {
1194 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1195 }
1196 let at = directory.map_or_else(|| self.0.as_raw_fd(), AsRawFd::as_raw_fd);
1197 let segment = c_string(segment.as_bytes())?;
1198 let fd = unsafe { libc::openat(at, segment.as_ptr(), flags) };
1202 if fd < 0 {
1203 return Err(std::io::Error::last_os_error());
1204 }
1205 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1207 }
1208 }
1209
1210 pub(super) type DirectoryHandle = OwnedFd;
1213
1214 pub(super) fn open_child_directory(
1218 parent: &DirectoryHandle,
1219 name: &str,
1220 ) -> std::io::Result<DirectoryHandle> {
1221 if !super::plain_segment(name) {
1222 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1223 }
1224 let segment = c_string(name.as_bytes())?;
1225 let fd = unsafe {
1229 libc::openat(
1230 parent.as_raw_fd(),
1231 segment.as_ptr(),
1232 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1233 )
1234 };
1235 if fd < 0 {
1236 return Err(std::io::Error::last_os_error());
1237 }
1238 let file = unsafe { File::from_raw_fd(fd) };
1240 if !file.metadata()?.is_dir() {
1241 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1242 }
1243 Ok(file.into())
1244 }
1245
1246 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1247
1248 fn entry_budget_error() -> std::io::Error {
1249 std::io::Error::other(ENTRY_BUDGET_MARKER)
1250 }
1251
1252 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1255 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1256 }
1257
1258 pub(super) fn list_entries(
1265 directory: &DirectoryHandle,
1266 max: usize,
1267 ) -> std::io::Result<Vec<(String, bool)>> {
1268 use std::os::fd::IntoRawFd;
1269
1270 let raw = directory.try_clone()?.into_raw_fd();
1273 let stream = unsafe { libc::fdopendir(raw) };
1276 if stream.is_null() {
1277 let error = std::io::Error::last_os_error();
1278 unsafe { libc::close(raw) };
1281 return Err(error);
1282 }
1283 let mut entries = Vec::new();
1284 loop {
1285 errno_clear();
1287 let entry = unsafe { libc::readdir(stream) };
1288 if entry.is_null() {
1289 let error = std::io::Error::last_os_error();
1290 unsafe { libc::closedir(stream) };
1293 if error.raw_os_error().is_some_and(|code| code != 0) {
1294 return Err(error);
1295 }
1296 break;
1297 }
1298 let name_bytes = unsafe {
1303 std::ffi::CStr::from_ptr((&raw const (*entry).d_name).cast::<libc::c_char>())
1304 };
1305 let Ok(name) = name_bytes.to_str() else {
1306 continue;
1307 };
1308 if name == "." || name == ".." {
1309 continue;
1310 }
1311 if entries.len() == max {
1312 unsafe { libc::closedir(stream) };
1314 return Err(entry_budget_error());
1315 }
1316 let kind = unsafe { (*entry).d_type };
1318 let is_directory = match kind {
1319 libc::DT_DIR => true,
1320 libc::DT_UNKNOWN => {
1321 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1324 let segment =
1325 c_string(name.as_bytes()).expect("directory entry names carry no NUL");
1326 let status = unsafe {
1330 libc::fstatat(
1331 libc::dirfd(stream),
1332 segment.as_ptr(),
1333 &raw mut stat,
1334 libc::AT_SYMLINK_NOFOLLOW,
1335 )
1336 };
1337 status == 0 && stat.st_mode & libc::S_IFMT == libc::S_IFDIR
1338 }
1339 _ => false,
1340 };
1341 entries.push((name.to_owned(), is_directory));
1342 }
1343 Ok(entries)
1344 }
1345
1346 fn errno_clear() {
1347 errno::set_errno(errno::Errno(0));
1348 }
1349
1350 fn clear_nonblock(file: &File) -> std::io::Result<()> {
1352 let fd = file.as_raw_fd();
1353 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1356 if flags < 0 {
1357 return Err(std::io::Error::last_os_error());
1358 }
1359 let status = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1361 if status < 0 {
1362 return Err(std::io::Error::last_os_error());
1363 }
1364 Ok(())
1365 }
1366
1367 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1368 matches!(error.raw_os_error(), Some(libc::ELOOP | libc::EMLINK))
1369 }
1370
1371 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1372 let _ = path;
1376 error.raw_os_error() == Some(libc::EISDIR)
1377 }
1378
1379 fn c_string(bytes: &[u8]) -> std::io::Result<CString> {
1380 CString::new(bytes).map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))
1381 }
1382
1383 #[cfg(test)]
1384 mod tests {
1385 use super::*;
1386
1387 #[test]
1388 fn open_root_refuses_a_symbolic_link_to_a_directory() {
1389 let base = std::env::temp_dir().join(format!(
1390 "powerio-core-open-root-{}-{}",
1391 std::process::id(),
1392 std::time::SystemTime::now()
1393 .duration_since(std::time::UNIX_EPOCH)
1394 .unwrap()
1395 .as_nanos()
1396 ));
1397 std::fs::create_dir_all(&base).unwrap();
1398 assert!(open_root(&base).is_ok());
1399 let link = base.join("link");
1400 std::os::unix::fs::symlink(&base, &link).unwrap();
1401 let error = open_root(&link).unwrap_err();
1402 assert!(
1403 is_symlink_refusal(&error) || error.kind() == std::io::ErrorKind::NotADirectory,
1404 "{error:?}"
1405 );
1406 std::fs::remove_dir_all(&base).unwrap();
1407 }
1408 }
1409}
1410
1411#[cfg(not(unix))]
1412mod platform {
1413 use std::fs::File;
1421 use std::path::{Path, PathBuf};
1422
1423 #[derive(Debug)]
1424 pub(super) struct RootHandle {
1425 root: PathBuf,
1426 }
1427
1428 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1429 let file = open_reparse_refused(path)?;
1430 Ok(file)
1431 }
1432
1433 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1434 drop(open_directory_pinned(path)?);
1438 Ok(RootHandle {
1439 root: path.to_path_buf(),
1440 })
1441 }
1442
1443 impl RootHandle {
1444 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1445 let mut path = self.root.clone();
1446 let (file_segment, directories) =
1447 segments.split_last().expect("resolution yields a file");
1448 let mut held = Vec::with_capacity(directories.len() + 1);
1452 held.push(open_directory_pinned(&self.root)?);
1453 for segment in directories {
1454 push_plain_segment(&mut path, segment)?;
1455 held.push(open_directory_pinned(&path)?);
1456 }
1457 push_plain_segment(&mut path, file_segment)?;
1458 let file = open_reparse_refused(&path)?;
1459 drop(held);
1460 Ok(file)
1461 }
1462
1463 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1466 let handle = open_directory_pinned(&self.root)?;
1467 Ok(DirectoryHandle {
1468 path: self.root.clone(),
1469 _handle: handle,
1470 })
1471 }
1472 }
1473
1474 #[derive(Debug)]
1478 pub(super) struct DirectoryHandle {
1479 path: PathBuf,
1480 _handle: File,
1481 }
1482
1483 pub(super) fn open_child_directory(
1487 parent: &DirectoryHandle,
1488 name: &str,
1489 ) -> std::io::Result<DirectoryHandle> {
1490 let mut path = parent.path.clone();
1491 push_plain_segment(&mut path, name)?;
1492 let handle = open_directory_pinned(&path)?;
1493 Ok(DirectoryHandle {
1494 path,
1495 _handle: handle,
1496 })
1497 }
1498
1499 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1500
1501 fn entry_budget_error() -> std::io::Error {
1502 std::io::Error::other(ENTRY_BUDGET_MARKER)
1503 }
1504
1505 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1508 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1509 }
1510
1511 pub(super) fn list_entries(
1516 directory: &DirectoryHandle,
1517 max: usize,
1518 ) -> std::io::Result<Vec<(String, bool)>> {
1519 let mut entries = Vec::new();
1520 for entry in std::fs::read_dir(&directory.path)? {
1521 let entry = entry?;
1522 let Ok(name) = entry.file_name().into_string() else {
1523 continue;
1524 };
1525 if entries.len() == max {
1526 return Err(entry_budget_error());
1527 }
1528 let is_directory = entry.file_type()?.is_dir();
1529 entries.push((name, is_directory));
1530 }
1531 Ok(entries)
1532 }
1533
1534 fn push_plain_segment(path: &mut PathBuf, segment: &str) -> std::io::Result<()> {
1537 if !super::plain_segment(segment) {
1538 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1539 }
1540 path.push(segment);
1541 Ok(())
1542 }
1543
1544 #[cfg(windows)]
1550 fn open_directory_pinned(path: &Path) -> std::io::Result<File> {
1551 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1552
1553 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1554 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1555 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1556 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
1557 const FILE_SHARE_READ: u32 = 0x0000_0001;
1558 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
1559
1560 let file = std::fs::OpenOptions::new()
1561 .read(true)
1562 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1563 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
1564 .open(path)?;
1565 let attributes = file.metadata()?.file_attributes();
1566 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1567 return Err(symlink_error());
1568 }
1569 if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1570 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1571 }
1572 Ok(file)
1573 }
1574
1575 #[cfg(not(windows))]
1576 fn open_directory_pinned(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 if !metadata.is_dir() {
1582 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1583 }
1584 File::open(path)
1585 }
1586
1587 #[cfg(windows)]
1588 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1589 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1590
1591 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1592 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1593
1594 let file = std::fs::OpenOptions::new()
1595 .read(true)
1596 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
1597 .open(path)?;
1598 let attributes = file.metadata()?.file_attributes();
1599 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1600 return Err(symlink_error());
1601 }
1602 Ok(file)
1603 }
1604
1605 #[cfg(not(windows))]
1606 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1607 let metadata = std::fs::symlink_metadata(path)?;
1608 if metadata.file_type().is_symlink() {
1609 return Err(symlink_error());
1610 }
1611 File::open(path)
1612 }
1613
1614 fn symlink_error() -> std::io::Error {
1615 std::io::Error::new(std::io::ErrorKind::InvalidData, "symbolic link refused")
1616 }
1617
1618 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1619 error.kind() == std::io::ErrorKind::InvalidData
1620 && error.to_string().contains("symbolic link refused")
1621 }
1622
1623 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1624 let _ = error;
1625 std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
1626 }
1627}
1628
1629pub const MEMORY_SOURCE_NAME: &str = "<memory>";
1633
1634pub trait IntoSource {
1645 fn into_source(self) -> Result<Source, Error>;
1651}
1652
1653impl IntoSource for Source {
1654 fn into_source(self) -> Result<Source, Error> {
1655 Ok(self)
1656 }
1657}
1658
1659impl IntoSource for &Source {
1660 fn into_source(self) -> Result<Source, Error> {
1661 Ok(self.clone())
1662 }
1663}
1664
1665macro_rules! into_source_by_name {
1667 ($($input:ty),* $(,)?) => {
1668 $(
1669 impl IntoSource for $input {
1670 fn into_source(self) -> Result<Source, Error> {
1671 Source::open(PathBuf::from(self))
1672 }
1673 }
1674 )*
1675 };
1676}
1677
1678into_source_by_name!(&str, &String, String, &Path, &PathBuf, PathBuf);
1679
1680macro_rules! into_source_by_content {
1682 ($($input:ty => $bytes:expr),* $(,)?) => {
1683 $(
1684 impl IntoSource for $input {
1685 fn into_source(self) -> Result<Source, Error> {
1686 let bytes: Arc<[u8]> = $bytes(self);
1687 Source::from_memory(MEMORY_SOURCE_NAME, bytes)
1688 }
1689 }
1690 )*
1691 };
1692}
1693
1694into_source_by_content!(
1695 &[u8] => Arc::<[u8]>::from,
1696 Vec<u8> => Arc::<[u8]>::from,
1697 Arc<[u8]> => std::convert::identity,
1698);
1699
1700impl<const N: usize> IntoSource for &[u8; N] {
1701 fn into_source(self) -> Result<Source, Error> {
1702 Source::from_memory(MEMORY_SOURCE_NAME, Arc::<[u8]>::from(self.as_slice()))
1703 }
1704}
1705
1706#[cfg(test)]
1707mod tests {
1708 use std::time::{SystemTime, UNIX_EPOCH};
1709
1710 use super::*;
1711 use crate::ArtifactPath;
1712
1713 static PROCESS_RESOURCE_TESTS: Mutex<()> = Mutex::new(());
1718
1719 fn process_resource_guard() -> MutexGuard<'static, ()> {
1720 PROCESS_RESOURCE_TESTS
1721 .lock()
1722 .unwrap_or_else(std::sync::PoisonError::into_inner)
1723 }
1724
1725 struct CountingAllocator;
1728
1729 static ALLOCATED_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1730
1731 thread_local! {
1732 static MEASURING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1733 }
1734
1735 fn measured_bytes<T>(work: impl FnOnce() -> T) -> (T, usize) {
1736 let _ = MEASURING.try_with(|flag| flag.set(true));
1737 let before = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1738 let value = work();
1739 let after = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1740 let _ = MEASURING.try_with(|flag| flag.set(false));
1741 (value, after.saturating_sub(before))
1742 }
1743
1744 unsafe impl std::alloc::GlobalAlloc for CountingAllocator {
1747 unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
1748 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1749 ALLOCATED_BYTES.fetch_add(layout.size(), std::sync::atomic::Ordering::Relaxed);
1750 }
1751 unsafe { std::alloc::System.alloc(layout) }
1752 }
1753
1754 unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
1755 unsafe { std::alloc::System.dealloc(ptr, layout) }
1756 }
1757
1758 unsafe fn realloc(
1759 &self,
1760 ptr: *mut u8,
1761 layout: std::alloc::Layout,
1762 new_size: usize,
1763 ) -> *mut u8 {
1764 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1765 ALLOCATED_BYTES.fetch_add(new_size, std::sync::atomic::Ordering::Relaxed);
1766 }
1767 unsafe { std::alloc::System.realloc(ptr, layout, new_size) }
1768 }
1769 }
1770
1771 #[global_allocator]
1772 static COUNTING: CountingAllocator = CountingAllocator;
1773
1774 fn test_root(name: &str) -> PathBuf {
1775 let nonce = SystemTime::now()
1776 .duration_since(UNIX_EPOCH)
1777 .unwrap()
1778 .as_nanos();
1779 std::env::temp_dir().join(format!(
1780 "powerio-core-{name}-{}-{nonce}",
1781 std::process::id()
1782 ))
1783 }
1784
1785 #[test]
1786 fn format_ids_use_the_exact_open_grammar() {
1787 for id in ["matpower", "psse-raw", "doe-go-3", "x1"] {
1788 assert_eq!(FormatId::new(id).unwrap().as_str(), id);
1789 }
1790 for id in ["", "1matpower", "MATPOWER", "psse--raw", "psse-", "p_sse"] {
1791 assert!(FormatId::new(id).is_err(), "{id}");
1792 }
1793 assert!(FormatId::new("a".repeat(MAX_FORMAT_ID_BYTES + 1)).is_err());
1794 }
1795
1796 #[test]
1797 fn memory_sources_retain_arbitrary_binary_bytes_without_copying() {
1798 let bytes: Arc<[u8]> = vec![0, 255, 0, 128].into();
1799 let pointer = bytes.as_ptr();
1800 let source = Source::from_memory("input.bin", Arc::clone(&bytes))
1801 .unwrap()
1802 .with_format(FormatId::new("pwb").unwrap());
1803 let buffer = source.primary_buffer().unwrap();
1804 assert_eq!(buffer.bytes(), [0, 255, 0, 128]);
1805 assert_eq!(buffer.bytes().as_ptr(), pointer);
1806 assert_eq!(source.format().unwrap().as_str(), "pwb");
1807 assert!(Source::from_memory("", Vec::new()).is_err());
1808 assert!(Source::from_memory("x\0y", Vec::new()).is_err());
1809 }
1810
1811 #[test]
1812 fn a_bom_is_retained_and_skipped_for_the_parser_without_a_second_buffer() {
1813 let bytes: Vec<u8> = [0xEF, 0xBB, 0xBF, b'm', b'p', b'c'].to_vec();
1814 let source = Source::from_memory("case.m", bytes).unwrap();
1815 let buffer = source.primary_buffer().unwrap();
1816 assert!(buffer.has_utf8_bom());
1817 assert_eq!(buffer.bytes().len(), 6);
1818 assert_eq!(buffer.content_bytes(), b"mpc");
1819 assert_eq!(
1821 buffer.content_bytes().as_ptr(),
1822 buffer.bytes()[3..].as_ptr()
1823 );
1824
1825 let plain = Source::from_memory("case.m", b"mpc".to_vec()).unwrap();
1826 let plain = plain.primary_buffer().unwrap();
1827 assert!(!plain.has_utf8_bom());
1828 assert_eq!(plain.content_bytes(), plain.bytes());
1829 }
1830
1831 #[test]
1832 fn a_memory_source_resolves_named_buffers_and_never_the_filesystem() {
1833 let source = Source::from_memory("master.dss", b"redirect sub/feeder.dss".to_vec())
1834 .unwrap()
1835 .with_named_buffer("sub/feeder.dss", b"feeder".to_vec())
1836 .unwrap();
1837 let primary = source.primary_buffer().unwrap();
1838 let feeder = source
1839 .referenced_buffer(&primary, "sub/feeder.dss")
1840 .unwrap();
1841 assert_eq!(feeder.bytes(), b"feeder");
1842
1843 let sibling = source
1846 .referenced_buffer(&feeder, "../sub/feeder.dss")
1847 .unwrap();
1848 assert_eq!(sibling.bytes(), b"feeder");
1849
1850 assert!(source.referenced_buffer(&primary, "missing.dss").is_err());
1851 let escape = source.referenced_buffer(&primary, "../outside.dss");
1852 assert_eq!(
1853 escape.unwrap_err().category(),
1854 crate::ErrorCategory::Request
1855 );
1856 }
1857
1858 #[test]
1859 fn a_file_source_acquires_referenced_files_beneath_its_containing_directory() {
1860 let root = test_root("file-refs");
1861 std::fs::create_dir_all(root.join("sub")).unwrap();
1862 std::fs::write(root.join("master.dss"), b"master").unwrap();
1863 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
1864 std::fs::write(root.join("sub/coords.csv"), b"coords").unwrap();
1865
1866 let source = Source::open(root.join("master.dss")).unwrap();
1867 let primary = source.primary_buffer().unwrap();
1868 assert_eq!(primary.bytes(), b"master");
1869
1870 let feeder = source
1871 .referenced_buffer(&primary, "sub/feeder.dss")
1872 .unwrap();
1873 assert_eq!(feeder.bytes(), b"feeder");
1874 let coords = source.referenced_buffer(&feeder, "coords.csv").unwrap();
1877 assert_eq!(coords.bytes(), b"coords");
1878 let master_again = source.referenced_buffer(&feeder, "../master.dss").unwrap();
1880 assert_eq!(master_again.bytes(), b"master");
1881 let escape = source.referenced_buffer(&primary, "../escape.dss");
1882 assert_eq!(
1883 escape.unwrap_err().category(),
1884 crate::ErrorCategory::Request
1885 );
1886
1887 let again = source
1889 .referenced_buffer(&primary, "sub/feeder.dss")
1890 .unwrap();
1891 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
1892 assert_eq!(source.acquired_buffers().len(), 4);
1893 std::fs::remove_dir_all(root).unwrap();
1894 }
1895
1896 #[test]
1897 fn an_explicitly_wider_root_admits_shared_files_and_still_confines() {
1898 let root = test_root("wider-root");
1899 std::fs::create_dir_all(root.join("cases")).unwrap();
1900 std::fs::create_dir_all(root.join("shared")).unwrap();
1901 std::fs::write(root.join("cases/master.dss"), b"master").unwrap();
1902 std::fs::write(root.join("shared/wires.dss"), b"wires").unwrap();
1903
1904 let narrow = Source::open(root.join("cases/master.dss")).unwrap();
1906 let primary = narrow.primary_buffer().unwrap();
1907 assert!(
1908 narrow
1909 .referenced_buffer(&primary, "../shared/wires.dss")
1910 .is_err()
1911 );
1912
1913 let wide = Source::open(root.join("cases/master.dss"))
1915 .unwrap()
1916 .with_acquisition_root(&root)
1917 .unwrap();
1918 let primary = wide.primary_buffer().unwrap();
1919 let wires = wide
1920 .referenced_buffer(&primary, "../shared/wires.dss")
1921 .unwrap();
1922 assert_eq!(wires.bytes(), b"wires");
1923 assert!(
1924 wide.referenced_buffer(&primary, "../../etc/passwd")
1925 .is_err()
1926 );
1927
1928 let outside = test_root("wider-root-outside");
1930 std::fs::create_dir_all(&outside).unwrap();
1931 assert!(
1932 Source::open(root.join("cases/master.dss"))
1933 .unwrap()
1934 .with_acquisition_root(&outside)
1935 .is_err()
1936 );
1937 std::fs::remove_dir_all(outside).ok();
1938 std::fs::remove_dir_all(root).unwrap();
1939 }
1940
1941 #[test]
1942 fn directory_buffers_are_lazy_cached_and_binary_safe() {
1943 let root = test_root("directory");
1944 std::fs::create_dir_all(root.join("nested")).unwrap();
1945 std::fs::write(root.join("nested/data.bin"), [0, 255, 7]).unwrap();
1946 let source = Source::open(&root).unwrap();
1947 assert!(source.is_directory());
1948 assert!(source.acquired_buffers().is_empty());
1949 let name = ArtifactPath::new("nested/data.bin").unwrap();
1950 let first = source.buffer(&name).unwrap();
1951 let second = source.buffer(&name).unwrap();
1952 assert_eq!(first.bytes(), [0, 255, 7]);
1953 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
1954 assert_eq!(source.acquired_buffers().len(), 1);
1955 std::fs::remove_dir_all(root).unwrap();
1956 }
1957
1958 #[cfg(unix)]
1959 #[test]
1960 fn source_acquisition_refuses_root_and_child_symlinks() {
1961 use std::os::unix::fs::symlink;
1962
1963 let root = test_root("symlink");
1964 std::fs::create_dir_all(&root).unwrap();
1965 std::fs::write(root.join("real.bin"), b"real").unwrap();
1966 symlink(root.join("real.bin"), root.join("link.bin")).unwrap();
1967 let source = Source::open(&root).unwrap();
1968 let error = source
1969 .buffer(&ArtifactPath::new("link.bin").unwrap())
1970 .unwrap_err();
1971 assert_eq!(error.category(), crate::ErrorCategory::Request);
1972
1973 std::fs::create_dir_all(root.join("real-dir")).unwrap();
1975 std::fs::write(root.join("real-dir/inner.bin"), b"inner").unwrap();
1976 symlink(root.join("real-dir"), root.join("link-dir")).unwrap();
1977 let error = source
1978 .buffer(&ArtifactPath::new("link-dir/inner.bin").unwrap())
1979 .unwrap_err();
1980 assert_eq!(error.category(), crate::ErrorCategory::Request);
1981
1982 let root_link = root.with_extension("link");
1983 symlink(&root, &root_link).unwrap();
1984 assert!(Source::open(&root_link).is_err());
1985 std::fs::remove_file(root_link).unwrap();
1986 std::fs::remove_dir_all(root).unwrap();
1987 }
1988
1989 #[cfg(unix)]
1990 #[test]
1991 fn a_named_pipe_is_refused_promptly_and_siblings_still_acquire() {
1992 use std::os::unix::ffi::OsStrExt;
1993
1994 let root = test_root("fifo");
1995 std::fs::create_dir_all(&root).unwrap();
1996 std::fs::write(root.join("real.csv"), b"real").unwrap();
1997 let fifo = root.join("pipe.dat");
1998 let c_path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
1999 assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
2002
2003 let (sender, receiver) = std::sync::mpsc::channel();
2007 let opened_root = root.clone();
2008 let worker = std::thread::spawn(move || {
2009 let open_error = Source::open(opened_root.join("pipe.dat")).map(|_| ());
2010 let directory = Source::open(&opened_root).unwrap();
2011 let buffer_error = directory
2012 .buffer(&ArtifactPath::new("pipe.dat").unwrap())
2013 .map(|_| ());
2014 let sibling = directory
2015 .buffer(&ArtifactPath::new("real.csv").unwrap())
2016 .map(|buffer| buffer.bytes().to_vec());
2017 sender.send((open_error, buffer_error, sibling)).unwrap();
2018 });
2019 let (open_error, buffer_error, sibling) = receiver
2020 .recv_timeout(std::time::Duration::from_secs(10))
2021 .expect("acquisition on a writerless pipe completes promptly");
2022 worker.join().unwrap();
2023 assert_eq!(
2024 open_error.unwrap_err().category(),
2025 crate::ErrorCategory::Request
2026 );
2027 assert_eq!(
2028 buffer_error.unwrap_err().category(),
2029 crate::ErrorCategory::Request
2030 );
2031 assert_eq!(sibling.unwrap(), b"real");
2032 std::fs::remove_dir_all(root).unwrap();
2033 }
2034
2035 #[test]
2036 fn primary_limits_validate_configuration_and_bound_allocation() {
2037 use std::ffi::OsStr;
2038 assert_eq!(parse_primary_limit(None).unwrap(), DEFAULT_PRIMARY_BYTES);
2039 assert_eq!(parse_primary_limit(Some(OsStr::new("32"))).unwrap(), 32);
2040 for value in ["", "0", "-1", "+1", " 32", "3KiB", "18446744073709551615"] {
2041 assert!(parse_primary_limit(Some(OsStr::new(value))).is_err());
2042 }
2043 let root = test_root("primary-budget");
2044 std::fs::create_dir_all(&root).unwrap();
2045 let path = root.join("input");
2046 std::fs::write(&path, b"abcd").unwrap();
2047 for limit in [4, 5] {
2048 let bytes = read_open_file(
2049 std::fs::File::open(&path).unwrap(),
2050 "input",
2051 ReadBudget::Primary(limit),
2052 )
2053 .unwrap();
2054 assert_eq!(&*bytes, b"abcd");
2055 }
2056 assert!(
2057 read_open_file(
2058 std::fs::File::open(&path).unwrap(),
2059 "input",
2060 ReadBudget::Primary(3)
2061 )
2062 .is_err()
2063 );
2064 std::fs::File::create(&path)
2065 .unwrap()
2066 .set_len(DEFAULT_PRIMARY_BYTES * 4)
2067 .unwrap();
2068 let (error, allocated) = measured_bytes(|| {
2069 read_open_file(
2070 std::fs::File::open(&path).unwrap(),
2071 "input",
2072 ReadBudget::Primary(DEFAULT_PRIMARY_BYTES),
2073 )
2074 .unwrap_err()
2075 });
2076 assert!(error.to_string().contains("primary source"));
2077 assert!(allocated < 4096);
2078 std::fs::remove_dir_all(root).unwrap();
2079 }
2080
2081 #[test]
2082 fn an_over_budget_referenced_file_is_refused_before_allocation() {
2083 let root = test_root("budget");
2084 std::fs::create_dir_all(&root).unwrap();
2085 std::fs::write(root.join("master.dss"), b"master").unwrap();
2086 let big = std::fs::File::create(root.join("big.dat")).unwrap();
2090 big.set_len(MAX_REFERENCED_BYTES * 4).unwrap();
2091 drop(big);
2092
2093 let source = Source::open(root.join("master.dss")).unwrap();
2094 let primary = source.primary_buffer().unwrap();
2095 let (error, allocated) =
2100 measured_bytes(|| source.referenced_buffer(&primary, "big.dat").unwrap_err());
2101 assert!(error.to_string().contains("acquisition budget"), "{error}");
2102 assert!(
2103 (allocated as u64) < MAX_REFERENCED_BYTES / 16,
2104 "the refused acquisition allocated {allocated} bytes"
2105 );
2106 std::fs::remove_dir_all(root).unwrap();
2107 }
2108
2109 #[cfg(unix)]
2110 #[test]
2111 fn racing_entry_listing_never_names_files_outside_the_root() {
2112 use std::os::unix::fs::symlink;
2113
2114 let root = test_root("race-list");
2115 std::fs::create_dir_all(root.join("sub")).unwrap();
2116 std::fs::write(root.join("sub/inside.txt"), b"inside").unwrap();
2117 let outside = test_root("race-list-outside");
2118 std::fs::create_dir_all(&outside).unwrap();
2119 std::fs::write(outside.join("outside-only.txt"), b"outside").unwrap();
2120
2121 let source = Source::open(&root).unwrap();
2122 let stop = std::sync::atomic::AtomicBool::new(false);
2123 std::thread::scope(|scope| {
2124 let flipper = scope.spawn(|| {
2125 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
2126 let _ = std::fs::remove_dir_all(root.join("sub"));
2127 let _ = symlink(&outside, root.join("sub"));
2128 let _ = std::fs::remove_file(root.join("sub"));
2129 let _ = std::fs::create_dir(root.join("sub"));
2130 let _ = std::fs::write(root.join("sub/inside.txt"), b"inside");
2131 }
2132 });
2133 for _ in 0..50 {
2134 if let Ok(names) = source.entry_names() {
2137 assert!(
2138 names
2139 .iter()
2140 .all(|name| !name.as_str().contains("outside-only")),
2141 "{names:?}"
2142 );
2143 }
2144 }
2145 stop.store(true, std::sync::atomic::Ordering::Relaxed);
2146 flipper.join().unwrap();
2147 });
2148 std::fs::remove_dir_all(&root).ok();
2149 std::fs::remove_dir_all(&outside).ok();
2150 }
2151
2152 #[test]
2153 fn referenced_names_must_be_portable_relative_paths() {
2154 let root = test_root("portable-names");
2155 std::fs::create_dir_all(root.join("sub")).unwrap();
2156 std::fs::write(root.join("master.dss"), b"master").unwrap();
2157 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
2158 let source = Source::open(root.join("master.dss")).unwrap();
2159 let primary = source.primary_buffer().unwrap();
2160
2161 for name in ["..\\escape.dss", "\\escape.dss", "C:\\escape.dss", "C:x"] {
2164 let error = source.referenced_buffer(&primary, name).unwrap_err();
2165 assert_eq!(
2166 error.category(),
2167 crate::ErrorCategory::Request,
2168 "{name}: {error}"
2169 );
2170 }
2171 assert!(source.root_buffer("..\\master.dss").is_err());
2172 assert!(source.root_buffer("\\master.dss").is_err());
2173
2174 let feeder = source
2176 .referenced_buffer(&primary, "sub/feeder.dss")
2177 .unwrap();
2178 assert_eq!(feeder.bytes(), b"feeder");
2179
2180 let absolute = root.canonicalize().unwrap().join("sub").join("feeder.dss");
2186 let again = source
2187 .referenced_buffer(&primary, absolute.to_str().unwrap())
2188 .unwrap();
2189 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
2190 assert!(
2192 source
2193 .acquired_buffers()
2194 .iter()
2195 .all(|buffer| !buffer.name().contains("escape"))
2196 );
2197 std::fs::remove_dir_all(root).unwrap();
2198 }
2199
2200 #[cfg(unix)]
2201 #[test]
2202 fn live_sources_hold_no_directory_descriptors_before_acquisition() {
2203 fn open_descriptor_count() -> usize {
2204 let table = if cfg!(target_os = "macos") {
2205 "/dev/fd"
2206 } else {
2207 "/proc/self/fd"
2208 };
2209 std::fs::read_dir(table).unwrap().count()
2210 }
2211
2212 let _guard = process_resource_guard();
2213 let root = test_root("fd-count");
2214 std::fs::create_dir_all(&root).unwrap();
2215 std::fs::write(root.join("case.m"), b"case").unwrap();
2216 std::fs::write(root.join("ref.csv"), b"ref").unwrap();
2217
2218 let before = open_descriptor_count();
2219 let sources: Vec<Source> = (0..300)
2220 .map(|_| Source::open(root.join("case.m")).unwrap())
2221 .collect();
2222 let held = open_descriptor_count();
2223 assert!(
2224 held <= before + 4,
2225 "{} sources hold {} descriptors over the baseline {}",
2226 sources.len(),
2227 held - before,
2228 before
2229 );
2230
2231 for source in sources.iter().take(32) {
2236 let primary = source.primary_buffer().unwrap();
2237 let first = source.referenced_buffer(&primary, "ref.csv").unwrap();
2238 let second = source.referenced_buffer(&primary, "ref.csv").unwrap();
2239 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
2240 }
2241 drop(sources);
2242 std::fs::remove_dir_all(root).unwrap();
2243 }
2244
2245 #[test]
2246 fn entry_listing_returns_names_of_every_length_exactly() {
2247 let root = test_root("name-lengths");
2250 std::fs::create_dir_all(&root).unwrap();
2251 let long = "n".repeat(200);
2252 for name in ["a", "medium-name.csv", long.as_str()] {
2253 std::fs::write(root.join(name), b"x").unwrap();
2254 }
2255 let source = Source::open(&root).unwrap();
2256 let mut names: Vec<String> = source
2257 .entry_names()
2258 .unwrap()
2259 .iter()
2260 .map(|name| name.as_str().to_owned())
2261 .collect();
2262 names.sort();
2263 let mut expected = vec!["a".to_owned(), "medium-name.csv".to_owned(), long];
2264 expected.sort();
2265 assert_eq!(names, expected);
2266 std::fs::remove_dir_all(root).unwrap();
2267 }
2268
2269 #[cfg(unix)]
2270 #[test]
2271 fn a_directory_nested_past_the_depth_bound_is_refused_promptly() {
2272 use std::os::fd::{AsRawFd, FromRawFd};
2273
2274 let _guard = process_resource_guard();
2277
2278 let root = test_root("deep-chain");
2279 std::fs::create_dir_all(&root).unwrap();
2280 let name = std::ffi::CString::new("d").unwrap();
2283 let mut level = std::fs::File::open(&root).unwrap();
2284 for _ in 0..(MAX_REFERENCED_DEPTH + 40) {
2285 unsafe {
2289 assert_eq!(libc::mkdirat(level.as_raw_fd(), name.as_ptr(), 0o755), 0);
2290 let fd = libc::openat(
2291 level.as_raw_fd(),
2292 name.as_ptr(),
2293 libc::O_RDONLY | libc::O_CLOEXEC,
2294 );
2295 assert!(fd >= 0);
2296 level = std::fs::File::from_raw_fd(fd);
2297 }
2298 }
2299 drop(level);
2300
2301 let (sender, receiver) = std::sync::mpsc::channel();
2302 let listed_root = root.clone();
2303 let worker = std::thread::spawn(move || {
2304 let source = Source::open(&listed_root).unwrap();
2305 sender.send(source.entry_names().map(|_| ())).unwrap();
2306 });
2307 let outcome = receiver
2308 .recv_timeout(std::time::Duration::from_secs(10))
2309 .expect("the depth refusal returns promptly");
2310 worker.join().unwrap();
2311 let error = outcome.expect_err("a chain past the depth bound refuses");
2312 assert!(error.to_string().contains("levels deep"), "{error}");
2313
2314 let mut fds = vec![std::fs::File::open(&root).unwrap()];
2317 loop {
2318 let last = fds.last().unwrap();
2319 let fd = unsafe {
2321 libc::openat(
2322 last.as_raw_fd(),
2323 name.as_ptr(),
2324 libc::O_RDONLY | libc::O_CLOEXEC,
2325 )
2326 };
2327 if fd < 0 {
2328 break;
2329 }
2330 fds.push(unsafe { std::fs::File::from_raw_fd(fd) });
2332 }
2333 while fds.len() > 1 {
2334 let parent = &fds[fds.len() - 2];
2335 unsafe {
2337 libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR);
2338 }
2339 fds.pop();
2340 }
2341 drop(fds);
2342 std::fs::remove_dir_all(&root).unwrap();
2343 }
2344
2345 #[test]
2346 fn a_directory_past_the_entry_budget_is_refused_with_bounded_memory() {
2347 let root = test_root("entry-budget");
2348 std::fs::create_dir_all(&root).unwrap();
2349 let excess = MAX_REFERENCED_FILES * 4;
2353 for index in 0..excess {
2354 std::fs::write(root.join(format!("f{index:05}.csv")), b"").unwrap();
2355 }
2356 let source = Source::open(&root).unwrap();
2357 let (error, allocated) = measured_bytes(|| source.entry_names().unwrap_err());
2358 assert!(error.to_string().contains("entries"), "{error}");
2359 assert!(
2364 allocated < MAX_REFERENCED_FILES * 192,
2365 "the refused listing allocated {allocated} bytes"
2366 );
2367 std::fs::remove_dir_all(root).unwrap();
2368 }
2369
2370 #[cfg(unix)]
2371 #[test]
2372 fn listing_breadth_never_scales_open_descriptors() {
2373 fn open_descriptor_count() -> usize {
2374 let table = if cfg!(target_os = "macos") {
2375 "/dev/fd"
2376 } else {
2377 "/proc/self/fd"
2378 };
2379 std::fs::read_dir(table).unwrap().count()
2380 }
2381
2382 let _guard = process_resource_guard();
2383 let root = test_root("breadth");
2384 let breadth = 400usize;
2388 for index in 0..breadth {
2389 let sub = root.join(format!("s{index:03}"));
2390 std::fs::create_dir_all(&sub).unwrap();
2391 std::fs::write(sub.join("data.csv"), b"x").unwrap();
2392 }
2393 std::fs::create_dir_all(root.join("nested/a/b/c")).unwrap();
2394 std::fs::write(root.join("nested/a/b/c/deep.csv"), b"x").unwrap();
2395
2396 let mut original: libc::rlimit = unsafe { std::mem::zeroed() };
2402 assert_eq!(
2403 unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut original) },
2405 0
2406 );
2407 let lowered = libc::rlimit {
2408 rlim_cur: 256,
2409 rlim_max: original.rlim_max,
2410 };
2411 assert_eq!(
2413 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const lowered) },
2414 0
2415 );
2416
2417 let baseline = open_descriptor_count();
2418 let peak = std::sync::atomic::AtomicUsize::new(0);
2419 let stop = std::sync::atomic::AtomicBool::new(false);
2420 let names = std::thread::scope(|scope| {
2421 let sampler = scope.spawn(|| {
2422 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
2423 let count = open_descriptor_count();
2424 peak.fetch_max(count, std::sync::atomic::Ordering::Relaxed);
2425 }
2426 });
2427 let source = Source::open(&root).unwrap();
2428 let names = source.entry_names().unwrap();
2429 stop.store(true, std::sync::atomic::Ordering::Relaxed);
2430 sampler.join().unwrap();
2431 drop(source);
2432 names
2433 });
2434 assert_eq!(
2436 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const original) },
2437 0
2438 );
2439
2440 assert_eq!(names.len(), breadth + 1, "every file listed");
2441 let sampled_peak = peak.load(std::sync::atomic::Ordering::Relaxed);
2442 assert!(
2443 sampled_peak <= baseline + MAX_REFERENCED_DEPTH + 16,
2444 "the walk held {sampled_peak} descriptors over a baseline of {baseline}"
2445 );
2446 std::fs::remove_dir_all(root).unwrap();
2447 }
2448
2449 #[test]
2450 fn a_directory_listing_still_names_windows_reserved_spellings() {
2451 let root = test_root("reserved-listing");
2454 std::fs::create_dir_all(&root).unwrap();
2455 std::fs::write(root.join("aux.dss"), b"content").unwrap();
2456 let source = Source::open(&root).unwrap();
2457 let names = source.entry_names().unwrap();
2458 assert_eq!(names.len(), 1);
2459 assert_eq!(names[0].as_str(), "aux.dss");
2460 std::fs::remove_dir_all(root).unwrap();
2461 }
2462
2463 #[test]
2464 fn a_directory_listing_repeats_and_survives_acquisition() {
2465 let root = test_root("repeat-listing");
2470 std::fs::create_dir_all(&root).unwrap();
2471 std::fs::write(root.join("network.csv"), b"name\nseq\n").unwrap();
2472 std::fs::write(root.join("buses.csv"), b"name\nB1\n").unwrap();
2473 let source = Source::open(&root).unwrap();
2474 let first = source.entry_names().unwrap();
2475 assert_eq!(first.len(), 2);
2476 let name = ArtifactPath::new("network.csv").unwrap();
2477 source.buffer(&name).unwrap();
2478 let second = source.entry_names().unwrap();
2479 assert_eq!(first, second);
2480 std::fs::remove_dir_all(root).unwrap();
2481 }
2482
2483 #[test]
2484 fn concurrent_acquisition_retains_one_buffer_for_one_name() {
2485 let root = test_root("concurrent");
2486 std::fs::create_dir_all(&root).unwrap();
2487 std::fs::write(root.join("shared.csv"), b"shared").unwrap();
2488 let source = Source::open(&root).unwrap();
2489 let name = ArtifactPath::new("shared.csv").unwrap();
2490 let buffers: Vec<_> = std::thread::scope(|scope| {
2491 (0..8)
2492 .map(|_| {
2493 let source = source.clone();
2494 let name = name.clone();
2495 scope.spawn(move || source.buffer(&name).unwrap())
2496 })
2497 .collect::<Vec<_>>()
2498 .into_iter()
2499 .map(|handle| handle.join().unwrap())
2500 .collect()
2501 });
2502 let pointer = buffers[0].bytes().as_ptr();
2503 assert!(
2504 buffers
2505 .iter()
2506 .all(|buffer| buffer.bytes().as_ptr() == pointer)
2507 );
2508 assert_eq!(source.acquired_buffers().len(), 1);
2509 std::fs::remove_dir_all(root).unwrap();
2510 }
2511 #[cfg(unix)]
2512 #[test]
2513 fn a_refused_listing_never_shortens_the_next_one() {
2514 let root = test_root("refused-listing");
2518 std::fs::create_dir_all(&root).unwrap();
2519 for index in 0..(MAX_REFERENCED_FILES + 5) {
2520 std::fs::write(root.join(format!("f{index}.txt")), b"x").unwrap();
2521 }
2522 let source = Source::open(&root).unwrap();
2523 let first = source.entry_names().unwrap_err();
2524 let second = source.entry_names().unwrap_err();
2525 assert_eq!(
2526 first.diagnostics().first().map(|d| d.code().to_owned()),
2527 second.diagnostics().first().map(|d| d.code().to_owned()),
2528 "the refusal repeats rather than shrinking into a partial listing"
2529 );
2530 drop(source);
2531 let _ = std::fs::remove_dir_all(&root);
2532 }
2533}