1use std::collections::BTreeMap;
2use std::ffi::{OsStr, OsString};
3use std::fmt;
4use std::io::Read;
5use std::path::{Component, Path, PathBuf};
6use std::sync::{Arc, Mutex, MutexGuard};
7
8use crate::validation::{MAX_FORMAT_ID_BYTES, valid_nonempty_text};
9use crate::{Error, SourceId};
10
11const MAX_REFERENCED_FILES: usize = 4_096;
14
15const DEFAULT_SOURCE_BYTES: u64 = 1 << 30;
17const MAX_REFERENCED_BYTES: u64 = DEFAULT_SOURCE_BYTES;
18const DEFAULT_PRIMARY_BYTES: u64 = DEFAULT_SOURCE_BYTES;
19
20const PRIMARY_LIMIT_VAR: &str = "POWERIO_MAX_PRIMARY_BYTES";
21const REFERENCED_LIMIT_VAR: &str = "POWERIO_MAX_REFERENCED_BYTES";
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26struct SourceLimits {
27 primary: u64,
28 referenced: u64,
29}
30
31impl SourceLimits {
32 fn from_lookup(lookup: impl Fn(&'static str) -> Option<OsString>) -> Result<Self, Error> {
36 Ok(Self {
37 primary: parse_primary_limit(lookup(PRIMARY_LIMIT_VAR).as_deref())?,
38 referenced: parse_referenced_limit(lookup(REFERENCED_LIMIT_VAR).as_deref())?,
39 })
40 }
41}
42
43fn referenced_byte_limit() -> Result<u64, Error> {
44 parse_referenced_limit(std::env::var_os(REFERENCED_LIMIT_VAR).as_deref())
45}
46
47fn parse_referenced_limit(value: Option<&OsStr>) -> Result<u64, Error> {
48 parse_byte_limit(REFERENCED_LIMIT_VAR, value, MAX_REFERENCED_BYTES)
49}
50
51fn parse_primary_limit(value: Option<&OsStr>) -> Result<u64, Error> {
52 parse_byte_limit(PRIMARY_LIMIT_VAR, value, DEFAULT_PRIMARY_BYTES)
53}
54
55fn parse_byte_limit(name: &str, value: Option<&OsStr>, default: u64) -> Result<u64, Error> {
56 let Some(value) = value else {
57 return Ok(default);
58 };
59 let limit = value
60 .to_str()
61 .filter(|value| !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()))
62 .and_then(|value| value.parse::<u64>().ok())
63 .filter(|&limit| limit > 0 && isize::try_from(limit).is_ok());
64 limit.ok_or_else(|| Error::new(&crate::codes::REQUEST_SOURCE_INVALID_LIMIT,
65 format!("{name} must be a positive decimal byte count within this platform's allocation limit")))
66}
67
68#[derive(Clone, Copy)]
69enum ReadBudget {
70 Primary(u64),
71 Referenced { remaining: u64, total: u64 },
72 ReferencedBuffer(u64),
73}
74
75impl ReadBudget {
76 fn bytes(self) -> u64 {
77 match self {
78 Self::Primary(n) | Self::ReferencedBuffer(n) => n,
79 Self::Referenced { remaining, .. } => remaining,
80 }
81 }
82 fn exceeded(self, name: &str) -> Error {
83 match self {
84 Self::Primary(limit) => Error::new(
85 &crate::codes::READ_IO_PRIMARY_BUDGET,
86 format!("primary source `{name}` exceeds its {limit} byte limit"),
87 ),
88 Self::Referenced { total, .. } => Error::new(
89 &crate::codes::READ_IO_REFERENCE_BUDGET,
90 format!(
91 "referenced file `{name}` would take this source past its {total} byte acquisition budget"
92 ),
93 ),
94 Self::ReferencedBuffer(limit) => Error::new(
95 &crate::codes::READ_IO_BUFFER_BUDGET,
96 format!("referenced buffer `{name}` exceeds the caller's {limit} byte limit"),
97 ),
98 }
99 }
100}
101
102const MAX_REFERENCED_DEPTH: usize = 64;
104
105const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
106
107#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
109pub struct FormatId(Box<str>);
110
111impl FormatId {
112 pub fn new(id: impl Into<String>) -> Result<Self, Error> {
114 let id = id.into();
115 if !valid_format_id(&id) {
116 return Err(Error::new(
117 &crate::codes::REQUEST_FORMAT_INVALID_ID,
118 "a format ID must be bounded lower case ASCII segments separated by single hyphens",
119 ));
120 }
121 Ok(Self(id.into_boxed_str()))
122 }
123
124 #[must_use]
125 pub fn as_str(&self) -> &str {
126 &self.0
127 }
128}
129
130impl fmt::Display for FormatId {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 formatter.write_str(&self.0)
133 }
134}
135
136fn valid_format_id(id: &str) -> bool {
137 if id.is_empty() || id.len() > MAX_FORMAT_ID_BYTES {
138 return false;
139 }
140 let bytes = id.as_bytes();
141 if !bytes[0].is_ascii_lowercase() || bytes.last() == Some(&b'-') {
142 return false;
143 }
144 let mut previous_hyphen = false;
145 for byte in bytes {
146 if *byte == b'-' {
147 if previous_hyphen {
148 return false;
149 }
150 previous_hyphen = true;
151 } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
152 previous_hyphen = false;
153 } else {
154 return false;
155 }
156 }
157 true
158}
159
160#[derive(Debug)]
161struct SourceBufferData {
162 id: SourceId,
163 name: Box<str>,
164 bytes: Arc<[u8]>,
165 directory: Box<[Box<str>]>,
169}
170
171#[derive(Clone, Debug)]
173pub struct SourceBuffer(Arc<SourceBufferData>);
174
175impl SourceBuffer {
176 fn new(
177 id: SourceId,
178 name: impl Into<String>,
179 bytes: Arc<[u8]>,
180 directory: Vec<String>,
181 ) -> Self {
182 Self(Arc::new(SourceBufferData {
183 id,
184 name: name.into().into_boxed_str(),
185 bytes,
186 directory: directory.into_iter().map(String::into_boxed_str).collect(),
187 }))
188 }
189
190 #[must_use]
191 pub fn id(&self) -> &SourceId {
192 &self.0.id
193 }
194
195 #[must_use]
196 pub fn name(&self) -> &str {
197 &self.0.name
198 }
199
200 #[must_use]
203 pub fn bytes(&self) -> &[u8] {
204 &self.0.bytes
205 }
206
207 #[must_use]
208 pub fn shared_bytes(&self) -> Arc<[u8]> {
209 Arc::clone(&self.0.bytes)
210 }
211
212 #[must_use]
214 pub fn has_utf8_bom(&self) -> bool {
215 self.0.bytes.starts_with(&UTF8_BOM)
216 }
217
218 #[must_use]
222 pub fn content_bytes(&self) -> &[u8] {
223 let bytes: &[u8] = &self.0.bytes;
224 if bytes.starts_with(&UTF8_BOM) {
225 &bytes[UTF8_BOM.len()..]
226 } else {
227 bytes
228 }
229 }
230
231 pub fn directory_segments(&self) -> impl Iterator<Item = &str> {
236 self.0.directory.iter().map(AsRef::as_ref)
237 }
238}
239
240#[derive(Debug)]
253struct FileAcquisition {
254 root_display: PathBuf,
255 max_bytes: u64,
256 selected: bool,
259 state: Mutex<AcquisitionState>,
260}
261
262#[derive(Debug, Default)]
263struct AcquisitionState {
264 root: Option<platform::RootHandle>,
265 cache: BTreeMap<String, SourceBuffer>,
266 listed: Option<Vec<crate::ArtifactPath>>,
271 files: usize,
272 bytes: u64,
273}
274
275impl AcquisitionState {
276 fn pinned_root(&mut self, root_display: &Path) -> Result<&platform::RootHandle, Error> {
279 if self.root.is_none() {
280 let root = platform::open_root(root_display)
281 .map_err(|cause| open_error(&crate::codes::READ_IO_OPEN, root_display, cause))?;
282 self.root = Some(root);
283 }
284 Ok(self.root.as_ref().expect("pinned above"))
285 }
286}
287
288#[derive(Debug)]
289enum SourceProvider {
290 Memory {
291 primary: Option<SourceBuffer>,
292 named: BTreeMap<String, SourceBuffer>,
293 },
294 File {
295 primary: SourceBuffer,
296 acquisition: FileAcquisition,
297 },
298 Directory {
299 acquisition: FileAcquisition,
300 },
301}
302
303pub const PRIMARY_SOURCE_ID: &str = "/input";
311
312#[derive(Clone)]
319pub struct Source {
320 name: Arc<str>,
321 provider: Arc<SourceProvider>,
322 declared_format: Option<FormatId>,
323}
324
325impl Source {
326 pub fn open(path: impl Into<PathBuf>) -> Result<Self, Error> {
336 let path: PathBuf = path.into();
337 Self::open_with_limits(&path, std::env::var_os)
338 }
339
340 fn open_with_limits(
341 path: &Path,
342 lookup: impl Fn(&'static str) -> Option<OsString>,
343 ) -> Result<Self, Error> {
344 if path.as_os_str().is_empty() {
345 return Err(Error::new(
346 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
347 "source path cannot be empty",
348 ));
349 }
350 let limits = SourceLimits::from_lookup(lookup)?;
352 let name: Arc<str> = path.to_string_lossy().into_owned().into();
353
354 let file = match platform::open_no_follow(path) {
359 Ok(file) => file,
360 Err(error) if platform::is_symlink_refusal(&error) => {
361 return Err(Error::new(
362 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
363 format!("source `{}` is a symbolic link", path.display()),
364 ));
365 }
366 Err(error) if platform::is_directory_open_failure(&error, path) => {
367 return Self::open_directory(name, path, limits.referenced);
368 }
369 Err(cause) => return Err(open_error(&crate::codes::READ_IO_OPEN, path, cause)),
370 };
371 let metadata = file
372 .metadata()
373 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, path, cause))?;
374 if metadata.is_dir() {
375 drop(file);
376 return Self::open_directory(name, path, limits.referenced);
377 }
378 let bytes = read_open_file(file, &name, ReadBudget::Primary(limits.primary))?;
379 let root_display = canonical_parent(path)?;
380 let primary = SourceBuffer::new(
381 SourceId::new(PRIMARY_SOURCE_ID)?,
382 name.to_string(),
383 bytes,
384 Vec::new(),
385 );
386 Ok(Self {
387 name,
388 provider: Arc::new(SourceProvider::File {
389 primary,
390 acquisition: FileAcquisition {
391 root_display,
392 max_bytes: limits.referenced,
393 selected: false,
394 state: Mutex::new(AcquisitionState::default()),
395 },
396 }),
397 declared_format: None,
398 })
399 }
400
401 fn open_directory(name: Arc<str>, path: &Path, max_bytes: u64) -> Result<Self, Error> {
402 let root_display = std::fs::canonicalize(path)
403 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, path, cause))?;
404 Ok(Self {
405 name,
406 provider: Arc::new(SourceProvider::Directory {
407 acquisition: FileAcquisition {
408 root_display,
409 max_bytes,
410 selected: false,
411 state: Mutex::new(AcquisitionState::default()),
412 },
413 }),
414 declared_format: None,
415 })
416 }
417
418 pub fn from_memory(
422 name: impl Into<String>,
423 bytes: impl Into<Arc<[u8]>>,
424 ) -> Result<Self, Error> {
425 let name = name.into();
426 if !valid_nonempty_text(&name) {
427 return Err(Error::new(
428 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
429 "an in-memory source requires a nonempty bounded name",
430 ));
431 }
432 let primary = SourceBuffer::new(
433 SourceId::new(PRIMARY_SOURCE_ID)?,
434 name.clone(),
435 bytes.into(),
436 Vec::new(),
437 );
438 Ok(Self {
439 name: name.into(),
440 provider: Arc::new(SourceProvider::Memory {
441 primary: Some(primary),
442 named: BTreeMap::new(),
443 }),
444 declared_format: None,
445 })
446 }
447
448 pub fn from_memory_tree(
455 name: impl Into<String>,
456 files: impl IntoIterator<Item = (crate::ArtifactPath, Arc<[u8]>)>,
457 primary: Option<crate::ArtifactPath>,
458 ) -> Result<Self, Error> {
459 let name = name.into();
460 if !valid_nonempty_text(&name) {
461 return Err(Error::new(
462 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
463 "an in-memory source requires a nonempty bounded name",
464 ));
465 }
466 let mut named = BTreeMap::new();
467 let mut total_bytes = 0u64;
468 for (path, bytes) in files {
469 let segments = resolve_segments(&[], path.as_str())?;
470 let key = segments.join("/");
471 total_bytes = total_bytes.saturating_add(bytes.len() as u64);
472 if named.len() >= MAX_REFERENCED_FILES || total_bytes > MAX_REFERENCED_BYTES {
473 return Err(Error::new(
474 &crate::codes::READ_IO_REFERENCE_BUDGET,
475 "an in-memory tree exceeds 4096 files or 1 GiB",
476 ));
477 }
478 let id = if primary.as_ref() == Some(&path) {
479 PRIMARY_SOURCE_ID
480 } else {
481 &key
482 };
483 let directory = segments[..segments.len() - 1].to_vec();
484 let buffer = SourceBuffer::new(SourceId::new(id)?, key.clone(), bytes, directory);
485 if named.insert(key.clone(), buffer).is_some() {
486 return Err(Error::new(
487 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
488 format!("duplicate in-memory file `{key}`"),
489 ));
490 }
491 }
492 let primary = primary
493 .map(|path| {
494 named.get(path.as_str()).cloned().ok_or_else(|| {
495 Error::new(
496 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
497 format!("primary file `{}` was not supplied", path.as_str()),
498 )
499 })
500 })
501 .transpose()?;
502 let name = primary.as_ref().map_or(name.as_str(), SourceBuffer::name);
503 Ok(Self {
504 name: name.into(),
505 provider: Arc::new(SourceProvider::Memory { primary, named }),
506 declared_format: None,
507 })
508 }
509
510 pub fn with_named_buffer(
515 self,
516 name: impl Into<String>,
517 bytes: impl Into<Arc<[u8]>>,
518 ) -> Result<Self, Error> {
519 let name = name.into();
520 let segments = resolve_segments(&[], &name)?;
521 let key = segments.join("/");
522 let mut provider = Arc::try_unwrap(self.provider).map_err(|_| {
523 Error::new(
524 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
525 "named buffers are supplied while constructing a source, before it is shared",
526 )
527 })?;
528 let SourceProvider::Memory { named, .. } = &mut provider else {
529 return Err(Error::new(
530 &crate::codes::REQUEST_SOURCE_INVALID_NAME,
531 "named buffers belong to in-memory sources; a file source acquires referenced files beneath its root",
532 ));
533 };
534 let directory = segments[..segments.len() - 1].to_vec();
535 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes.into(), directory);
536 named.insert(key, buffer);
537 Ok(Self {
538 name: self.name,
539 provider: Arc::new(provider),
540 declared_format: self.declared_format,
541 })
542 }
543
544 pub fn with_acquisition_root(self, root: impl Into<PathBuf>) -> Result<Self, Error> {
548 let requested = root.into();
549 let SourceProvider::File {
550 primary,
551 acquisition,
552 } = &*self.provider
553 else {
554 return Err(Error::new(
555 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
556 "an acquisition root applies to a file source",
557 ));
558 };
559 let canonical = std::fs::canonicalize(&requested)
560 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, &requested, cause))?;
561 let Ok(remainder) = acquisition.root_display.strip_prefix(&canonical) else {
562 return Err(Error::new(
563 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
564 format!(
565 "the case file directory {} is outside the requested acquisition root {}",
566 acquisition.root_display.display(),
567 canonical.display()
568 ),
569 ));
570 };
571 let mut directory = Vec::new();
572 for component in remainder.components() {
573 let Component::Normal(segment) = component else {
574 return Err(Error::new(
575 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
576 "the acquisition root does not resolve to a plain prefix of the case directory",
577 ));
578 };
579 let Some(segment) = segment.to_str().filter(|text| plain_segment(text)) else {
580 return Err(Error::new(
581 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
582 "the acquisition root does not resolve to a plain prefix of the case directory",
583 ));
584 };
585 directory.push(segment.to_owned());
586 }
587 let primary = SourceBuffer::new(
588 primary.id().clone(),
589 primary.name().to_owned(),
590 primary.shared_bytes(),
591 directory,
592 );
593 Ok(Self {
594 name: self.name,
595 provider: Arc::new(SourceProvider::File {
596 primary,
597 acquisition: FileAcquisition {
598 root_display: canonical,
599 max_bytes: acquisition.max_bytes,
600 selected: true,
601 state: Mutex::new(AcquisitionState::default()),
602 },
603 }),
604 declared_format: self.declared_format,
605 })
606 }
607
608 #[must_use]
610 pub fn with_format(mut self, format: FormatId) -> Self {
611 self.declared_format = Some(format);
612 self
613 }
614
615 #[must_use]
616 pub fn name(&self) -> &str {
617 &self.name
618 }
619
620 #[must_use]
621 pub const fn format(&self) -> Option<&FormatId> {
622 self.declared_format.as_ref()
623 }
624
625 #[must_use]
626 pub fn is_directory(&self) -> bool {
627 matches!(
628 &*self.provider,
629 SourceProvider::Directory { .. } | SourceProvider::Memory { primary: None, .. }
630 )
631 }
632
633 pub fn acquisition_byte_limit(&self) -> Result<u64, Error> {
637 match &*self.provider {
638 SourceProvider::File { acquisition, .. }
639 | SourceProvider::Directory { acquisition } => Ok(acquisition.max_bytes),
640 SourceProvider::Memory { .. } => referenced_byte_limit(),
641 }
642 }
643
644 pub fn primary_buffer(&self) -> Result<SourceBuffer, Error> {
646 match &*self.provider {
647 SourceProvider::Memory {
648 primary: Some(primary),
649 ..
650 }
651 | SourceProvider::File { primary, .. } => Ok(primary.clone()),
652 SourceProvider::Directory { .. } | SourceProvider::Memory { primary: None, .. } => {
653 Err(Error::new(
654 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
655 "a directory source has no implicit primary buffer",
656 )
657 .with_source(self.clone()))
658 }
659 }
660 }
661
662 pub fn buffer(&self, name: &crate::ArtifactPath) -> Result<SourceBuffer, Error> {
665 if matches!(
666 &*self.provider,
667 SourceProvider::Memory { primary: None, .. }
668 ) {
669 return self.root_buffer(name.as_str());
670 }
671 let SourceProvider::Directory { acquisition } = &*self.provider else {
672 return Err(Error::new(
673 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
674 "named child buffers require a directory source",
675 )
676 .with_source(self.clone()));
677 };
678 let segments = resolve_segments(&[], name.as_str())
679 .map_err(|error| error.with_source(self.clone()))?;
680 acquisition
681 .acquire(&segments)
682 .map_err(|error| error.with_source(self.clone()))
683 }
684
685 pub fn root_buffer(&self, name: &str) -> Result<SourceBuffer, Error> {
690 match &*self.provider {
691 SourceProvider::Memory { named, .. } => {
692 let segments = resolve_segments(&[], name)?;
693 let key = segments.join("/");
694 named.get(&key).cloned().ok_or_else(|| {
695 Error::new(
696 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
697 format!(
698 "referenced buffer `{key}` was not supplied to this in-memory source"
699 ),
700 )
701 .with_source(self.clone())
702 })
703 }
704 SourceProvider::File { acquisition, .. }
705 | SourceProvider::Directory { acquisition } => {
706 let segments = resolve_segments(&[], name)?;
707 acquisition
708 .acquire(&segments)
709 .map_err(|error| error.with_source(self.clone()))
710 }
711 }
712 }
713
714 #[must_use]
720 pub fn selected_acquisition_root(&self) -> Option<&Path> {
721 match &*self.provider {
722 SourceProvider::Memory { .. } | SourceProvider::Directory { .. } => None,
723 SourceProvider::File { acquisition, .. } => acquisition
724 .selected
725 .then_some(acquisition.root_display.as_path()),
726 }
727 }
728
729 pub fn referenced_buffer(
734 &self,
735 referrer: &SourceBuffer,
736 name: &str,
737 ) -> Result<SourceBuffer, Error> {
738 self.referenced_buffer_with_limit(referrer, name, u64::MAX)
739 }
740
741 pub fn referenced_buffer_with_limit(
753 &self,
754 referrer: &SourceBuffer,
755 name: &str,
756 max_bytes: u64,
757 ) -> Result<SourceBuffer, Error> {
758 let referrer_directory: Vec<&str> = referrer.directory_segments().collect();
759 match &*self.provider {
760 SourceProvider::Memory { named, .. } => {
761 let segments = resolve_segments(&referrer_directory, name)?;
762 let key = segments.join("/");
763 let buffer = named.get(&key).ok_or_else(|| {
764 Error::new(
765 &crate::codes::REQUEST_SOURCE_UNKNOWN_BUFFER,
766 format!(
767 "referenced buffer `{key}` was not supplied to this in-memory source"
768 ),
769 )
770 .with_source(self.clone())
771 })?;
772 if buffer.bytes().len() as u64 > max_bytes {
773 return Err(ReadBudget::ReferencedBuffer(max_bytes)
774 .exceeded(&key)
775 .with_source(self.clone()));
776 }
777 Ok(buffer.clone())
778 }
779 SourceProvider::File { acquisition, .. }
780 | SourceProvider::Directory { acquisition } => {
781 let segments = match absolute_to_root_relative(&acquisition.root_display, name) {
782 Some(root_relative) => root_relative?,
783 None => resolve_segments(&referrer_directory, name)?,
784 };
785 acquisition
786 .acquire_with_limit(&segments, max_bytes)
787 .map_err(|error| error.with_source(self.clone()))
788 }
789 }
790 }
791
792 #[allow(clippy::too_many_lines)] pub fn entry_names(&self) -> Result<Vec<crate::ArtifactPath>, Error> {
799 match &*self.provider {
800 SourceProvider::Memory { named, .. } => named
801 .keys()
802 .map(|name| crate::ArtifactPath::new(name.clone()))
803 .collect(),
804 SourceProvider::File { .. } => Err(Error::new(
805 &crate::codes::REQUEST_SOURCE_DIRECTORY_REQUIRED,
806 "entry listing requires a directory source",
807 )
808 .with_source(self.clone())),
809 SourceProvider::Directory { acquisition } => {
810 struct Frame<Handle> {
824 prefix: Vec<String>,
825 directory: Handle,
826 subdirectories: Vec<String>,
828 }
829
830 let mut state = acquisition.lock();
831 if let Some(listed) = &state.listed {
832 return Ok(listed.clone());
833 }
834 let root = state.pinned_root(&acquisition.root_display)?;
835 let budget_refusal = || {
836 Error::new(
837 &crate::codes::READ_IO_REFERENCE_BUDGET,
838 format!(
839 "the source directory holds more than {MAX_REFERENCED_FILES} entries"
840 ),
841 )
842 };
843 let root_handle = root.duplicate_handle().map_err(|cause| {
844 Error::new(
845 &crate::codes::READ_IO_METADATA,
846 "cannot list the source directory root",
847 )
848 .with_cause(cause)
849 })?;
850 let mut names = Vec::new();
851 let mut undescended = 0usize;
854 let mut frames = Vec::new();
855 let mut arriving = Some((Vec::<String>::new(), root_handle));
856 while let Some((prefix, directory)) = arriving.take() {
857 let allowance = MAX_REFERENCED_FILES
858 .checked_sub(names.len() + undescended)
859 .filter(|allowance| *allowance > 0)
860 .ok_or_else(budget_refusal)?;
861 let entries =
862 platform::list_entries(&directory, allowance).map_err(|cause| {
863 if platform::is_entry_budget(&cause) {
864 budget_refusal()
865 } else {
866 listing_error(&prefix.join("/"), cause)
867 }
868 })?;
869 let mut subdirectories = Vec::new();
870 for (name, is_directory) in entries {
871 if names.len() + undescended + subdirectories.len() >= MAX_REFERENCED_FILES
872 {
873 return Err(budget_refusal());
874 }
875 if is_directory {
876 if prefix.len() + 1 >= MAX_REFERENCED_DEPTH {
877 return Err(Error::new(
878 &crate::codes::READ_IO_REFERENCE_BUDGET,
879 format!(
880 "the source directory nests more than {MAX_REFERENCED_DEPTH} levels deep"
881 ),
882 ));
883 }
884 subdirectories.push(name);
885 } else {
886 let mut child = prefix.clone();
887 child.push(name);
888 names.push(crate::ArtifactPath::new(child.join("/"))?);
889 }
890 }
891 undescended += subdirectories.len();
892 frames.push(Frame {
893 prefix,
894 directory,
895 subdirectories,
896 });
897
898 while let Some(frame) = frames.last_mut() {
903 let Some(name) = frame.subdirectories.pop() else {
904 frames.pop();
905 continue;
906 };
907 undescended -= 1;
908 let mut child = frame.prefix.clone();
909 let handle = platform::open_child_directory(&frame.directory, &name)
910 .map_err(|cause| {
911 child.push(name.clone());
912 listing_error(&child.join("/"), cause)
913 })?;
914 child.push(name);
915 arriving = Some((child, handle));
916 break;
917 }
918 }
919 names.sort();
920 state.listed = Some(names.clone());
921 Ok(names)
922 }
923 }
924 }
925
926 #[must_use]
928 pub fn acquired_buffers(&self) -> Vec<SourceBuffer> {
929 match &*self.provider {
930 SourceProvider::Memory { primary, named } => {
931 let mut buffers: Vec<_> = primary.iter().cloned().collect();
932 buffers.extend(
933 named
934 .values()
935 .filter(|buffer| {
936 primary
937 .as_ref()
938 .is_none_or(|entry| entry.id() != buffer.id())
939 })
940 .cloned(),
941 );
942 buffers
943 }
944 SourceProvider::File {
945 primary,
946 acquisition,
947 } => {
948 let mut buffers = vec![primary.clone()];
949 buffers.extend(acquisition.lock().cache.values().cloned());
950 buffers
951 }
952 SourceProvider::Directory { acquisition } => {
953 acquisition.lock().cache.values().cloned().collect()
954 }
955 }
956 }
957}
958
959impl fmt::Debug for Source {
960 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
961 formatter
962 .debug_struct("Source")
963 .field("name", &self.name)
964 .field("is_directory", &self.is_directory())
965 .field("declared_format", &self.declared_format)
966 .field("acquired_buffer_count", &self.acquired_buffers().len())
967 .finish_non_exhaustive()
968 }
969}
970
971impl FileAcquisition {
972 fn lock(&self) -> MutexGuard<'_, AcquisitionState> {
973 self.state
974 .lock()
975 .unwrap_or_else(std::sync::PoisonError::into_inner)
976 }
977
978 fn acquire(&self, segments: &[String]) -> Result<SourceBuffer, Error> {
985 self.acquire_with_limit(segments, u64::MAX)
986 }
987
988 fn acquire_with_limit(
989 &self,
990 segments: &[String],
991 max_bytes: u64,
992 ) -> Result<SourceBuffer, Error> {
993 let key = segments.join("/");
994 let mut state = self.lock();
995 if let Some(buffer) = state.cache.get(&key) {
996 if buffer.bytes().len() as u64 > max_bytes {
997 return Err(ReadBudget::ReferencedBuffer(max_bytes).exceeded(&key));
998 }
999 return Ok(buffer.clone());
1000 }
1001 if state.files >= MAX_REFERENCED_FILES {
1002 return Err(Error::new(
1003 &crate::codes::READ_IO_REFERENCE_BUDGET,
1004 format!("this source already acquired {MAX_REFERENCED_FILES} referenced files"),
1005 ));
1006 }
1007 let remaining = self.max_bytes.saturating_sub(state.bytes);
1008 let budget = if max_bytes <= remaining {
1009 ReadBudget::ReferencedBuffer(max_bytes)
1010 } else {
1011 ReadBudget::Referenced {
1012 remaining,
1013 total: self.max_bytes,
1014 }
1015 };
1016 let root = state.pinned_root(&self.root_display)?;
1017 let file = root.open_beneath(segments).map_err(|error| {
1018 if platform::is_symlink_refusal(&error) {
1019 Error::new(
1020 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
1021 format!("referenced file `{key}` crosses a symbolic link"),
1022 )
1023 } else {
1024 Error::new(
1025 &crate::codes::READ_IO_OPEN,
1026 format!("cannot open referenced file `{key}`"),
1027 )
1028 .with_cause(error)
1029 }
1030 })?;
1031 let bytes = read_open_file(file, &key, budget)?;
1032 let directory = segments[..segments.len() - 1].to_vec();
1033 let buffer = SourceBuffer::new(SourceId::new(&key)?, key.clone(), bytes, directory);
1034 state.files += 1;
1035 state.bytes += buffer.bytes().len() as u64;
1036 state.cache.insert(key, buffer.clone());
1037 Ok(buffer)
1038 }
1039}
1040
1041fn plain_segment(segment: &str) -> bool {
1047 if segment.is_empty()
1048 || segment == "."
1049 || segment == ".."
1050 || segment.contains(['/', '\\', '\0', ':'])
1051 {
1052 return false;
1053 }
1054 let mut components = Path::new(segment).components();
1055 matches!(components.next(), Some(Component::Normal(text)) if text == std::ffi::OsStr::new(segment))
1056 && components.next().is_none()
1057}
1058
1059fn resolve_segments(referrer_directory: &[&str], name: &str) -> Result<Vec<String>, Error> {
1066 if name.is_empty()
1067 || name.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
1068 || name.contains('\0')
1069 || name.starts_with(['/', '\\'])
1070 {
1071 return Err(Error::new(
1072 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
1073 "a referenced name must be a nonempty bounded relative path",
1074 ));
1075 }
1076 let mut segments: Vec<String> = referrer_directory
1077 .iter()
1078 .map(|segment| (*segment).to_owned())
1079 .collect();
1080 for raw in name.split(['/', '\\']) {
1081 match raw {
1082 "" | "." => {}
1083 ".." => {
1084 if segments.pop().is_none() {
1085 return Err(Error::new(
1086 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
1087 format!("referenced name `{name}` resolves outside the acquisition root"),
1088 ));
1089 }
1090 }
1091 segment => {
1092 if segment.len() > crate::validation::MAX_ARTIFACT_PATH_BYTES
1093 || !plain_segment(segment)
1094 {
1095 return Err(Error::new(
1096 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
1097 format!("referenced name `{name}` is not a portable relative path"),
1098 ));
1099 }
1100 segments.push(segment.to_owned());
1101 }
1102 }
1103 }
1104 if segments.is_empty() {
1105 return Err(Error::new(
1106 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
1107 format!("referenced name `{name}` does not name a file"),
1108 ));
1109 }
1110 if segments.len() > MAX_REFERENCED_DEPTH {
1111 return Err(Error::new(
1112 &crate::codes::READ_IO_REFERENCE_BUDGET,
1113 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
1114 ));
1115 }
1116 Ok(segments)
1117}
1118
1119fn absolute_to_root_relative(root: &Path, name: &str) -> Option<Result<Vec<String>, Error>> {
1125 let path = Path::new(name);
1126 if !path.is_absolute() {
1127 return None;
1128 }
1129 let Ok(remainder) = path.strip_prefix(root) else {
1130 return Some(Err(Error::new(
1131 &crate::codes::REQUEST_SOURCE_ESCAPES_ROOT,
1132 format!("referenced name `{name}` resolves outside the acquisition root"),
1133 )));
1134 };
1135 let mut segments = Vec::new();
1136 for component in remainder.components() {
1137 let text = match component {
1138 Component::Normal(text) => text.to_str(),
1139 _ => None,
1140 };
1141 let Some(text) = text.filter(|text| plain_segment(text)) else {
1142 return Some(Err(Error::new(
1143 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
1144 format!("referenced name `{name}` is not a portable path beneath the root"),
1145 )));
1146 };
1147 segments.push(text.to_owned());
1148 }
1149 if segments.is_empty() {
1150 return Some(Err(Error::new(
1151 &crate::codes::REQUEST_SOURCE_INVALID_PATH,
1152 format!("referenced name `{name}` does not name a file"),
1153 )));
1154 }
1155 if segments.len() > MAX_REFERENCED_DEPTH {
1156 return Some(Err(Error::new(
1157 &crate::codes::READ_IO_REFERENCE_BUDGET,
1158 format!("referenced name `{name}` nests more than {MAX_REFERENCED_DEPTH} levels deep"),
1159 )));
1160 }
1161 Some(Ok(segments))
1162}
1163
1164fn canonical_parent(path: &Path) -> Result<PathBuf, Error> {
1165 let parent = match path.parent() {
1166 Some(parent) if !parent.as_os_str().is_empty() => parent,
1167 _ => Path::new("."),
1168 };
1169 std::fs::canonicalize(parent)
1170 .map_err(|cause| open_error(&crate::codes::READ_IO_METADATA, parent, cause))
1171}
1172
1173fn open_error(info: &'static crate::DiagnosticInfo, path: &Path, cause: std::io::Error) -> Error {
1174 Error::new(info, format!("cannot open source `{}`", path.display())).with_cause(cause)
1175}
1176
1177fn listing_error(display: &str, cause: std::io::Error) -> Error {
1180 if platform::is_symlink_refusal(&cause) {
1181 Error::new(
1182 &crate::codes::REQUEST_SOURCE_SYMLINK_REFUSED,
1183 format!("source directory `{display}` crosses a symbolic link"),
1184 )
1185 } else {
1186 Error::new(
1187 &crate::codes::READ_IO_METADATA,
1188 format!("cannot list source directory `{display}`"),
1189 )
1190 .with_cause(cause)
1191 }
1192}
1193
1194fn read_open_file(file: std::fs::File, name: &str, budget: ReadBudget) -> Result<Arc<[u8]>, Error> {
1201 let max_bytes = budget.bytes();
1202 let metadata = file.metadata().map_err(|cause| {
1203 Error::new(
1204 &crate::codes::READ_IO_METADATA,
1205 format!("cannot inspect source buffer `{name}`"),
1206 )
1207 .with_cause(cause)
1208 })?;
1209 if !metadata.is_file() {
1210 return Err(Error::new(
1211 &crate::codes::REQUEST_SOURCE_NOT_A_FILE,
1212 format!("source buffer `{name}` is not a regular file"),
1213 ));
1214 }
1215 let declared_length = metadata.len();
1216 if declared_length > max_bytes {
1217 return Err(budget.exceeded(name));
1218 }
1219 let capacity = usize::try_from(declared_length).map_err(|cause| {
1220 Error::new(
1221 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1222 format!("source buffer `{name}` is too large for this platform"),
1223 )
1224 .with_cause(cause)
1225 })?;
1226 let mut bytes = Vec::new();
1227 bytes.try_reserve_exact(capacity).map_err(|cause| {
1228 Error::new(
1229 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1230 format!("cannot reserve {declared_length} bytes for source buffer `{name}`"),
1231 )
1232 .with_cause(cause)
1233 })?;
1234 let read_limit = declared_length
1235 .checked_add(1)
1236 .ok_or_else(|| {
1237 Error::new(
1238 &crate::codes::READ_IO_ALLOCATION_REFUSED,
1239 format!("source buffer `{name}` is too large to read safely"),
1240 )
1241 })?
1242 .min(max_bytes.saturating_add(1));
1243 let mut file = file;
1244 file.by_ref()
1245 .take(read_limit)
1246 .read_to_end(&mut bytes)
1247 .map_err(|cause| {
1248 Error::new(
1249 &crate::codes::READ_IO_READ,
1250 format!("cannot read source buffer `{name}`"),
1251 )
1252 .with_cause(cause)
1253 })?;
1254 if bytes.len() != capacity {
1255 return Err(Error::new(
1256 &crate::codes::READ_IO_SOURCE_CHANGED,
1257 format!("source buffer `{name}` changed length while it was read"),
1258 ));
1259 }
1260 Ok(bytes.into())
1261}
1262
1263#[cfg(unix)]
1264mod platform {
1265 use std::ffi::CString;
1275 use std::fs::File;
1276 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
1277 use std::os::unix::ffi::OsStrExt;
1278 use std::path::Path;
1279
1280 #[derive(Debug)]
1281 pub(super) struct RootHandle(OwnedFd);
1282
1283 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1286 let path = c_string(path.as_os_str().as_bytes())?;
1287 let fd = unsafe {
1291 libc::open(
1292 path.as_ptr(),
1293 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1294 )
1295 };
1296 if fd < 0 {
1297 return Err(std::io::Error::last_os_error());
1298 }
1299 let file = unsafe { File::from_raw_fd(fd) };
1301 clear_nonblock(&file)?;
1302 Ok(file)
1303 }
1304
1305 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1310 let path = c_string(path.as_os_str().as_bytes())?;
1311 let fd = unsafe {
1313 libc::open(
1314 path.as_ptr(),
1315 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1316 )
1317 };
1318 if fd < 0 {
1319 return Err(std::io::Error::last_os_error());
1320 }
1321 let file = unsafe { File::from_raw_fd(fd) };
1323 if !file.metadata()?.is_dir() {
1324 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1325 }
1326 clear_nonblock(&file)?;
1327 Ok(RootHandle(file.into()))
1328 }
1329
1330 impl RootHandle {
1331 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1332 let mut directory: Option<OwnedFd> = None;
1333 let (file_segment, directories) =
1334 segments.split_last().expect("resolution yields a file");
1335 for segment in directories {
1336 let next = self.open_at(
1341 directory.as_ref(),
1342 segment,
1343 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1344 )?;
1345 let next = File::from(next);
1346 if !next.metadata()?.is_dir() {
1347 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1348 }
1349 directory = Some(next.into());
1350 }
1351 let fd = self.open_at(
1352 directory.as_ref(),
1353 file_segment,
1354 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1355 )?;
1356 let file = File::from(fd);
1357 clear_nonblock(&file)?;
1358 Ok(file)
1359 }
1360
1361 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1369 let segment = c_string(b".")?;
1370 let fd = unsafe {
1373 libc::openat(
1374 self.0.as_raw_fd(),
1375 segment.as_ptr(),
1376 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY,
1377 )
1378 };
1379 if fd < 0 {
1380 return Err(std::io::Error::last_os_error());
1381 }
1382 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1384 }
1385
1386 fn open_at(
1387 &self,
1388 directory: Option<&OwnedFd>,
1389 segment: &str,
1390 flags: libc::c_int,
1391 ) -> std::io::Result<OwnedFd> {
1392 if !super::plain_segment(segment) {
1393 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1394 }
1395 let at = directory.map_or_else(|| self.0.as_raw_fd(), AsRawFd::as_raw_fd);
1396 let segment = c_string(segment.as_bytes())?;
1397 let fd = unsafe { libc::openat(at, segment.as_ptr(), flags) };
1401 if fd < 0 {
1402 return Err(std::io::Error::last_os_error());
1403 }
1404 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1406 }
1407 }
1408
1409 pub(super) type DirectoryHandle = OwnedFd;
1412
1413 pub(super) fn open_child_directory(
1417 parent: &DirectoryHandle,
1418 name: &str,
1419 ) -> std::io::Result<DirectoryHandle> {
1420 if !super::plain_segment(name) {
1421 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1422 }
1423 let segment = c_string(name.as_bytes())?;
1424 let fd = unsafe {
1428 libc::openat(
1429 parent.as_raw_fd(),
1430 segment.as_ptr(),
1431 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
1432 )
1433 };
1434 if fd < 0 {
1435 return Err(std::io::Error::last_os_error());
1436 }
1437 let file = unsafe { File::from_raw_fd(fd) };
1439 if !file.metadata()?.is_dir() {
1440 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1441 }
1442 Ok(file.into())
1443 }
1444
1445 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1446
1447 fn entry_budget_error() -> std::io::Error {
1448 std::io::Error::other(ENTRY_BUDGET_MARKER)
1449 }
1450
1451 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1454 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1455 }
1456
1457 pub(super) fn list_entries(
1464 directory: &DirectoryHandle,
1465 max: usize,
1466 ) -> std::io::Result<Vec<(String, bool)>> {
1467 use std::os::fd::IntoRawFd;
1468
1469 let raw = directory.try_clone()?.into_raw_fd();
1472 let stream = unsafe { libc::fdopendir(raw) };
1475 if stream.is_null() {
1476 let error = std::io::Error::last_os_error();
1477 unsafe { libc::close(raw) };
1480 return Err(error);
1481 }
1482 let mut entries = Vec::new();
1483 loop {
1484 errno_clear();
1486 let entry = unsafe { libc::readdir(stream) };
1487 if entry.is_null() {
1488 let error = std::io::Error::last_os_error();
1489 unsafe { libc::closedir(stream) };
1492 if error.raw_os_error().is_some_and(|code| code != 0) {
1493 return Err(error);
1494 }
1495 break;
1496 }
1497 let name_bytes = unsafe {
1502 std::ffi::CStr::from_ptr((&raw const (*entry).d_name).cast::<libc::c_char>())
1503 };
1504 let Ok(name) = name_bytes.to_str() else {
1505 continue;
1506 };
1507 if name == "." || name == ".." {
1508 continue;
1509 }
1510 if entries.len() == max {
1511 unsafe { libc::closedir(stream) };
1513 return Err(entry_budget_error());
1514 }
1515 let kind = unsafe { (*entry).d_type };
1517 let is_directory = match kind {
1518 libc::DT_DIR => true,
1519 libc::DT_UNKNOWN => {
1520 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1523 let segment =
1524 c_string(name.as_bytes()).expect("directory entry names carry no NUL");
1525 let status = unsafe {
1529 libc::fstatat(
1530 libc::dirfd(stream),
1531 segment.as_ptr(),
1532 &raw mut stat,
1533 libc::AT_SYMLINK_NOFOLLOW,
1534 )
1535 };
1536 status == 0 && stat.st_mode & libc::S_IFMT == libc::S_IFDIR
1537 }
1538 _ => false,
1539 };
1540 entries.push((name.to_owned(), is_directory));
1541 }
1542 Ok(entries)
1543 }
1544
1545 fn errno_clear() {
1546 errno::set_errno(errno::Errno(0));
1547 }
1548
1549 fn clear_nonblock(file: &File) -> std::io::Result<()> {
1551 let fd = file.as_raw_fd();
1552 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1555 if flags < 0 {
1556 return Err(std::io::Error::last_os_error());
1557 }
1558 let status = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1560 if status < 0 {
1561 return Err(std::io::Error::last_os_error());
1562 }
1563 Ok(())
1564 }
1565
1566 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1567 matches!(error.raw_os_error(), Some(libc::ELOOP | libc::EMLINK))
1568 }
1569
1570 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1571 let _ = path;
1575 error.raw_os_error() == Some(libc::EISDIR)
1576 }
1577
1578 fn c_string(bytes: &[u8]) -> std::io::Result<CString> {
1579 CString::new(bytes).map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))
1580 }
1581
1582 #[cfg(test)]
1583 mod tests {
1584 use super::*;
1585
1586 #[test]
1587 fn open_root_refuses_a_symbolic_link_to_a_directory() {
1588 let base = std::env::temp_dir().join(format!(
1589 "powerio-core-open-root-{}-{}",
1590 std::process::id(),
1591 std::time::SystemTime::now()
1592 .duration_since(std::time::UNIX_EPOCH)
1593 .unwrap()
1594 .as_nanos()
1595 ));
1596 std::fs::create_dir_all(&base).unwrap();
1597 assert!(open_root(&base).is_ok());
1598 let link = base.join("link");
1599 std::os::unix::fs::symlink(&base, &link).unwrap();
1600 let error = open_root(&link).unwrap_err();
1601 assert!(
1602 is_symlink_refusal(&error) || error.kind() == std::io::ErrorKind::NotADirectory,
1603 "{error:?}"
1604 );
1605 std::fs::remove_dir_all(&base).unwrap();
1606 }
1607 }
1608}
1609
1610#[cfg(not(unix))]
1611mod platform {
1612 use std::fs::File;
1620 use std::path::{Path, PathBuf};
1621
1622 #[derive(Debug)]
1623 pub(super) struct RootHandle {
1624 root: PathBuf,
1625 }
1626
1627 pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1628 let file = open_reparse_refused(path)?;
1629 Ok(file)
1630 }
1631
1632 pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1633 drop(open_directory_pinned(path)?);
1637 Ok(RootHandle {
1638 root: path.to_path_buf(),
1639 })
1640 }
1641
1642 impl RootHandle {
1643 pub(super) fn open_beneath(&self, segments: &[String]) -> std::io::Result<File> {
1644 let mut path = self.root.clone();
1645 let (file_segment, directories) =
1646 segments.split_last().expect("resolution yields a file");
1647 let mut held = Vec::with_capacity(directories.len() + 1);
1651 held.push(open_directory_pinned(&self.root)?);
1652 for segment in directories {
1653 push_plain_segment(&mut path, segment)?;
1654 held.push(open_directory_pinned(&path)?);
1655 }
1656 push_plain_segment(&mut path, file_segment)?;
1657 let file = open_reparse_refused(&path)?;
1658 drop(held);
1659 Ok(file)
1660 }
1661
1662 pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1665 let handle = open_directory_pinned(&self.root)?;
1666 Ok(DirectoryHandle {
1667 path: self.root.clone(),
1668 _handle: handle,
1669 })
1670 }
1671 }
1672
1673 #[derive(Debug)]
1677 pub(super) struct DirectoryHandle {
1678 path: PathBuf,
1679 _handle: File,
1680 }
1681
1682 pub(super) fn open_child_directory(
1686 parent: &DirectoryHandle,
1687 name: &str,
1688 ) -> std::io::Result<DirectoryHandle> {
1689 let mut path = parent.path.clone();
1690 push_plain_segment(&mut path, name)?;
1691 let handle = open_directory_pinned(&path)?;
1692 Ok(DirectoryHandle {
1693 path,
1694 _handle: handle,
1695 })
1696 }
1697
1698 const ENTRY_BUDGET_MARKER: &str = "directory entry allowance exhausted";
1699
1700 fn entry_budget_error() -> std::io::Error {
1701 std::io::Error::other(ENTRY_BUDGET_MARKER)
1702 }
1703
1704 pub(super) fn is_entry_budget(error: &std::io::Error) -> bool {
1707 error.kind() == std::io::ErrorKind::Other && error.to_string().contains(ENTRY_BUDGET_MARKER)
1708 }
1709
1710 pub(super) fn list_entries(
1715 directory: &DirectoryHandle,
1716 max: usize,
1717 ) -> std::io::Result<Vec<(String, bool)>> {
1718 let mut entries = Vec::new();
1719 for entry in std::fs::read_dir(&directory.path)? {
1720 let entry = entry?;
1721 let Ok(name) = entry.file_name().into_string() else {
1722 continue;
1723 };
1724 if entries.len() == max {
1725 return Err(entry_budget_error());
1726 }
1727 let is_directory = entry.file_type()?.is_dir();
1728 entries.push((name, is_directory));
1729 }
1730 Ok(entries)
1731 }
1732
1733 fn push_plain_segment(path: &mut PathBuf, segment: &str) -> std::io::Result<()> {
1736 if !super::plain_segment(segment) {
1737 return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
1738 }
1739 path.push(segment);
1740 Ok(())
1741 }
1742
1743 #[cfg(windows)]
1749 fn open_directory_pinned(path: &Path) -> std::io::Result<File> {
1750 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1751
1752 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1753 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1754 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1755 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
1756 const FILE_SHARE_READ: u32 = 0x0000_0001;
1757 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
1758
1759 let file = std::fs::OpenOptions::new()
1760 .read(true)
1761 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1762 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
1763 .open(path)?;
1764 let attributes = file.metadata()?.file_attributes();
1765 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1766 return Err(symlink_error());
1767 }
1768 if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1769 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1770 }
1771 Ok(file)
1772 }
1773
1774 #[cfg(not(windows))]
1775 fn open_directory_pinned(path: &Path) -> std::io::Result<File> {
1776 let metadata = std::fs::symlink_metadata(path)?;
1777 if metadata.file_type().is_symlink() {
1778 return Err(symlink_error());
1779 }
1780 if !metadata.is_dir() {
1781 return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory));
1782 }
1783 File::open(path)
1784 }
1785
1786 #[cfg(windows)]
1787 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1788 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1789
1790 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1791 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1792
1793 let file = std::fs::OpenOptions::new()
1794 .read(true)
1795 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
1796 .open(path)?;
1797 let attributes = file.metadata()?.file_attributes();
1798 if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1799 return Err(symlink_error());
1800 }
1801 Ok(file)
1802 }
1803
1804 #[cfg(not(windows))]
1805 fn open_reparse_refused(path: &Path) -> std::io::Result<File> {
1806 let metadata = std::fs::symlink_metadata(path)?;
1807 if metadata.file_type().is_symlink() {
1808 return Err(symlink_error());
1809 }
1810 File::open(path)
1811 }
1812
1813 fn symlink_error() -> std::io::Error {
1814 std::io::Error::new(std::io::ErrorKind::InvalidData, "symbolic link refused")
1815 }
1816
1817 pub(super) fn is_symlink_refusal(error: &std::io::Error) -> bool {
1818 error.kind() == std::io::ErrorKind::InvalidData
1819 && error.to_string().contains("symbolic link refused")
1820 }
1821
1822 pub(super) fn is_directory_open_failure(error: &std::io::Error, path: &Path) -> bool {
1823 let _ = error;
1824 std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
1825 }
1826}
1827
1828pub const MEMORY_SOURCE_NAME: &str = "<memory>";
1832
1833pub trait IntoSource {
1844 fn into_source(self) -> Result<Source, Error>;
1850}
1851
1852impl IntoSource for Source {
1853 fn into_source(self) -> Result<Source, Error> {
1854 Ok(self)
1855 }
1856}
1857
1858impl IntoSource for &Source {
1859 fn into_source(self) -> Result<Source, Error> {
1860 Ok(self.clone())
1861 }
1862}
1863
1864macro_rules! into_source_by_name {
1866 ($($input:ty),* $(,)?) => {
1867 $(
1868 impl IntoSource for $input {
1869 fn into_source(self) -> Result<Source, Error> {
1870 Source::open(PathBuf::from(self))
1871 }
1872 }
1873 )*
1874 };
1875}
1876
1877into_source_by_name!(&str, &String, String, &Path, &PathBuf, PathBuf);
1878
1879macro_rules! into_source_by_content {
1881 ($($input:ty => $bytes:expr),* $(,)?) => {
1882 $(
1883 impl IntoSource for $input {
1884 fn into_source(self) -> Result<Source, Error> {
1885 let bytes: Arc<[u8]> = $bytes(self);
1886 Source::from_memory(MEMORY_SOURCE_NAME, bytes)
1887 }
1888 }
1889 )*
1890 };
1891}
1892
1893into_source_by_content!(
1894 &[u8] => Arc::<[u8]>::from,
1895 Vec<u8> => Arc::<[u8]>::from,
1896 Arc<[u8]> => std::convert::identity,
1897);
1898
1899impl<const N: usize> IntoSource for &[u8; N] {
1900 fn into_source(self) -> Result<Source, Error> {
1901 Source::from_memory(MEMORY_SOURCE_NAME, Arc::<[u8]>::from(self.as_slice()))
1902 }
1903}
1904
1905#[cfg(test)]
1906mod tests {
1907 use std::time::{SystemTime, UNIX_EPOCH};
1908
1909 use super::*;
1910 use crate::ArtifactPath;
1911
1912 static PROCESS_RESOURCE_TESTS: Mutex<()> = Mutex::new(());
1917
1918 fn process_resource_guard() -> MutexGuard<'static, ()> {
1919 PROCESS_RESOURCE_TESTS
1920 .lock()
1921 .unwrap_or_else(std::sync::PoisonError::into_inner)
1922 }
1923
1924 struct CountingAllocator;
1927
1928 static ALLOCATED_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1929
1930 thread_local! {
1931 static MEASURING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1932 }
1933
1934 fn measured_bytes<T>(work: impl FnOnce() -> T) -> (T, usize) {
1935 let _ = MEASURING.try_with(|flag| flag.set(true));
1936 let before = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1937 let value = work();
1938 let after = ALLOCATED_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1939 let _ = MEASURING.try_with(|flag| flag.set(false));
1940 (value, after.saturating_sub(before))
1941 }
1942
1943 unsafe impl std::alloc::GlobalAlloc for CountingAllocator {
1946 unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
1947 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1948 ALLOCATED_BYTES.fetch_add(layout.size(), std::sync::atomic::Ordering::Relaxed);
1949 }
1950 unsafe { std::alloc::System.alloc(layout) }
1951 }
1952
1953 unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
1954 unsafe { std::alloc::System.dealloc(ptr, layout) }
1955 }
1956
1957 unsafe fn realloc(
1958 &self,
1959 ptr: *mut u8,
1960 layout: std::alloc::Layout,
1961 new_size: usize,
1962 ) -> *mut u8 {
1963 if MEASURING.try_with(std::cell::Cell::get).unwrap_or(false) {
1964 ALLOCATED_BYTES.fetch_add(new_size, std::sync::atomic::Ordering::Relaxed);
1965 }
1966 unsafe { std::alloc::System.realloc(ptr, layout, new_size) }
1967 }
1968 }
1969
1970 #[global_allocator]
1971 static COUNTING: CountingAllocator = CountingAllocator;
1972
1973 fn test_root(name: &str) -> PathBuf {
1974 let nonce = SystemTime::now()
1975 .duration_since(UNIX_EPOCH)
1976 .unwrap()
1977 .as_nanos();
1978 std::env::temp_dir().join(format!(
1979 "powerio-core-{name}-{}-{nonce}",
1980 std::process::id()
1981 ))
1982 }
1983
1984 #[test]
1985 fn format_ids_use_the_exact_open_grammar() {
1986 for id in ["matpower", "psse-raw", "doe-go-3", "x1"] {
1987 assert_eq!(FormatId::new(id).unwrap().as_str(), id);
1988 }
1989 for id in ["", "1matpower", "MATPOWER", "psse--raw", "psse-", "p_sse"] {
1990 assert!(FormatId::new(id).is_err(), "{id}");
1991 }
1992 assert!(FormatId::new("a".repeat(MAX_FORMAT_ID_BYTES + 1)).is_err());
1993 }
1994
1995 #[test]
1996 fn memory_sources_retain_arbitrary_binary_bytes_without_copying() {
1997 let bytes: Arc<[u8]> = vec![0, 255, 0, 128].into();
1998 let pointer = bytes.as_ptr();
1999 let source = Source::from_memory("input.bin", Arc::clone(&bytes))
2000 .unwrap()
2001 .with_format(FormatId::new("pwb").unwrap());
2002 let buffer = source.primary_buffer().unwrap();
2003 assert_eq!(buffer.bytes(), [0, 255, 0, 128]);
2004 assert_eq!(buffer.bytes().as_ptr(), pointer);
2005 assert_eq!(source.format().unwrap().as_str(), "pwb");
2006 assert!(Source::from_memory("", Vec::new()).is_err());
2007 assert!(Source::from_memory("x\0y", Vec::new()).is_err());
2008 }
2009
2010 #[test]
2011 fn a_bom_is_retained_and_skipped_for_the_parser_without_a_second_buffer() {
2012 let bytes: Vec<u8> = [0xEF, 0xBB, 0xBF, b'm', b'p', b'c'].to_vec();
2013 let source = Source::from_memory("case.m", bytes).unwrap();
2014 let buffer = source.primary_buffer().unwrap();
2015 assert!(buffer.has_utf8_bom());
2016 assert_eq!(buffer.bytes().len(), 6);
2017 assert_eq!(buffer.content_bytes(), b"mpc");
2018 assert_eq!(
2020 buffer.content_bytes().as_ptr(),
2021 buffer.bytes()[3..].as_ptr()
2022 );
2023
2024 let plain = Source::from_memory("case.m", b"mpc".to_vec()).unwrap();
2025 let plain = plain.primary_buffer().unwrap();
2026 assert!(!plain.has_utf8_bom());
2027 assert_eq!(plain.content_bytes(), plain.bytes());
2028 }
2029
2030 #[test]
2031 fn a_memory_source_resolves_named_buffers_and_never_the_filesystem() {
2032 let source = Source::from_memory("master.dss", b"redirect sub/feeder.dss".to_vec())
2033 .unwrap()
2034 .with_named_buffer("sub/feeder.dss", b"feeder".to_vec())
2035 .unwrap();
2036 let primary = source.primary_buffer().unwrap();
2037 let feeder = source
2038 .referenced_buffer(&primary, "sub/feeder.dss")
2039 .unwrap();
2040 assert_eq!(feeder.bytes(), b"feeder");
2041
2042 let sibling = source
2045 .referenced_buffer(&feeder, "../sub/feeder.dss")
2046 .unwrap();
2047 assert_eq!(sibling.bytes(), b"feeder");
2048
2049 assert!(source.referenced_buffer(&primary, "missing.dss").is_err());
2050 let escape = source.referenced_buffer(&primary, "../outside.dss");
2051 assert_eq!(
2052 escape.unwrap_err().category(),
2053 crate::ErrorCategory::Request
2054 );
2055 }
2056
2057 #[test]
2058 fn a_file_source_acquires_referenced_files_beneath_its_containing_directory() {
2059 let root = test_root("file-refs");
2060 std::fs::create_dir_all(root.join("sub")).unwrap();
2061 std::fs::write(root.join("master.dss"), b"master").unwrap();
2062 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
2063 std::fs::write(root.join("sub/coords.csv"), b"coords").unwrap();
2064
2065 let source = Source::open(root.join("master.dss")).unwrap();
2066 let primary = source.primary_buffer().unwrap();
2067 assert_eq!(primary.bytes(), b"master");
2068
2069 let feeder = source
2070 .referenced_buffer(&primary, "sub/feeder.dss")
2071 .unwrap();
2072 assert_eq!(feeder.bytes(), b"feeder");
2073 let coords = source.referenced_buffer(&feeder, "coords.csv").unwrap();
2076 assert_eq!(coords.bytes(), b"coords");
2077 let master_again = source.referenced_buffer(&feeder, "../master.dss").unwrap();
2079 assert_eq!(master_again.bytes(), b"master");
2080 let escape = source.referenced_buffer(&primary, "../escape.dss");
2081 assert_eq!(
2082 escape.unwrap_err().category(),
2083 crate::ErrorCategory::Request
2084 );
2085
2086 let again = source
2088 .referenced_buffer(&primary, "sub/feeder.dss")
2089 .unwrap();
2090 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
2091 assert_eq!(source.acquired_buffers().len(), 4);
2092 std::fs::remove_dir_all(root).unwrap();
2093 }
2094
2095 #[test]
2096 fn an_explicitly_wider_root_admits_shared_files_and_still_confines() {
2097 let root = test_root("wider-root");
2098 std::fs::create_dir_all(root.join("cases")).unwrap();
2099 std::fs::create_dir_all(root.join("shared")).unwrap();
2100 std::fs::write(root.join("cases/master.dss"), b"master").unwrap();
2101 std::fs::write(root.join("shared/wires.dss"), b"wires").unwrap();
2102
2103 let narrow = Source::open(root.join("cases/master.dss")).unwrap();
2105 let primary = narrow.primary_buffer().unwrap();
2106 assert!(
2107 narrow
2108 .referenced_buffer(&primary, "../shared/wires.dss")
2109 .is_err()
2110 );
2111
2112 let wide = Source::open(root.join("cases/master.dss"))
2114 .unwrap()
2115 .with_acquisition_root(&root)
2116 .unwrap();
2117 let primary = wide.primary_buffer().unwrap();
2118 let wires = wide
2119 .referenced_buffer(&primary, "../shared/wires.dss")
2120 .unwrap();
2121 assert_eq!(wires.bytes(), b"wires");
2122 assert!(
2123 wide.referenced_buffer(&primary, "../../etc/passwd")
2124 .is_err()
2125 );
2126
2127 let outside = test_root("wider-root-outside");
2129 std::fs::create_dir_all(&outside).unwrap();
2130 assert!(
2131 Source::open(root.join("cases/master.dss"))
2132 .unwrap()
2133 .with_acquisition_root(&outside)
2134 .is_err()
2135 );
2136 std::fs::remove_dir_all(outside).ok();
2137 std::fs::remove_dir_all(root).unwrap();
2138 }
2139
2140 #[test]
2141 fn directory_buffers_are_lazy_cached_and_binary_safe() {
2142 let root = test_root("directory");
2143 std::fs::create_dir_all(root.join("nested")).unwrap();
2144 std::fs::write(root.join("nested/data.bin"), [0, 255, 7]).unwrap();
2145 let source = Source::open(&root).unwrap();
2146 assert!(source.is_directory());
2147 assert!(source.acquired_buffers().is_empty());
2148 let name = ArtifactPath::new("nested/data.bin").unwrap();
2149 let first = source.buffer(&name).unwrap();
2150 let second = source.buffer(&name).unwrap();
2151 assert_eq!(first.bytes(), [0, 255, 7]);
2152 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
2153 assert_eq!(source.acquired_buffers().len(), 1);
2154 std::fs::remove_dir_all(root).unwrap();
2155 }
2156
2157 #[cfg(unix)]
2158 #[test]
2159 fn source_acquisition_refuses_root_and_child_symlinks() {
2160 use std::os::unix::fs::symlink;
2161
2162 let root = test_root("symlink");
2163 std::fs::create_dir_all(&root).unwrap();
2164 std::fs::write(root.join("real.bin"), b"real").unwrap();
2165 symlink(root.join("real.bin"), root.join("link.bin")).unwrap();
2166 let source = Source::open(&root).unwrap();
2167 let error = source
2168 .buffer(&ArtifactPath::new("link.bin").unwrap())
2169 .unwrap_err();
2170 assert_eq!(error.category(), crate::ErrorCategory::Request);
2171
2172 std::fs::create_dir_all(root.join("real-dir")).unwrap();
2174 std::fs::write(root.join("real-dir/inner.bin"), b"inner").unwrap();
2175 symlink(root.join("real-dir"), root.join("link-dir")).unwrap();
2176 let error = source
2177 .buffer(&ArtifactPath::new("link-dir/inner.bin").unwrap())
2178 .unwrap_err();
2179 assert_eq!(error.category(), crate::ErrorCategory::Request);
2180
2181 let root_link = root.with_extension("link");
2182 symlink(&root, &root_link).unwrap();
2183 assert!(Source::open(&root_link).is_err());
2184 std::fs::remove_file(root_link).unwrap();
2185 std::fs::remove_dir_all(root).unwrap();
2186 }
2187
2188 #[cfg(unix)]
2189 #[test]
2190 fn a_named_pipe_is_refused_promptly_and_siblings_still_acquire() {
2191 use std::os::unix::ffi::OsStrExt;
2192
2193 let root = test_root("fifo");
2194 std::fs::create_dir_all(&root).unwrap();
2195 std::fs::write(root.join("real.csv"), b"real").unwrap();
2196 let fifo = root.join("pipe.dat");
2197 let c_path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
2198 assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
2201
2202 let (sender, receiver) = std::sync::mpsc::channel();
2206 let opened_root = root.clone();
2207 let worker = std::thread::spawn(move || {
2208 let open_error = Source::open(opened_root.join("pipe.dat")).map(|_| ());
2209 let directory = Source::open(&opened_root).unwrap();
2210 let buffer_error = directory
2211 .buffer(&ArtifactPath::new("pipe.dat").unwrap())
2212 .map(|_| ());
2213 let sibling = directory
2214 .buffer(&ArtifactPath::new("real.csv").unwrap())
2215 .map(|buffer| buffer.bytes().to_vec());
2216 sender.send((open_error, buffer_error, sibling)).unwrap();
2217 });
2218 let (open_error, buffer_error, sibling) = receiver
2219 .recv_timeout(std::time::Duration::from_secs(10))
2220 .expect("acquisition on a writerless pipe completes promptly");
2221 worker.join().unwrap();
2222 assert_eq!(
2223 open_error.unwrap_err().category(),
2224 crate::ErrorCategory::Request
2225 );
2226 assert_eq!(
2227 buffer_error.unwrap_err().category(),
2228 crate::ErrorCategory::Request
2229 );
2230 assert_eq!(sibling.unwrap(), b"real");
2231 std::fs::remove_dir_all(root).unwrap();
2232 }
2233
2234 #[test]
2235 fn default_budget_accepts_a_primary_and_reference_larger_than_64_mib() {
2236 assert_eq!(DEFAULT_SOURCE_BYTES, 1_073_741_824);
2237 let root = test_root("large-default-budget");
2238 std::fs::create_dir_all(&root).unwrap();
2239 let path = root.join("large.dat");
2240 let size = (64 << 20) + 1;
2241 std::fs::File::create(&path).unwrap().set_len(size).unwrap();
2242 let source = Source::open(&path).unwrap();
2243 assert_eq!(source.primary_buffer().unwrap().bytes().len() as u64, size);
2244 drop(source);
2245 let directory = Source::open(&root).unwrap();
2246 let name = crate::ArtifactPath::new("large.dat").unwrap();
2247 assert_eq!(directory.buffer(&name).unwrap().bytes().len() as u64, size);
2248 std::fs::remove_dir_all(root).unwrap();
2249 }
2250
2251 #[test]
2252 fn primary_limits_validate_configuration_and_bound_allocation() {
2253 use std::ffi::OsStr;
2254 assert_eq!(parse_primary_limit(None).unwrap(), DEFAULT_PRIMARY_BYTES);
2255 assert_eq!(parse_primary_limit(Some(OsStr::new("32"))).unwrap(), 32);
2256 for value in ["", "0", "-1", "+1", " 32", "3KiB", "18446744073709551615"] {
2257 assert!(parse_primary_limit(Some(OsStr::new(value))).is_err());
2258 }
2259 let root = test_root("primary-budget");
2260 std::fs::create_dir_all(&root).unwrap();
2261 let path = root.join("input");
2262 std::fs::write(&path, b"abcd").unwrap();
2263 for limit in [4, 5] {
2264 let bytes = read_open_file(
2265 std::fs::File::open(&path).unwrap(),
2266 "input",
2267 ReadBudget::Primary(limit),
2268 )
2269 .unwrap();
2270 assert_eq!(&*bytes, b"abcd");
2271 }
2272 assert!(
2273 read_open_file(
2274 std::fs::File::open(&path).unwrap(),
2275 "input",
2276 ReadBudget::Primary(3)
2277 )
2278 .is_err()
2279 );
2280 std::fs::File::create(&path)
2281 .unwrap()
2282 .set_len(DEFAULT_PRIMARY_BYTES * 4)
2283 .unwrap();
2284 let (error, allocated) = measured_bytes(|| {
2285 read_open_file(
2286 std::fs::File::open(&path).unwrap(),
2287 "input",
2288 ReadBudget::Primary(DEFAULT_PRIMARY_BYTES),
2289 )
2290 .unwrap_err()
2291 });
2292 assert!(error.to_string().contains("primary source"));
2293 assert!(allocated < 4096);
2294 std::fs::remove_dir_all(root).unwrap();
2295 }
2296
2297 #[test]
2298 fn open_validates_both_limits_before_reading_files_or_directories() {
2299 fn lookup(
2302 primary: Option<&'static str>,
2303 referenced: Option<&'static str>,
2304 ) -> impl Fn(&'static str) -> Option<OsString> {
2305 move |name| match name {
2306 PRIMARY_LIMIT_VAR => primary.map(OsString::from),
2307 REFERENCED_LIMIT_VAR => referenced.map(OsString::from),
2308 other => panic!("unexpected variable {other}"),
2309 }
2310 }
2311 fn code(result: Result<Source, Error>) -> &'static str {
2312 result
2313 .err()
2314 .and_then(|e| e.info())
2315 .expect("registered")
2316 .code
2317 }
2318 let invalid = crate::codes::REQUEST_SOURCE_INVALID_LIMIT.code;
2319
2320 assert_eq!(
2321 SourceLimits::from_lookup(lookup(None, None)).unwrap(),
2322 SourceLimits {
2323 primary: DEFAULT_PRIMARY_BYTES,
2324 referenced: MAX_REFERENCED_BYTES,
2325 }
2326 );
2327 let root = test_root("limit-validation");
2328 std::fs::create_dir_all(&root).unwrap();
2329 let file = root.join("input");
2330 std::fs::write(&file, b"abcd").unwrap();
2331
2332 for (primary, referenced) in [(Some("0"), None), (None, Some("-1"))] {
2334 let opened = Source::open_with_limits(&root, lookup(primary, referenced));
2335 assert_eq!(code(opened), invalid);
2336 }
2337 let opened = Source::open_with_limits(&file, lookup(Some("3"), Some("1KiB")));
2340 assert_eq!(code(opened), invalid);
2341 let opened = Source::open_with_limits(&file, lookup(Some("3"), Some("9")));
2342 assert_eq!(code(opened), crate::codes::READ_IO_PRIMARY_BUDGET.code);
2343
2344 let source = Source::open_with_limits(&file, lookup(Some("4"), Some("7"))).unwrap();
2345 assert_eq!(source.primary_buffer().unwrap().bytes(), b"abcd");
2346 assert_eq!(source.acquisition_byte_limit().unwrap(), 7);
2347 let directory = Source::open_with_limits(&root, lookup(Some("1"), Some("5"))).unwrap();
2348 assert_eq!(directory.acquisition_byte_limit().unwrap(), 5);
2349 std::fs::remove_dir_all(root).unwrap();
2350 }
2351
2352 #[test]
2353 fn referenced_limits_are_aggregate_and_cached_reads_are_free() {
2354 use std::ffi::OsStr;
2355 assert_eq!(parse_byte_limit("limit", None, 8).unwrap(), 8);
2356 assert_eq!(
2357 parse_byte_limit("limit", Some(OsStr::new("12")), 8).unwrap(),
2358 12
2359 );
2360 for value in ["", "0", "-1", "+1", " 8", "8MiB", "18446744073709551615"] {
2361 assert!(parse_byte_limit("limit", Some(OsStr::new(value)), 8).is_err());
2362 }
2363 let root = test_root("small-referenced-budget");
2364 std::fs::create_dir_all(&root).unwrap();
2365 std::fs::write(root.join("a.xml"), b"abcd").unwrap();
2366 std::fs::write(root.join("b.xml"), b"efgh").unwrap();
2367 std::fs::write(root.join("c.xml"), b"i").unwrap();
2368 let mut source = Source::open(&root).unwrap();
2369 let SourceProvider::Directory { acquisition } = Arc::get_mut(&mut source.provider).unwrap()
2370 else {
2371 panic!("directory provider");
2372 };
2373 acquisition.max_bytes = 8;
2374 let name = |s: &str| crate::ArtifactPath::new(s).unwrap();
2375 source.buffer(&name("a.xml")).unwrap();
2376 source.clone().buffer(&name("a.xml")).unwrap();
2377 source.buffer(&name("b.xml")).unwrap();
2378 let error = source.buffer(&name("c.xml")).unwrap_err();
2379 assert!(error.to_string().contains("8 byte acquisition budget"));
2380 std::fs::remove_dir_all(root).unwrap();
2381 }
2382
2383 #[test]
2384 fn an_over_budget_referenced_file_is_refused_before_allocation() {
2385 let root = test_root("budget");
2386 std::fs::create_dir_all(&root).unwrap();
2387 std::fs::write(root.join("master.dss"), b"master").unwrap();
2388 let big = std::fs::File::create(root.join("big.dat")).unwrap();
2392 big.set_len(MAX_REFERENCED_BYTES * 4).unwrap();
2393 drop(big);
2394
2395 let source = Source::open(root.join("master.dss")).unwrap();
2396 let primary = source.primary_buffer().unwrap();
2397 let (error, allocated) =
2402 measured_bytes(|| source.referenced_buffer(&primary, "big.dat").unwrap_err());
2403 assert!(error.to_string().contains("acquisition budget"), "{error}");
2404 assert!(
2405 (allocated as u64) < MAX_REFERENCED_BYTES / 16,
2406 "the refused acquisition allocated {allocated} bytes"
2407 );
2408 std::fs::remove_dir_all(root).unwrap();
2409 }
2410
2411 #[test]
2412 fn a_caller_reference_limit_refuses_before_allocation_or_acquisition() {
2413 let root = test_root("caller-reference-budget");
2414 std::fs::create_dir_all(&root).unwrap();
2415 std::fs::write(root.join("master.glm"), b"master").unwrap();
2416 let limit = 64 << 20;
2417 let big = std::fs::File::create(root.join("big.glm")).unwrap();
2418 big.set_len(limit + 1).unwrap();
2419 drop(big);
2420 let source = Source::open(root.join("master.glm")).unwrap();
2421 let primary = source.primary_buffer().unwrap();
2422
2423 let (error, allocated) = measured_bytes(|| {
2424 source
2425 .referenced_buffer_with_limit(&primary, "big.glm", limit)
2426 .unwrap_err()
2427 });
2428 assert_eq!(
2429 error.diagnostics()[0].code(),
2430 crate::codes::READ_IO_BUFFER_BUDGET.code
2431 );
2432 assert!(
2433 (allocated as u64) < limit / 16,
2434 "allocated {allocated} bytes"
2435 );
2436 assert!(error.retained_source().is_some());
2437 assert_eq!(source.acquired_buffers().len(), 1);
2438 let SourceProvider::File { acquisition, .. } = &*source.provider else {
2439 panic!("file provider");
2440 };
2441 assert_eq!(acquisition.lock().bytes, 0);
2442 assert_eq!(acquisition.lock().files, 0);
2443 std::fs::remove_dir_all(root).unwrap();
2444 }
2445
2446 #[test]
2447 fn caller_reference_limits_check_cached_buffers_and_preserve_the_source_budget() {
2448 let root = test_root("caller-reference-cache");
2449 std::fs::create_dir_all(&root).unwrap();
2450 std::fs::write(root.join("master.glm"), b"master").unwrap();
2451 std::fs::write(root.join("first.glm"), b"abcd").unwrap();
2452 std::fs::write(root.join("second.glm"), b"efgh").unwrap();
2453 std::fs::write(root.join("third.glm"), b"i").unwrap();
2454 let source = Source::open_with_limits(&root.join("master.glm"), |name| {
2455 (name == REFERENCED_LIMIT_VAR).then(|| OsString::from("8"))
2456 })
2457 .unwrap();
2458 let primary = source.primary_buffer().unwrap();
2459 let error = source
2460 .referenced_buffer_with_limit(&primary, "first.glm", 3)
2461 .unwrap_err();
2462 assert_eq!(
2463 error.diagnostics()[0].code(),
2464 crate::codes::READ_IO_BUFFER_BUDGET.code
2465 );
2466 assert_eq!(source.acquired_buffers().len(), 1);
2467 let first = source
2468 .referenced_buffer_with_limit(&primary, "first.glm", 4)
2469 .unwrap();
2470 assert_eq!(first.bytes(), b"abcd");
2471 let error = source
2472 .referenced_buffer_with_limit(&primary, "first.glm", 3)
2473 .unwrap_err();
2474 assert_eq!(
2475 error.diagnostics()[0].code(),
2476 crate::codes::READ_IO_BUFFER_BUDGET.code
2477 );
2478 let cached = source.referenced_buffer(&primary, "first.glm").unwrap();
2481 assert_eq!(cached.bytes().as_ptr(), first.bytes().as_ptr());
2482 source.referenced_buffer(&primary, "second.glm").unwrap();
2483 let error = source
2484 .referenced_buffer_with_limit(&primary, "third.glm", 1)
2485 .unwrap_err();
2486 assert_eq!(
2487 error.diagnostics()[0].code(),
2488 crate::codes::READ_IO_REFERENCE_BUDGET.code
2489 );
2490 assert_eq!(
2492 source
2493 .referenced_buffer_with_limit(&primary, "first.glm", 4)
2494 .unwrap()
2495 .bytes(),
2496 b"abcd"
2497 );
2498 std::fs::remove_dir_all(root).unwrap();
2499 }
2500
2501 #[test]
2502 fn caller_reference_limits_also_apply_to_memory_buffers_including_zero() {
2503 let source = Source::from_memory("master.glm", &b"master"[..])
2504 .unwrap()
2505 .with_named_buffer("part.glm", &b"ab"[..])
2506 .unwrap()
2507 .with_named_buffer("empty.glm", &b""[..])
2508 .unwrap();
2509 let primary = source.primary_buffer().unwrap();
2510 for limit in [0, 1] {
2511 let error = source
2512 .referenced_buffer_with_limit(&primary, "part.glm", limit)
2513 .unwrap_err();
2514 assert_eq!(
2515 error.diagnostics()[0].code(),
2516 crate::codes::READ_IO_BUFFER_BUDGET.code
2517 );
2518 }
2519 assert_eq!(
2520 source
2521 .referenced_buffer_with_limit(&primary, "part.glm", 2)
2522 .unwrap()
2523 .bytes(),
2524 b"ab"
2525 );
2526 assert_eq!(
2527 source
2528 .referenced_buffer_with_limit(&primary, "empty.glm", 0)
2529 .unwrap()
2530 .bytes(),
2531 b""
2532 );
2533 }
2534
2535 #[cfg(unix)]
2536 #[test]
2537 fn racing_entry_listing_never_names_files_outside_the_root() {
2538 use std::os::unix::fs::symlink;
2539
2540 let root = test_root("race-list");
2541 std::fs::create_dir_all(root.join("sub")).unwrap();
2542 std::fs::write(root.join("sub/inside.txt"), b"inside").unwrap();
2543 let outside = test_root("race-list-outside");
2544 std::fs::create_dir_all(&outside).unwrap();
2545 std::fs::write(outside.join("outside-only.txt"), b"outside").unwrap();
2546
2547 let source = Source::open(&root).unwrap();
2548 let stop = std::sync::atomic::AtomicBool::new(false);
2549 std::thread::scope(|scope| {
2550 let flipper = scope.spawn(|| {
2551 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
2552 let _ = std::fs::remove_dir_all(root.join("sub"));
2553 let _ = symlink(&outside, root.join("sub"));
2554 let _ = std::fs::remove_file(root.join("sub"));
2555 let _ = std::fs::create_dir(root.join("sub"));
2556 let _ = std::fs::write(root.join("sub/inside.txt"), b"inside");
2557 }
2558 });
2559 for _ in 0..50 {
2560 if let Ok(names) = source.entry_names() {
2563 assert!(
2564 names
2565 .iter()
2566 .all(|name| !name.as_str().contains("outside-only")),
2567 "{names:?}"
2568 );
2569 }
2570 }
2571 stop.store(true, std::sync::atomic::Ordering::Relaxed);
2572 flipper.join().unwrap();
2573 });
2574 std::fs::remove_dir_all(&root).ok();
2575 std::fs::remove_dir_all(&outside).ok();
2576 }
2577
2578 #[test]
2579 fn referenced_names_must_be_portable_relative_paths() {
2580 let root = test_root("portable-names");
2581 std::fs::create_dir_all(root.join("sub")).unwrap();
2582 std::fs::write(root.join("master.dss"), b"master").unwrap();
2583 std::fs::write(root.join("sub/feeder.dss"), b"feeder").unwrap();
2584 let source = Source::open(root.join("master.dss")).unwrap();
2585 let primary = source.primary_buffer().unwrap();
2586
2587 for name in ["..\\escape.dss", "\\escape.dss", "C:\\escape.dss", "C:x"] {
2590 let error = source.referenced_buffer(&primary, name).unwrap_err();
2591 assert_eq!(
2592 error.category(),
2593 crate::ErrorCategory::Request,
2594 "{name}: {error}"
2595 );
2596 }
2597 assert!(source.root_buffer("..\\master.dss").is_err());
2598 assert!(source.root_buffer("\\master.dss").is_err());
2599
2600 let feeder = source
2602 .referenced_buffer(&primary, "sub/feeder.dss")
2603 .unwrap();
2604 assert_eq!(feeder.bytes(), b"feeder");
2605
2606 let absolute = root.canonicalize().unwrap().join("sub").join("feeder.dss");
2612 let again = source
2613 .referenced_buffer(&primary, absolute.to_str().unwrap())
2614 .unwrap();
2615 assert_eq!(again.bytes().as_ptr(), feeder.bytes().as_ptr());
2616 assert!(
2618 source
2619 .acquired_buffers()
2620 .iter()
2621 .all(|buffer| !buffer.name().contains("escape"))
2622 );
2623 std::fs::remove_dir_all(root).unwrap();
2624 }
2625
2626 #[cfg(unix)]
2627 #[test]
2628 fn live_sources_hold_no_directory_descriptors_before_acquisition() {
2629 fn open_descriptor_count() -> usize {
2630 let table = if cfg!(target_os = "macos") {
2631 "/dev/fd"
2632 } else {
2633 "/proc/self/fd"
2634 };
2635 std::fs::read_dir(table).unwrap().count()
2636 }
2637
2638 let _guard = process_resource_guard();
2639 let root = test_root("fd-count");
2640 std::fs::create_dir_all(&root).unwrap();
2641 std::fs::write(root.join("case.m"), b"case").unwrap();
2642 std::fs::write(root.join("ref.csv"), b"ref").unwrap();
2643
2644 let before = open_descriptor_count();
2645 let sources: Vec<Source> = (0..300)
2646 .map(|_| Source::open(root.join("case.m")).unwrap())
2647 .collect();
2648 let held = open_descriptor_count();
2649 assert!(
2650 held <= before + 4,
2651 "{} sources hold {} descriptors over the baseline {}",
2652 sources.len(),
2653 held - before,
2654 before
2655 );
2656
2657 for source in sources.iter().take(32) {
2662 let primary = source.primary_buffer().unwrap();
2663 let first = source.referenced_buffer(&primary, "ref.csv").unwrap();
2664 let second = source.referenced_buffer(&primary, "ref.csv").unwrap();
2665 assert_eq!(first.bytes().as_ptr(), second.bytes().as_ptr());
2666 }
2667 drop(sources);
2668 std::fs::remove_dir_all(root).unwrap();
2669 }
2670
2671 #[test]
2672 fn entry_listing_returns_names_of_every_length_exactly() {
2673 let root = test_root("name-lengths");
2676 std::fs::create_dir_all(&root).unwrap();
2677 let long = "n".repeat(200);
2678 for name in ["a", "medium-name.csv", long.as_str()] {
2679 std::fs::write(root.join(name), b"x").unwrap();
2680 }
2681 let source = Source::open(&root).unwrap();
2682 let mut names: Vec<String> = source
2683 .entry_names()
2684 .unwrap()
2685 .iter()
2686 .map(|name| name.as_str().to_owned())
2687 .collect();
2688 names.sort();
2689 let mut expected = vec!["a".to_owned(), "medium-name.csv".to_owned(), long];
2690 expected.sort();
2691 assert_eq!(names, expected);
2692 std::fs::remove_dir_all(root).unwrap();
2693 }
2694
2695 #[cfg(unix)]
2696 #[test]
2697 fn a_directory_nested_past_the_depth_bound_is_refused_promptly() {
2698 use std::os::fd::{AsRawFd, FromRawFd};
2699
2700 let _guard = process_resource_guard();
2703
2704 let root = test_root("deep-chain");
2705 std::fs::create_dir_all(&root).unwrap();
2706 let name = std::ffi::CString::new("d").unwrap();
2709 let mut level = std::fs::File::open(&root).unwrap();
2710 for _ in 0..(MAX_REFERENCED_DEPTH + 40) {
2711 unsafe {
2715 assert_eq!(libc::mkdirat(level.as_raw_fd(), name.as_ptr(), 0o755), 0);
2716 let fd = libc::openat(
2717 level.as_raw_fd(),
2718 name.as_ptr(),
2719 libc::O_RDONLY | libc::O_CLOEXEC,
2720 );
2721 assert!(fd >= 0);
2722 level = std::fs::File::from_raw_fd(fd);
2723 }
2724 }
2725 drop(level);
2726
2727 let (sender, receiver) = std::sync::mpsc::channel();
2728 let listed_root = root.clone();
2729 let worker = std::thread::spawn(move || {
2730 let source = Source::open(&listed_root).unwrap();
2731 sender.send(source.entry_names().map(|_| ())).unwrap();
2732 });
2733 let outcome = receiver
2734 .recv_timeout(std::time::Duration::from_secs(10))
2735 .expect("the depth refusal returns promptly");
2736 worker.join().unwrap();
2737 let error = outcome.expect_err("a chain past the depth bound refuses");
2738 assert!(error.to_string().contains("levels deep"), "{error}");
2739
2740 let mut fds = vec![std::fs::File::open(&root).unwrap()];
2743 loop {
2744 let last = fds.last().unwrap();
2745 let fd = unsafe {
2747 libc::openat(
2748 last.as_raw_fd(),
2749 name.as_ptr(),
2750 libc::O_RDONLY | libc::O_CLOEXEC,
2751 )
2752 };
2753 if fd < 0 {
2754 break;
2755 }
2756 fds.push(unsafe { std::fs::File::from_raw_fd(fd) });
2758 }
2759 while fds.len() > 1 {
2760 let parent = &fds[fds.len() - 2];
2761 unsafe {
2763 libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR);
2764 }
2765 fds.pop();
2766 }
2767 drop(fds);
2768 std::fs::remove_dir_all(&root).unwrap();
2769 }
2770
2771 #[test]
2772 fn a_directory_past_the_entry_budget_is_refused_with_bounded_memory() {
2773 let root = test_root("entry-budget");
2774 std::fs::create_dir_all(&root).unwrap();
2775 let excess = MAX_REFERENCED_FILES * 4;
2779 for index in 0..excess {
2780 std::fs::write(root.join(format!("f{index:05}.csv")), b"").unwrap();
2781 }
2782 let source = Source::open(&root).unwrap();
2783 let (error, allocated) = measured_bytes(|| source.entry_names().unwrap_err());
2784 assert!(error.to_string().contains("entries"), "{error}");
2785 assert!(
2790 allocated < MAX_REFERENCED_FILES * 192,
2791 "the refused listing allocated {allocated} bytes"
2792 );
2793 std::fs::remove_dir_all(root).unwrap();
2794 }
2795
2796 #[cfg(unix)]
2797 #[test]
2798 fn listing_breadth_never_scales_open_descriptors() {
2799 fn open_descriptor_count() -> usize {
2800 let table = if cfg!(target_os = "macos") {
2801 "/dev/fd"
2802 } else {
2803 "/proc/self/fd"
2804 };
2805 std::fs::read_dir(table).unwrap().count()
2806 }
2807
2808 let _guard = process_resource_guard();
2809 let root = test_root("breadth");
2810 let breadth = 400usize;
2814 for index in 0..breadth {
2815 let sub = root.join(format!("s{index:03}"));
2816 std::fs::create_dir_all(&sub).unwrap();
2817 std::fs::write(sub.join("data.csv"), b"x").unwrap();
2818 }
2819 std::fs::create_dir_all(root.join("nested/a/b/c")).unwrap();
2820 std::fs::write(root.join("nested/a/b/c/deep.csv"), b"x").unwrap();
2821
2822 let mut original: libc::rlimit = unsafe { std::mem::zeroed() };
2828 assert_eq!(
2829 unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut original) },
2831 0
2832 );
2833 let lowered = libc::rlimit {
2834 rlim_cur: 256,
2835 rlim_max: original.rlim_max,
2836 };
2837 assert_eq!(
2839 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const lowered) },
2840 0
2841 );
2842
2843 let baseline = open_descriptor_count();
2844 let peak = std::sync::atomic::AtomicUsize::new(0);
2845 let stop = std::sync::atomic::AtomicBool::new(false);
2846 let names = std::thread::scope(|scope| {
2847 let sampler = scope.spawn(|| {
2848 while !stop.load(std::sync::atomic::Ordering::Relaxed) {
2849 let count = open_descriptor_count();
2850 peak.fetch_max(count, std::sync::atomic::Ordering::Relaxed);
2851 }
2852 });
2853 let source = Source::open(&root).unwrap();
2854 let names = source.entry_names().unwrap();
2855 stop.store(true, std::sync::atomic::Ordering::Relaxed);
2856 sampler.join().unwrap();
2857 drop(source);
2858 names
2859 });
2860 assert_eq!(
2862 unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const original) },
2863 0
2864 );
2865
2866 assert_eq!(names.len(), breadth + 1, "every file listed");
2867 let sampled_peak = peak.load(std::sync::atomic::Ordering::Relaxed);
2868 assert!(
2869 sampled_peak <= baseline + MAX_REFERENCED_DEPTH + 16,
2870 "the walk held {sampled_peak} descriptors over a baseline of {baseline}"
2871 );
2872 std::fs::remove_dir_all(root).unwrap();
2873 }
2874
2875 #[test]
2876 fn a_directory_listing_still_names_windows_reserved_spellings() {
2877 let root = test_root("reserved-listing");
2880 std::fs::create_dir_all(&root).unwrap();
2881 std::fs::write(root.join("aux.dss"), b"content").unwrap();
2882 let source = Source::open(&root).unwrap();
2883 let names = source.entry_names().unwrap();
2884 assert_eq!(names.len(), 1);
2885 assert_eq!(names[0].as_str(), "aux.dss");
2886 std::fs::remove_dir_all(root).unwrap();
2887 }
2888
2889 #[test]
2890 fn a_directory_listing_repeats_and_survives_acquisition() {
2891 let root = test_root("repeat-listing");
2896 std::fs::create_dir_all(&root).unwrap();
2897 std::fs::write(root.join("network.csv"), b"name\nseq\n").unwrap();
2898 std::fs::write(root.join("buses.csv"), b"name\nB1\n").unwrap();
2899 let source = Source::open(&root).unwrap();
2900 let first = source.entry_names().unwrap();
2901 assert_eq!(first.len(), 2);
2902 let name = ArtifactPath::new("network.csv").unwrap();
2903 source.buffer(&name).unwrap();
2904 let second = source.entry_names().unwrap();
2905 assert_eq!(first, second);
2906 std::fs::remove_dir_all(root).unwrap();
2907 }
2908
2909 #[test]
2910 fn concurrent_acquisition_retains_one_buffer_for_one_name() {
2911 let root = test_root("concurrent");
2912 std::fs::create_dir_all(&root).unwrap();
2913 std::fs::write(root.join("shared.csv"), b"shared").unwrap();
2914 let source = Source::open(&root).unwrap();
2915 let name = ArtifactPath::new("shared.csv").unwrap();
2916 let buffers: Vec<_> = std::thread::scope(|scope| {
2917 (0..8)
2918 .map(|_| {
2919 let source = source.clone();
2920 let name = name.clone();
2921 scope.spawn(move || source.buffer(&name).unwrap())
2922 })
2923 .collect::<Vec<_>>()
2924 .into_iter()
2925 .map(|handle| handle.join().unwrap())
2926 .collect()
2927 });
2928 let pointer = buffers[0].bytes().as_ptr();
2929 assert!(
2930 buffers
2931 .iter()
2932 .all(|buffer| buffer.bytes().as_ptr() == pointer)
2933 );
2934 assert_eq!(source.acquired_buffers().len(), 1);
2935 std::fs::remove_dir_all(root).unwrap();
2936 }
2937 #[cfg(unix)]
2938 #[test]
2939 fn a_refused_listing_never_shortens_the_next_one() {
2940 let root = test_root("refused-listing");
2944 std::fs::create_dir_all(&root).unwrap();
2945 for index in 0..(MAX_REFERENCED_FILES + 5) {
2946 std::fs::write(root.join(format!("f{index}.txt")), b"x").unwrap();
2947 }
2948 let source = Source::open(&root).unwrap();
2949 let first = source.entry_names().unwrap_err();
2950 let second = source.entry_names().unwrap_err();
2951 assert_eq!(
2952 first.diagnostics().first().map(|d| d.code().to_owned()),
2953 second.diagnostics().first().map(|d| d.code().to_owned()),
2954 "the refusal repeats rather than shrinking into a partial listing"
2955 );
2956 drop(source);
2957 let _ = std::fs::remove_dir_all(&root);
2958 }
2959}