Skip to main content

archive_trait/
extract.rs

1//! Format-neutral archive extraction.
2//!
3//! [`ExtractPolicy`] configures common path, overwrite, and link behavior.
4//! Filesystem mutation is capability-relative and confined to the destination.
5
6mod path;
7mod root;
8
9use std::path::Path;
10
11use self::{path::decode_member, root::ExtractionRoot};
12use super::*;
13
14/// Controls behavior shared by [`Archive::extract_in`] implementations.
15///
16/// See each configuration API for its default.
17#[derive(Clone, Copy, Debug)]
18pub struct ExtractPolicy {
19    pub(crate) link_policy: LinkPolicy,
20    pub(crate) allow_overwrites: bool,
21    pub(crate) name_validation: crate::name::NameValidation,
22}
23
24/// Controls how symbolic- and hard-link members are extracted.
25///
26/// By default, symbolic links are preserved as native links, including links
27/// to missing targets. Hard links and ambient symbolic-link targets require
28/// explicit opt-in.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct LinkPolicy {
31    pub(crate) symlink_policy: SymlinkPolicy,
32    pub(crate) allow_hard_links: bool,
33    pub(crate) allow_ambient_targets: bool,
34    pub(crate) allow_missing_targets: bool,
35}
36
37/// Controls how symbolic-link members are handled during extraction.
38#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
39pub enum SymlinkPolicy {
40    /// Preserve symbolic-link members as native filesystem links.
41    #[default]
42    Preserve,
43    /// Ignore symbolic-link members without changing the filesystem.
44    Skip,
45    /// Reject archives containing symbolic-link members.
46    Reject,
47}
48
49impl Default for ExtractPolicy {
50    fn default() -> Self {
51        Self {
52            link_policy: LinkPolicy::default(),
53            allow_overwrites: true,
54            name_validation: crate::name::NameValidation::Default,
55        }
56    }
57}
58
59impl Default for LinkPolicy {
60    fn default() -> Self {
61        Self {
62            symlink_policy: SymlinkPolicy::default(),
63            allow_hard_links: false,
64            allow_ambient_targets: false,
65            allow_missing_targets: true,
66        }
67    }
68}
69
70impl ExtractPolicy {
71    /// Configures symbolic- and hard-link extraction behavior.
72    pub fn link_policy(mut self, policy: LinkPolicy) -> Self {
73        self.link_policy = policy;
74        self
75    }
76
77    /// Configures whether archive members may replace existing entries.
78    ///
79    /// Overwrites are **allowed by default**. Replacement never follows
80    /// symbolic links or recursively removes non-empty directories. Real
81    /// directories are always reused, including when overwrites are disabled.
82    pub fn allow_overwrites(mut self, allow: bool) -> Self {
83        self.allow_overwrites = allow;
84        self
85    }
86
87    /// Configures validation for member names and link targets.
88    ///
89    /// Passing [`None`] disables configurable name validation. UTF-8 and
90    /// extraction containment requirements still apply.
91    pub fn name_validator(mut self, validator: Option<NameValidator>) -> Self {
92        self.name_validation = crate::name::NameValidation::from_validator(validator);
93        self
94    }
95
96    fn check_name<E>(
97        self,
98        position: u64,
99        context: &'static str,
100        value: &str,
101    ) -> Result<(), ExtractError<E>> {
102        if !self.name_validation.accepts(value) {
103            return Err(ExtractError::policy_violation(
104                position,
105                ExtractPolicyViolation::NameRejected {
106                    context,
107                    value: value.to_owned(),
108                },
109            ));
110        }
111        Ok(())
112    }
113}
114
115impl LinkPolicy {
116    /// Configures how symbolic-link members are handled during extraction.
117    ///
118    /// Symbolic links are preserved by default. Platforms without native
119    /// symbolic-link creation require [`SymlinkPolicy::Skip`] or
120    /// [`SymlinkPolicy::Reject`].
121    pub fn symlink_policy(mut self, policy: SymlinkPolicy) -> Self {
122        self.symlink_policy = policy;
123        self
124    }
125
126    /// Configures whether hard-link members may be extracted.
127    ///
128    /// Hard links are **forbidden by default** because they are uncommon,
129    /// difficult to extract consistently, and prone to implementation
130    /// differentials. Enable them only for trusted archives.
131    pub fn allow_hard_links(mut self, allow: bool) -> Self {
132        self.allow_hard_links = allow;
133        self
134    }
135
136    /// Configures whether pre-existing symbolic-link targets may be used.
137    ///
138    /// Existing symbolic links are followed only when capability-relative
139    /// resolution remains beneath the extraction root. Ambient targets are
140    /// **forbidden by default**. This does not affect hard-link validation.
141    pub fn allow_ambient_targets(mut self, allow: bool) -> Self {
142        self.allow_ambient_targets = allow;
143        self
144    }
145
146    /// Configures whether symbolic links to missing targets may be extracted.
147    ///
148    /// Missing symbolic-link targets are **allowed by default**. This does not
149    /// affect hard-link validation.
150    pub fn allow_missing_targets(mut self, allow: bool) -> Self {
151        self.allow_missing_targets = allow;
152        self
153    }
154}
155
156/// Extracts a member stream into `destination` under the shared extraction policy.
157pub(crate) async fn extract<A: Archive>(
158    mut members: Members<A>,
159    destination: &Path,
160    policy: ExtractPolicy,
161) -> Result<(), ExtractError<A::Error>> {
162    let mut root = ExtractionRoot::<A::Error>::open(destination, policy.allow_overwrites).await?;
163    // Scratch space reused for each payload read and streamed directly for large files.
164    let mut chunk_buffer = Vec::new();
165    // Complete small-file contents, buffered so payload validation precedes file creation.
166    let mut buffered_payload = Vec::new();
167    let result: Result<(), ExtractError<A::Error>> = async {
168        while let Some(member) = members.next().await.map_err(ExtractError::Archive)? {
169            check_member_policy(&member, policy)?;
170            let decoded = decode_member(&member, policy)?;
171            match member {
172                Member::File {
173                    size,
174                    executable,
175                    payload,
176                    ..
177                } => {
178                    root.extract_file(
179                        &decoded.path,
180                        size,
181                        executable,
182                        payload,
183                        &mut chunk_buffer,
184                        &mut buffered_payload,
185                    )
186                    .await?;
187                }
188                Member::Directory { .. } => root.extract_directory(&decoded.path).await?,
189                Member::SymbolicLink { .. } => {
190                    if policy.link_policy.symlink_policy == SymlinkPolicy::Preserve {
191                        root.reserve_symlink(&decoded).await?;
192                    }
193                }
194                Member::HardLink { size, payload, .. } => {
195                    root.extract_hard_link(&decoded, size, payload, &mut chunk_buffer)
196                        .await?;
197                }
198                Member::Special { kind, .. } => {
199                    return Err(ExtractError::UnsupportedMember {
200                        position: decoded.position,
201                        path: decoded.path.to_path_buf(),
202                        kind,
203                    });
204                }
205            }
206        }
207        Ok(())
208    }
209    .await;
210    // Commit earlier validated files before reporting a later member error.
211    root.flush_buffered_files().await?;
212    result?;
213    root.finalize_symlinks(policy.link_policy).await
214}
215
216fn check_member_policy<E, P>(
217    member: &Member<P>,
218    policy: ExtractPolicy,
219) -> Result<(), ExtractError<E>> {
220    let position = member.metadata().position;
221    match member {
222        Member::SymbolicLink { .. } => {
223            let violation = match policy.link_policy.symlink_policy {
224                SymlinkPolicy::Reject => Some(ExtractPolicyViolation::SymbolicLink),
225                #[cfg(not(unix))]
226                SymlinkPolicy::Preserve => {
227                    Some(ExtractPolicyViolation::NativeSymlinkCreationUnsupported)
228                }
229                _ => None,
230            };
231            if let Some(violation) = violation {
232                return Err(ExtractError::policy_violation(position, violation));
233            }
234        }
235        Member::HardLink { .. } if !policy.link_policy.allow_hard_links => {
236            return Err(ExtractError::policy_violation(
237                position,
238                ExtractPolicyViolation::HardLink,
239            ));
240        }
241        _ => {}
242    }
243    Ok(())
244}