winbrew_core\fs\archive/
context.rs1use std::collections::HashMap;
2use std::fs;
3use std::io::ErrorKind;
4use std::marker::PhantomData;
5use std::path::{Path, PathBuf};
6
7use crate::fs::{FsError, Result};
8
9use super::cleanup::ExtractionCleanup;
10use super::limits::ExtractionLimits;
11use super::platform::PlatformAdapter;
12use super::types::{CachedPath, PathInfo};
13
14pub(crate) struct ExtractionContext<P: PlatformAdapter> {
15 cached_paths: HashMap<PathBuf, CachedPath>,
16 cleanup: ExtractionCleanup,
17 limits: ExtractionLimits,
18 current_total_size: u64,
19 current_file_count: usize,
20 platform: PhantomData<P>,
21}
22
23impl<P: PlatformAdapter> ExtractionContext<P> {
24 pub(crate) fn new(limits: ExtractionLimits) -> Self {
25 Self {
26 cached_paths: HashMap::new(),
27 cleanup: ExtractionCleanup::new(),
28 limits,
29 current_total_size: 0,
30 current_file_count: 0,
31 platform: PhantomData,
32 }
33 }
34
35 pub(crate) fn commit(self) {
36 self.cleanup.commit();
37 }
38
39 pub(crate) fn validate_target(&mut self, path: &Path, destination_dir: &Path) -> Result<()> {
40 let mut current = Some(path);
41 let mut is_final_component = true;
42
43 while let Some(candidate) = current {
44 if candidate == destination_dir {
45 break;
46 }
47
48 match self.inspect_cached(candidate)? {
49 CachedPath::Present(info) => {
50 if info.is_reparse_point {
51 return Err(FsError::reparse_point(candidate));
52 }
53
54 if is_final_component && !info.is_directory && info.hard_link_count > 1 {
55 return Err(FsError::hardlinked_target(candidate));
56 }
57 }
58 CachedPath::Missing => {}
59 }
60
61 is_final_component = false;
62 current = candidate.parent();
63 }
64
65 Ok(())
66 }
67
68 pub(crate) fn ensure_directory_tree(&mut self, path: &Path) -> Result<()> {
69 let mut missing_directories = Vec::new();
70 let mut current = Some(path);
71
72 while let Some(candidate) = current {
73 match self.inspect_cached(candidate)? {
74 CachedPath::Present(info) => {
75 if !info.is_directory {
76 return Err(FsError::path_not_directory(candidate));
77 }
78
79 break;
80 }
81 CachedPath::Missing => {
82 missing_directories.push(candidate.to_path_buf());
83 current = candidate.parent();
84 }
85 }
86 }
87
88 if let Some(deepest_missing) = missing_directories.first() {
89 fs::create_dir_all(deepest_missing)
90 .map_err(|err| FsError::create_directory(deepest_missing, err))?;
91
92 for directory in missing_directories.iter().rev() {
93 self.record_directory(directory);
94 }
95 }
96
97 Ok(())
98 }
99
100 pub(crate) fn check_limits(
101 &mut self,
102 path: &Path,
103 entry_size: u64,
104 compressed_size: u64,
105 ) -> Result<()> {
106 let path_depth = path.components().count();
107
108 if path_depth > self.limits.max_path_depth {
109 return Err(FsError::path_too_deep(
110 path,
111 path_depth,
112 self.limits.max_path_depth,
113 ));
114 }
115
116 if entry_size > 0
117 && (compressed_size == 0
118 || entry_size > compressed_size.saturating_mul(self.limits.max_compression_ratio))
119 {
120 return Err(FsError::suspicious_compression_ratio(
121 path,
122 entry_size,
123 compressed_size,
124 self.limits.max_compression_ratio,
125 ));
126 }
127
128 let new_total_size = self
129 .current_total_size
130 .checked_add(entry_size)
131 .filter(|&size| size <= self.limits.max_total_size)
132 .ok_or_else(|| {
133 FsError::quota_exceeded(
134 self.limits.max_total_size,
135 self.current_total_size,
136 entry_size,
137 )
138 })?;
139
140 let new_file_count = self
141 .current_file_count
142 .checked_add(1)
143 .filter(|&count| count <= self.limits.max_file_count)
144 .ok_or_else(|| {
145 FsError::file_count_exceeded(self.limits.max_file_count, self.current_file_count)
146 })?;
147
148 self.current_total_size = new_total_size;
149 self.current_file_count = new_file_count;
150 Ok(())
151 }
152
153 pub(crate) fn record_file(&mut self, path: &Path) {
154 self.cached_paths.insert(
155 path.to_path_buf(),
156 CachedPath::Present(PathInfo {
157 is_directory: false,
158 is_reparse_point: false,
159 hard_link_count: 1,
160 }),
161 );
162 self.cleanup.record_file(path.to_path_buf());
163 }
164
165 fn record_directory(&mut self, path: &Path) {
166 self.cached_paths.insert(
167 path.to_path_buf(),
168 CachedPath::Present(PathInfo {
169 is_directory: true,
170 is_reparse_point: false,
171 hard_link_count: 1,
172 }),
173 );
174 self.cleanup.record_directory(path.to_path_buf());
175 }
176
177 fn inspect_cached(&mut self, path: &Path) -> Result<CachedPath> {
178 if let Some(cached) = self.cached_paths.get(path) {
179 return Ok(*cached);
180 }
181
182 let state = match P::inspect_path(path) {
183 Ok(info) => CachedPath::Present(info),
184 Err(err) if err.kind() == ErrorKind::NotFound => CachedPath::Missing,
185 Err(err) => return Err(FsError::inspect(path, err)),
186 };
187
188 self.cached_paths.insert(path.to_path_buf(), state);
189 Ok(state)
190 }
191}