Skip to main content

powerio_core/
source.rs

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
11/// Referenced files one source may acquire. Matches the OpenDSS include
12/// budget the distribution reader has enforced since 0.7.
13const MAX_REFERENCED_FILES: usize = 4_096;
14
15// One default for primary input and cumulative acquisition, including archives.
16const 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/// Both byte limits a filesystem source applies, read together so that an
24/// invalid setting is refused before any input is opened or read.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26struct SourceLimits {
27    primary: u64,
28    referenced: u64,
29}
30
31impl SourceLimits {
32    /// Read each limit through `lookup`, which maps a variable name to its
33    /// value. [`Source::open`] passes `std::env::var_os`; tests pass a fixed
34    /// table so they never touch the process environment.
35    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
102/// Deepest resolved or walked path beneath one acquisition root, in segments.
103const MAX_REFERENCED_DEPTH: usize = 64;
104
105const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
106
107/// Open stable identifier used to select a parser or writer.
108#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
109pub struct FormatId(Box<str>);
110
111impl FormatId {
112    /// Validate the shared C, Python, Julia, JSON, MCP, and Rust spelling.
113    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 of this buffer relative to the acquisition root, as the
166    /// segments a referenced name resolves against. Empty for memory buffers
167    /// and for files sitting directly in the root.
168    directory: Box<[Box<str>]>,
169}
170
171/// Immutable named bytes retained by a [`Source`].
172#[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    /// The exact retained bytes, including a UTF-8 byte order mark when the
201    /// input carried one. Same format writing echoes these bytes.
202    #[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    /// True when the retained bytes begin with a UTF-8 byte order mark.
213    #[must_use]
214    pub fn has_utf8_bom(&self) -> bool {
215        self.0.bytes.starts_with(&UTF8_BOM)
216    }
217
218    /// The bytes a parser decodes: the retained bytes with a leading UTF-8
219    /// byte order mark skipped. This is a subslice of the one retained buffer,
220    /// never a second decoded copy.
221    #[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    /// Directory of this buffer relative to the acquisition root, as path
232    /// segments. Empty for in-memory buffers and for files sitting directly
233    /// in the root. A parser that resolves referenced names itself joins
234    /// these onto the root to seed its resolution base.
235    pub fn directory_segments(&self) -> impl Iterator<Item = &str> {
236        self.0.directory.iter().map(AsRef::as_ref)
237    }
238}
239
240/// Files acquired beneath one pinned root directory.
241///
242/// The root is pinned as an open directory handle where the platform supports
243/// it, opened once on the first acquisition and reused for every later one, so
244/// the number of descriptors a process holds does not grow with the number of
245/// live sources that never acquire a referenced file. Referenced names are
246/// resolved lexically first and then walked one component at a time relative
247/// to that handle with symbolic links refused, so a path component replaced
248/// during acquisition cannot redirect the read outside the root. Acquisition
249/// is serialized by the one lock, which also makes the cache and the budget
250/// exact: a file is read and retained once, and concurrent requests for one
251/// name share the same buffer.
252#[derive(Debug)]
253struct FileAcquisition {
254    root_display: PathBuf,
255    max_bytes: u64,
256    /// True when the root was selected with [`Source::with_acquisition_root`]
257    /// rather than defaulted to the containing directory.
258    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    /// The one directory listing, cached on the first successful walk. Every
267    /// caller then reads the same immutable view, and a directory handle
268    /// whose stream position a platform shares across duplicated descriptors
269    /// cannot silently return an empty second listing.
270    listed: Option<Vec<crate::ArtifactPath>>,
271    files: usize,
272    bytes: u64,
273}
274
275impl AcquisitionState {
276    /// The pinned root handle, opened on first use with symbolic links at the
277    /// root refused and the directory confirmed on the opened descriptor.
278    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
303/// Opaque owner or provider of named immutable input buffers.
304///
305/// File acquisition policy belongs here rather than to parser entry points.
306/// The primary buffer's reserved identity. The leading slash is a spelling
307/// [`resolve_segments`] can never produce (a referenced name must be
308/// relative), so an acquired or named buffer's identity is disjoint from the
309/// primary's by construction.
310pub const PRIMARY_SOURCE_ID: &str = "/input";
311
312/// [`Source::open`] on a file retains the primary bytes and permits
313/// constrained acquisition of referenced files beneath the file's canonical
314/// containing directory; [`Source::with_acquisition_root`] widens that root at
315/// construction, and never from a parser. [`Source::from_memory`] grants no
316/// filesystem access; referenced content reaches an in-memory source only
317/// through [`Source::with_named_buffer`].
318#[derive(Clone)]
319pub struct Source {
320    name: Arc<str>,
321    provider: Arc<SourceProvider>,
322    declared_format: Option<FormatId>,
323}
324
325impl Source {
326    /// Acquire a file eagerly or a directory lazily.
327    ///
328    /// Primary files are limited to 1 GiB unless `POWERIO_MAX_PRIMARY_BYTES`
329    /// supplies a positive decimal byte count. The limit is checked before
330    /// allocation. Referenced files have a separate cumulative budget.
331    /// `POWERIO_MAX_REFERENCED_BYTES` overrides that budget (1 GiB by default).
332    /// Both variables are read and validated once, before the path is opened,
333    /// for file and directory sources alike; the budget is shared by every
334    /// clone of the source.
335    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        // Refuse an invalid setting before anything is opened or read.
351        let limits = SourceLimits::from_lookup(lookup)?;
352        let name: Arc<str> = path.to_string_lossy().into_owned().into();
353
354        // The open itself refuses a symbolic link at the named path, so there
355        // is no metadata inspection that a concurrent replacement could
356        // invalidate before the read; the file or directory decision reads
357        // the already opened handle.
358        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    /// Retain a caller-owned binary or text buffer. An `Arc<[u8]>` argument
419    /// is retained without copying; a `Vec<u8>` is copied once into the
420    /// shared buffer, since `Arc<[u8]>` needs its own allocation.
421    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    /// Retain a tree of caller-owned files without filesystem access.
449    ///
450    /// A primary path selects a file and resolves its references relative to
451    /// its containing directory. Without a primary path, the source is a
452    /// directory. Paths must be unique and portable; the tree shares the
453    /// referenced-file count, depth, and byte limits of filesystem sources.
454    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    /// Supply one referenced buffer to an in-memory source under the relative
511    /// name a format uses to refer to it. This is the only way referenced
512    /// content reaches a source built by [`Source::from_memory`]; such a source
513    /// never touches the filesystem.
514    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    /// Widen the acquisition root of a file source to a directory that
545    /// contains the file, selected while constructing the source. A parser
546    /// can never widen the root.
547    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    /// Select one parser explicitly while retaining the same source owner.
609    #[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    /// Byte limit for cumulative acquisition and expanded archive contents.
634    /// Filesystem sources capture `POWERIO_MAX_REFERENCED_BYTES` when opened;
635    /// memory sources read it when a parser requests expansion. Defaults to 1 GiB.
636    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    /// Borrow the sole primary buffer of a file or memory source.
645    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    /// Acquire and retain one file of a directory source by its root relative
663    /// name.
664    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    /// Acquire and retain one file beneath the acquisition root by its root
686    /// relative name, for a parser that resolves referenced names itself and
687    /// hands over the resolved result. An in-memory source consults the
688    /// buffers the caller supplied.
689    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    /// The canonical root selected with [`Source::with_acquisition_root`],
715    /// `None` when the root defaulted to the containing directory or the
716    /// source is not file backed. Read-only context for a parser's own
717    /// resolution and refusal wording; acquisition itself always goes
718    /// through this source.
719    #[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    /// Acquire and retain one file referenced by `referrer`, resolved against
730    /// the referring file's directory and confined beneath the acquisition
731    /// root. An in-memory source resolves the same name against the buffers
732    /// the caller supplied and never touches the filesystem.
733    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    /// Acquire a reference with an additional limit on this buffer's length.
742    /// The tighter of `max_bytes` and the source's remaining acquisition budget
743    /// bounds the read before allocation, through the same confined open as
744    /// [`Self::referenced_buffer`]. A caller can pass its remaining format budget
745    /// without changing the budget shared by other users of this source.
746    ///
747    /// Cached and caller-supplied buffers must also fit `max_bytes`, even though
748    /// they require no read. Zero permits only an empty buffer. Exceeding this
749    /// limit emits `READ.IO.BUFFER_BUDGET`; exceeding the source's tighter budget
750    /// retains `READ.IO.REFERENCE_BUDGET`. Refused reads acquire no buffer and do
751    /// not charge the source's budget.
752    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    /// The root relative file names of a directory source, in sorted order,
793    /// so a directory format can report files outside its profile without
794    /// touching the filesystem itself. Symbolic links are listed by name and
795    /// refused if acquired. The listing is bounded by the referenced file
796    /// budget; a directory holding more entries is refused.
797    #[allow(clippy::too_many_lines)] // one bounded walk, framed and budgeted in place
798    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                // The walk runs against the pinned root handle so a directory
811                // component swapped for a symbolic link mid-listing fails the
812                // descriptor walk rather than redirecting the listing outside
813                // the root. The walk is depth first over frames, one open
814                // handle per level: a child is opened from its parent's live
815                // handle, and a frame's handle closes when its subdirectories
816                // are exhausted, so the descriptors held at any moment are
817                // bounded by the depth bound, never by the entry budget or a
818                // directory's fan-out. Every budget bounds the work before it
819                // is incurred: entries stop being read the moment the
820                // remaining allowance is exhausted, and a prefix at the depth
821                // bound is refused before it is walked. The lock is held
822                // across the walk, like acquisition.
823                struct Frame<Handle> {
824                    prefix: Vec<String>,
825                    directory: Handle,
826                    /// Subdirectory names discovered and not yet descended.
827                    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                // Subdirectory names discovered across every frame and not
852                // yet listed; each still charges the entry budget.
853                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                    // Descend into the deepest frame's next subdirectory;
899                    // pop and drop a frame — closing its handle — as soon as
900                    // its subdirectories are exhausted, before any sibling
901                    // opens.
902                    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    /// Buffers already retained by this source, in deterministic name order.
927    #[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    /// Read and retain one file beneath the root. The lock is held across the
979    /// read so a file is read once, retained once, and charged once even under
980    /// concurrent requests. The remaining byte budget bounds the allocation
981    /// itself: it is computed before the read and handed to the reader, so a
982    /// file that would overrun the budget is refused before its bytes are
983    /// reserved.
984    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
1041/// True when `segment` is a single plain file name on every supported
1042/// platform: it reduces to exactly one normal path component equal to itself,
1043/// so it carries no separator of any platform, no drive or root prefix, no
1044/// NUL, and is neither `.` nor `..` nor empty. Joining such segments one at a
1045/// time onto a directory cannot reach outside that directory.
1046fn 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
1059/// Resolve a referenced name against the referring directory, lexically and
1060/// before any filesystem access. The name is split on both separators, `.` is
1061/// dropped, `..` pops within the root and is refused past it, a leading
1062/// separator or drive spelling is refused, and every remaining segment must be
1063/// a single plain file name on the platform under test. The result is the
1064/// exact component list the platform walk opens one at a time.
1065fn 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
1119/// An absolute referenced name is accepted only when it sits lexically beneath
1120/// the canonical root; the walk then reopens it component by component from
1121/// the pinned root handle. Returns `None` for a relative name. The segment
1122/// list is built from the platform's own path components, one segment per
1123/// directory component, each required to be a plain file name.
1124fn 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
1177/// A listing walk failure at one root relative directory: a symbolic link is
1178/// the acquisition refusal; anything else is an I/O failure.
1179fn 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
1194/// Read an already opened regular file completely. The handle was opened with
1195/// symbolic links refused, and the regular file check runs on the open
1196/// descriptor, so no path is consulted twice. The budget bounds the
1197/// allocation itself: a file whose declared length exceeds it is refused
1198/// before any bytes are reserved, and the reader is capped so a file that
1199/// grows past the bound during the read is refused rather than read.
1200fn 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    //! Descriptor-relative acquisition: the root directory is pinned by an
1266    //! open descriptor, and every referenced component is opened relative to
1267    //! it with `O_NOFOLLOW`, so replacing a component with a symbolic link
1268    //! during acquisition fails instead of redirecting the read outside the
1269    //! root. Every open also carries `O_NONBLOCK`, so the open call itself
1270    //! never waits on another process (a FIFO with no writer opens
1271    //! immediately and is then refused by the regular file check on the
1272    //! descriptor); the flag is cleared before any read.
1273
1274    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    /// Open the named path with symbolic links at the final component refused
1284    /// by the kernel and without the open itself blocking on another process.
1285    pub(super) fn open_no_follow(path: &Path) -> std::io::Result<File> {
1286        let path = c_string(path.as_os_str().as_bytes())?;
1287        // SAFETY: the pointer references a NUL-terminated buffer owned by
1288        // `path`, which outlives the call; the returned descriptor is owned
1289        // exclusively by the `File` constructed below.
1290        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        // SAFETY: `fd` is a freshly opened descriptor this function owns.
1300        let file = unsafe { File::from_raw_fd(fd) };
1301        clear_nonblock(&file)?;
1302        Ok(file)
1303    }
1304
1305    /// Open a root directory with a symbolic link at the final component
1306    /// refused, and the directory confirmed on the opened descriptor. Not
1307    /// `O_DIRECTORY | O_NOFOLLOW`: Darwin reports that combination on a
1308    /// symbolic link as `ENOTDIR`, hiding the refusal reason.
1309    pub(super) fn open_root(path: &Path) -> std::io::Result<RootHandle> {
1310        let path = c_string(path.as_os_str().as_bytes())?;
1311        // SAFETY: as in `open_no_follow`.
1312        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        // SAFETY: `fd` is a freshly opened descriptor this function owns.
1322        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                // `O_NOFOLLOW` alone, then a directory check on the opened
1337                // descriptor: Darwin reports `O_NOFOLLOW | O_DIRECTORY` on a
1338                // symbolic link as `ENOTDIR`, which would hide the refusal
1339                // reason, and checking the descriptor cannot race.
1340                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        /// A fresh open file description of the root, for a listing walk
1362        /// that opens each child directory relative to its parent. `dup`
1363        /// would share the directory stream position with the pinned root
1364        /// (and with every other walk), so a walk that stops early would
1365        /// leave the next one a shorter listing; `openat(fd, ".")` yields an
1366        /// independent description of the same directory with no path in
1367        /// between.
1368        pub(super) fn duplicate_handle(&self) -> std::io::Result<DirectoryHandle> {
1369            let segment = c_string(b".")?;
1370            // SAFETY: the root descriptor is live for the call and the
1371            // pointer references a NUL-terminated buffer owned by `segment`.
1372            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            // SAFETY: `fd` is a freshly opened descriptor this function owns.
1383            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            // SAFETY: `at` is a live descriptor owned by `self` or by the
1398            // caller's `directory` for the duration of the call, and the
1399            // pointer references a NUL-terminated buffer owned by `segment`.
1400            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            // SAFETY: `fd` is a freshly opened descriptor this function owns.
1405            Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1406        }
1407    }
1408
1409    /// An open directory the listing walk can read entries from and open
1410    /// children relative to.
1411    pub(super) type DirectoryHandle = OwnedFd;
1412
1413    /// Open one child directory of an already opened directory, with a
1414    /// symbolic link at the child refused and the directory confirmed on the
1415    /// opened descriptor.
1416    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        // SAFETY: `parent` is a live descriptor for the duration of the call,
1425        // and the pointer references the NUL-terminated buffer owned by
1426        // `segment`.
1427        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        // SAFETY: `fd` is a freshly opened descriptor this function owns.
1438        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    /// True when a listing failed because it reached the caller's entry
1452    /// allowance rather than a real I/O failure.
1453    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    /// Read the entries of an open directory, at most `max` of them: the
1458    /// bound is enforced inside the read loop, before the entry that would
1459    /// cross it is accepted, so the work and the memory of a listing are
1460    /// bounded by the allowance rather than by the directory's true entry
1461    /// count. Returns each UTF-8 entry name with whether it is a directory;
1462    /// symbolic links are listed by name and refused when acquired.
1463    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        // The stream takes ownership of a duplicate, so the caller's handle
1470        // stays usable for opening children.
1471        let raw = directory.try_clone()?.into_raw_fd();
1472        // SAFETY: `raw` is a live directory descriptor whose ownership
1473        // transfers to the returned stream; on failure it is closed here.
1474        let stream = unsafe { libc::fdopendir(raw) };
1475        if stream.is_null() {
1476            let error = std::io::Error::last_os_error();
1477            // SAFETY: `raw` is still owned by this function when `fdopendir`
1478            // fails.
1479            unsafe { libc::close(raw) };
1480            return Err(error);
1481        }
1482        let mut entries = Vec::new();
1483        loop {
1484            // SAFETY: `stream` is the live directory stream opened above.
1485            errno_clear();
1486            let entry = unsafe { libc::readdir(stream) };
1487            if entry.is_null() {
1488                let error = std::io::Error::last_os_error();
1489                // SAFETY: `stream` is the live directory stream opened above;
1490                // it is closed exactly once.
1491                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            // SAFETY: `entry` is valid until the next `readdir` on this
1498            // stream, and only the NUL-terminated name within the entry is
1499            // read: the raw pointer to the array's first element is followed
1500            // to its terminator, never the whole declared array.
1501            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                // SAFETY: as above; the stream is closed exactly once.
1512                unsafe { libc::closedir(stream) };
1513                return Err(entry_budget_error());
1514            }
1515            // SAFETY: as above; `d_type` is a plain byte field read by copy.
1516            let kind = unsafe { (*entry).d_type };
1517            let is_directory = match kind {
1518                libc::DT_DIR => true,
1519                libc::DT_UNKNOWN => {
1520                    // A filesystem without `d_type` support: ask the
1521                    // descriptor, without following a symbolic link.
1522                    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                    // SAFETY: `stream` is live, so `dirfd` is a live
1526                    // descriptor; the name pointer references the
1527                    // NUL-terminated buffer owned by `segment`.
1528                    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    /// Clear `O_NONBLOCK` on an opened descriptor before it is read.
1550    fn clear_nonblock(file: &File) -> std::io::Result<()> {
1551        let fd = file.as_raw_fd();
1552        // SAFETY: `fd` is a live descriptor owned by `file` for the duration
1553        // of both calls.
1554        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1555        if flags < 0 {
1556            return Err(std::io::Error::last_os_error());
1557        }
1558        // SAFETY: as above.
1559        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        // Opening a directory read-only succeeds on Linux and macOS, so the
1572        // usual route is the metadata check on the opened handle; this covers
1573        // a platform whose open refuses directories outright.
1574        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    //! Windows and other platforms have no `openat`. The walk opens every
1613    //! intermediate directory into a handle held for the remainder of the
1614    //! walk, with reparse points refused on the opened handle and the share
1615    //! mode excluding delete, so a held component can be neither replaced by
1616    //! a link nor renamed away while a child is opened beneath it — the same
1617    //! invariant the Unix descriptor walk enforces.
1618
1619    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        // Verified at open; each walk re-pins the root for its own duration,
1634        // so the source's lifetime holds no lock that would block legitimate
1635        // tree changes between walks.
1636        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            // Every handle from the root down stays alive until the final
1648            // open: the next component is opened only while every ancestor
1649            // is still held, and all release when the walk returns.
1650            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        /// The root as a listing handle: the pinned handle plus the verified
1663        /// path each child extends.
1664        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    /// A verified directory the listing walk extends one plain component at
1674    /// a time, holding its own pinned handle; the walk keeps a frame per
1675    /// level, so every ancestor of an open frame stays held.
1676    #[derive(Debug)]
1677    pub(super) struct DirectoryHandle {
1678        path: PathBuf,
1679        _handle: File,
1680    }
1681
1682    /// Extend the walk by one child directory, opened while the parent's
1683    /// handle is held, with a reparse point at the child refused on the
1684    /// opened handle.
1685    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    /// True when a listing failed because it reached the caller's entry
1705    /// allowance rather than a real I/O failure.
1706    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    /// Read the entries of one held directory, at most `max` of them: the
1711    /// bound is enforced inside the read loop, before the entry that would
1712    /// cross it is accepted. The held handle keeps the listed path the
1713    /// verified directory for the duration.
1714    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    /// Refuse a segment that is not a single plain file name before it is
1734    /// joined onto the walked path.
1735    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    /// Open one directory into a held handle with the reparse point refused
1744    /// on the handle itself. On Windows the open uses backup semantics (a
1745    /// directory needs it), keeps `FILE_SHARE_DELETE` out of the share mode
1746    /// so the held component cannot be renamed or deleted, and refuses a
1747    /// handle whose attributes carry the reparse flag.
1748    #[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
1828/// The name a source built from content in memory carries when the caller
1829/// supplies none. Format detection and diagnostics read it, so content whose
1830/// name carries meaning goes through [`Source::from_memory`] instead.
1831pub const MEMORY_SOURCE_NAME: &str = "<memory>";
1832
1833/// One input a read operation can acquire.
1834///
1835/// A string or path names a file or directory to open. A byte buffer is
1836/// content already in memory, named [`MEMORY_SOURCE_NAME`]. A [`Source`]
1837/// passes through, so a source carrying named buffers or a widened
1838/// acquisition root reaches the same operations as a file name.
1839///
1840/// A caller's own input type reaches the same operations by implementing it.
1841/// It carries this one method and gains no further required method: options
1842/// belong to the operation, not to the input.
1843pub trait IntoSource {
1844    /// Acquire the input.
1845    ///
1846    /// # Errors
1847    /// The file or directory cannot be acquired, or the in-memory name is
1848    /// invalid.
1849    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
1864/// Names a file or directory to open.
1865macro_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
1879/// Content already in memory.
1880macro_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    /// Serializes the tests that measure process wide resources (open
1913    /// descriptor counts, descriptor limits), so a measurement compares
1914    /// against a baseline taken under the same guard rather than a free
1915    /// running count other tests move.
1916    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    /// Byte counting global allocator, scoped to the one measuring thread so
1925    /// parallel tests never pollute a measurement.
1926    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    // SAFETY: delegates every operation to the system allocator; the counter
1944    // is a side effect on the measuring thread only.
1945    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        // The parser slice points into the one retained buffer.
2019        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        // Referrer-relative resolution: from inside `sub/`, a sibling name
2043        // resolves beneath `sub/` and `..` climbs within the supplied names.
2044        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        // Names resolve against the referring file: from the feeder, a bare
2074        // sibling name lands in `sub/`.
2075        let coords = source.referenced_buffer(&feeder, "coords.csv").unwrap();
2076        assert_eq!(coords.bytes(), b"coords");
2077        // And `..` climbs back toward the root but never past it.
2078        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        // The same file is retained once.
2087        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        // Confined by default: the sibling directory is outside.
2104        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        // The wider root is selected while constructing the source.
2113        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        // A root that does not contain the case file is refused.
2128        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        // A symlinked intermediate directory is refused by the component walk.
2173        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        // SAFETY: the pointer references the NUL-terminated buffer owned by
2199        // `c_path`, which outlives the call.
2200        assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
2201
2202        // No process is attached to the pipe, so a blocking open would hang;
2203        // each operation is driven from a worker with a bounded wait so a
2204        // regression fails the test rather than hanging it.
2205        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        // A fixed table stands in for the process environment, so this test
2300        // never races another test that reads or sets the variables.
2301        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        // A directory open used to ignore the primary variable entirely.
2333        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        // A primary over its limit would fail with PRIMARY_BUDGET once read;
2338        // INVALID_LIMIT shows that both settings are checked first.
2339        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        // A sparse file whose declared length far exceeds the acquisition
2389        // budget; no bytes are written, so the refusal must come from the
2390        // declared length, before any reservation.
2391        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        // The refusal happens before the reserve: the bytes allocated on this
2398        // thread during the refused acquisition are a tiny fraction of the
2399        // declared length. The measurement is thread scoped, so it fails when
2400        // the pre-reserve refusal is removed and never passes by accident.
2401        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        // A failed limit must not replace the cached buffer or tighten the
2479        // source's own budget, shared by the ordinary acquisition API.
2480        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        // A cached buffer needs no additional acquisition allowance.
2491        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                // Either the real subtree lists, or the walk fails; a name
2561                // that exists only outside the root never appears.
2562                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        // A climb spelled with the platform's alternate separator is the same
2588        // climb; a leading separator and a drive spelling are refused.
2589        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        // Ordinary relative names keep resolving.
2601        let feeder = source
2602            .referenced_buffer(&primary, "sub/feeder.dss")
2603            .unwrap();
2604        assert_eq!(feeder.bytes(), b"feeder");
2605
2606        // An absolute in-root name resolves to one segment per directory
2607        // component: it lands on the same cached buffer the relative name
2608        // produced, proving the per component walk ran on the same key. The
2609        // acquisition root is the canonical containing directory, so the
2610        // absolute spelling is canonical too.
2611        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        // No acquisition returned bytes from outside the root.
2617        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        // Sources still acquire afterwards, and one source's two acquisitions
2658        // resolve through the same pinned root to one buffer. Acquisition
2659        // pins one descriptor per source, so this runs on a subset that stays
2660        // under the default descriptor limit.
2661        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        // Guard on the entry-name read: names are read to their terminator,
2674        // whatever length the entry actually occupies.
2675        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        // The unwind below holds one descriptor per level; serialize with the
2701        // other descriptor sensitive tests so their measurements stay exact.
2702        let _guard = process_resource_guard();
2703
2704        let root = test_root("deep-chain");
2705        std::fs::create_dir_all(&root).unwrap();
2706        // Build the chain by relative creation from inside each level, so the
2707        // tree reaches past any absolute path length limit.
2708        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            // SAFETY: `level` owns a live directory descriptor for both
2712            // calls, and the pointer references the NUL-terminated buffer
2713            // owned by `name`.
2714            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        // The chain is deeper than remove_dir_all's own recursion budget on
2741        // some platforms; unwind it level by level with the same descriptors.
2742        let mut fds = vec![std::fs::File::open(&root).unwrap()];
2743        loop {
2744            let last = fds.last().unwrap();
2745            // SAFETY: as above.
2746            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            // SAFETY: `fd` is a freshly opened descriptor.
2757            fds.push(unsafe { std::fs::File::from_raw_fd(fd) });
2758        }
2759        while fds.len() > 1 {
2760            let parent = &fds[fds.len() - 2];
2761            // SAFETY: as above; AT_REMOVEDIR removes the empty directory.
2762            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        // Four times the budget: an unbounded read would materialize four
2776        // times the names the allowance admits, which the thread scoped
2777        // allocation measurement separates decisively from the bounded read.
2778        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        // The listing stopped reading at the allowance: the bytes allocated
2786        // on this thread are bounded by the entry budget, never by the
2787        // directory's true entry count. Removing the in-loop bound reads all
2788        // `excess` names and fails this assertion.
2789        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        // Far more immediate subdirectories than the lowered descriptor
2811        // limit, each holding one file, plus one nested chain, so the walk
2812        // proves its held descriptors follow depth rather than breadth.
2813        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        // Lower the descriptor soft limit for the duration, so a walk whose
2823        // descriptor use scales with breadth fails here rather than passing
2824        // on a machine with a raised limit.
2825        // SAFETY: `getrlimit` fills the zeroed out-parameter; the lowered
2826        // limit is restored below.
2827        let mut original: libc::rlimit = unsafe { std::mem::zeroed() };
2828        assert_eq!(
2829            // SAFETY: as above.
2830            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        // SAFETY: lowering the soft limit for this process; restored below.
2838        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        // SAFETY: restoring the limit read above.
2861        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        // The output predicate refusing reserved device stems must not narrow
2878        // what a source directory can list.
2879        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        // The walk runs once and its result is the source's one immutable
2892        // listing: a second call between or after buffer acquisitions
2893        // returns the same names rather than an empty second walk from a
2894        // directory stream a platform shares across duplicated descriptors.
2895        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        // A walk that stops early (the entry budget) must not leave the next
2941        // walk a partially consumed directory stream: every retry reads the
2942        // whole directory again and refuses the same way.
2943        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}