1use crate::disk::AsarError;
2use crate::integrity::FileIntegrity;
3use indexmap::IndexMap;
4use std::collections::HashSet;
5use std::path::{Component, Path, PathBuf};
6
7const SYMLINK_MAX_DEPTH: usize = 40;
8const UINT32_MAX: u64 = 4_294_967_295;
9
10#[derive(Debug, Clone)]
15pub struct FileEntry {
16 pub offset: String,
18 pub size: u64,
20 pub executable: bool,
22 pub unpacked: bool,
24 pub integrity: Option<FileIntegrity>,
26}
27
28#[derive(Debug, Clone)]
30pub struct DirectoryEntry {
31 pub files: IndexMap<String, FilesystemEntry>,
33 pub unpacked: bool,
35}
36
37#[derive(Debug, Clone)]
39pub struct LinkEntry {
40 pub link: String,
42 pub unpacked: bool,
44}
45
46#[derive(Debug, Clone)]
48pub enum FilesystemEntry {
49 File(FileEntry),
50 Directory(DirectoryEntry),
51 Link(LinkEntry),
52}
53
54impl FilesystemEntry {
55 pub fn is_directory(&self) -> bool {
56 matches!(self, FilesystemEntry::Directory(_))
57 }
58
59 pub fn is_file(&self) -> bool {
60 matches!(self, FilesystemEntry::File(_))
61 }
62
63 pub fn is_link(&self) -> bool {
64 matches!(self, FilesystemEntry::Link(_))
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct Filesystem {
70 src: PathBuf,
71 header: FilesystemEntry,
72 header_size: u32,
73 offset: u64,
74}
75
76impl Filesystem {
77 pub fn new(src: &Path) -> Self {
78 Filesystem {
79 src: src.to_path_buf(),
80 header: FilesystemEntry::Directory(DirectoryEntry {
81 files: IndexMap::new(),
82 unpacked: false,
83 }),
84 header_size: 0,
85 offset: 0,
86 }
87 }
88
89 pub fn root_path(&self) -> &Path {
90 &self.src
91 }
92
93 pub fn get_header(&self) -> &FilesystemEntry {
94 &self.header
95 }
96
97 pub fn header_size(&self) -> u32 {
98 self.header_size
99 }
100
101 pub fn set_header(&mut self, header: FilesystemEntry, header_size: u32) {
102 self.header = header;
103 self.header_size = header_size;
104 }
105
106 pub fn current_offset(&self) -> u64 {
107 self.offset
108 }
109
110 pub fn advance_offset(&mut self, size: u64) {
111 self.offset += size;
112 }
113
114 fn search_node_from_directory(
115 &mut self,
116 p: &Path,
117 ) -> Result<&mut FilesystemEntry, AsarError> {
118 let mut current = &mut self.header;
119 for component in p.components() {
120 if matches!(component, Component::RootDir | Component::CurDir) {
121 continue;
122 }
123 let name = component.as_os_str().to_str().unwrap_or("");
124 if name.is_empty() {
125 continue;
126 }
127 if let FilesystemEntry::Directory(dir) = current {
128 current = dir.files.entry(name.to_string()).or_insert_with(|| {
129 FilesystemEntry::Directory(DirectoryEntry {
130 files: IndexMap::new(),
131 unpacked: false,
132 })
133 });
134 } else {
135 return Err(AsarError::Other(format!(
136 "Unexpected directory state while traversing: {}",
137 p.display()
138 )));
139 }
140 }
141 Ok(current)
142 }
143
144 fn search_node_from_path(&mut self, p: &Path) -> Result<&mut FilesystemEntry, AsarError> {
145 let relative = p.strip_prefix(&self.src).unwrap_or(p);
146 if relative.as_os_str().is_empty() {
147 return Ok(&mut self.header);
148 }
149 let parent = relative.parent().unwrap_or(Path::new("."));
150 let name = relative.file_name().unwrap().to_str().unwrap_or("");
151
152 let node = self.search_node_from_directory(parent)?;
153 if let FilesystemEntry::Directory(dir) = node {
154 let entry = dir.files.entry(name.to_string()).or_insert_with(|| {
155 FilesystemEntry::File(FileEntry {
156 offset: "0".to_string(),
157 size: 0,
158 executable: false,
159 unpacked: false,
160 integrity: None,
161 })
162 });
163 Ok(entry)
164 } else {
165 Err(AsarError::Other(format!(
166 "Unexpected state while searching path: {}",
167 p.display()
168 )))
169 }
170 }
171
172 pub fn insert_directory(&mut self, p: &Path, should_unpack: bool) -> Result<(), AsarError> {
173 let node = self.search_node_from_path(p)?;
174 match node {
175 FilesystemEntry::Directory(dir) => {
176 if should_unpack {
177 dir.unpacked = true;
178 }
179 }
180 _ => {
181 *node = FilesystemEntry::Directory(DirectoryEntry {
182 files: IndexMap::new(),
183 unpacked: should_unpack,
184 });
185 }
186 }
187 Ok(())
188 }
189
190 pub fn insert_file(
191 &mut self,
192 p: &Path,
193 size: u64,
194 executable: bool,
195 should_unpack: bool,
196 integrity: Option<FileIntegrity>,
197 ) -> Result<(), AsarError> {
198 if !should_unpack && size > UINT32_MAX {
199 return Err(AsarError::FileTooLarge {
200 path: p.display().to_string(),
201 });
202 }
203
204 let offset = self.offset;
205 let parent = p.parent().unwrap_or(Path::new("."));
206 let relative_parent = parent.strip_prefix(&self.src).unwrap_or(parent);
207 let parent_node = self.search_node_from_directory(relative_parent)?;
208 let parent_unpacked = match parent_node {
209 FilesystemEntry::Directory(dir) => dir.unpacked,
210 _ => false,
211 };
212
213 let node = self.search_node_from_path(p)?;
214 match node {
215 FilesystemEntry::File(file) => {
216 if should_unpack || parent_unpacked {
217 file.size = size;
218 file.unpacked = true;
219 file.integrity = integrity;
220 } else {
221 file.size = size;
222 file.offset = offset.to_string();
223 file.executable = executable;
224 file.integrity = integrity;
225 self.offset = offset + size;
226 }
227 }
228 _ => {
229 return Err(AsarError::Other(format!(
230 "Expected file entry for: {}",
231 p.display()
232 )));
233 }
234 }
235 Ok(())
236 }
237
238 pub fn insert_link(&mut self, p: &Path, link: String, should_unpack: bool) -> Result<(), AsarError> {
239 let parent = p.parent().unwrap_or(Path::new("."));
240 let relative_parent = parent.strip_prefix(&self.src).unwrap_or(parent);
241 let parent_node = self.search_node_from_directory(relative_parent)?;
242 let parent_unpacked = match parent_node {
243 FilesystemEntry::Directory(dir) => dir.unpacked,
244 _ => false,
245 };
246
247 let node = self.search_node_from_path(p)?;
248 *node = FilesystemEntry::Link(LinkEntry {
249 link,
250 unpacked: should_unpack || parent_unpacked,
251 });
252 Ok(())
253 }
254
255 pub fn list_files(&self, options: Option<&ListOptions>) -> Vec<String> {
256 let mut files = Vec::with_capacity(64);
257 self.fill_files_from_metadata("/", &self.header, &mut files, options);
258 files
259 }
260
261 fn fill_files_from_metadata(
262 &self,
263 base_path: &str,
264 metadata: &FilesystemEntry,
265 files: &mut Vec<String>,
266 options: Option<&ListOptions>,
267 ) {
268 if let FilesystemEntry::Directory(dir) = metadata {
269 let mut keys: Vec<&String> = dir.files.keys().collect();
270 keys.sort_unstable();
271 for name in keys {
272 let child = &dir.files[name];
273 let full_path = if base_path == "/" {
274 format!("/{}", name)
275 } else {
276 format!("{}/{}", base_path, name)
277 };
278
279 let display = if let Some(opts) = options
280 && opts.is_pack
281 {
282 let state = match child {
283 FilesystemEntry::File(f) if f.unpacked => "unpack",
284 FilesystemEntry::Link(l) if l.unpacked => "unpack",
285 FilesystemEntry::Directory(d) if d.unpacked => "unpack",
286 _ => "pack ",
287 };
288 format!("{} : {}", state, full_path)
289 } else {
290 full_path.clone()
291 };
292 files.push(display);
293
294 self.fill_files_from_metadata(&full_path, child, files, options);
295 }
296 }
297 }
298
299 pub fn get_file(&self, p: &str, follow_links: bool) -> Result<&FilesystemEntry, AsarError> {
300 self.get_file_internal(p, follow_links, 0, &mut HashSet::new())
301 }
302
303 fn check_symlink(&self, p: &str, link_target: &str, depth: usize, visited: &mut HashSet<String>) -> Result<(), AsarError> {
304 if visited.contains(link_target) {
305 return Err(AsarError::CircularSymlink(format!("\"{}\": circular symlink detected at \"{}\"", p, link_target)));
306 }
307 if depth >= SYMLINK_MAX_DEPTH {
308 return Err(AsarError::SymlinkDepth);
309 }
310 visited.insert(link_target.to_string());
311 Ok(())
312 }
313
314 fn get_file_internal(
315 &self,
316 p: &str,
317 follow_links: bool,
318 depth: usize,
319 visited: &mut HashSet<String>,
320 ) -> Result<&FilesystemEntry, AsarError> {
321 let info = self.get_node_internal(p, follow_links, depth, visited)?;
322
323 if let FilesystemEntry::Link(link_entry) = info
324 && follow_links
325 {
326 let link = link_entry.link.clone();
327 self.check_symlink(p, &link, depth, visited)?;
328 return self.get_file_internal(&link, follow_links, depth + 1, visited);
329 }
330
331 Ok(info)
332 }
333
334 fn get_node_internal(
335 &self,
336 p: &str,
337 follow_links: bool,
338 depth: usize,
339 visited: &mut HashSet<String>,
340 ) -> Result<&FilesystemEntry, AsarError> {
341 let path = Path::new(p);
342 let parent = path.parent().unwrap_or(Path::new("."));
343 let name = path.file_name().unwrap_or_default().to_str().unwrap_or("");
344
345 let node = self.search_node_from_directory_readonly(parent);
346
347 if let FilesystemEntry::Link(link_entry) = node
348 && follow_links
349 {
350 let resolved = Path::new(&link_entry.link).join(name);
351 let resolved_str = resolved.to_str().unwrap_or("").to_string();
352 self.check_symlink(p, &resolved_str, depth, visited)?;
353 return self.get_node_internal(&resolved_str, follow_links, depth + 1, visited);
354 }
355
356 if name.is_empty() {
357 Ok(node)
358 } else if let FilesystemEntry::Directory(dir) = node {
359 dir.files
360 .get(name)
361 .ok_or_else(|| AsarError::NotFound(format!("\"{}\" was not found in this archive", p)))
362 } else {
363 Err(AsarError::NotFound(format!("\"{}\" was not found in this archive", p)))
364 }
365 }
366
367 fn search_node_from_directory_readonly(&self, p: &Path) -> &FilesystemEntry {
368 let mut current = &self.header;
369 for component in p.components() {
370 if matches!(component, Component::RootDir | Component::CurDir) {
371 continue;
372 }
373 let name = component.as_os_str().to_str().unwrap_or("");
374 if name.is_empty() {
375 continue;
376 }
377 if let FilesystemEntry::Directory(dir) = current {
378 match dir.files.get(name) {
379 Some(node) => current = node,
380 None => return current,
381 }
382 } else {
383 return current;
384 }
385 }
386 current
387 }
388}
389
390pub struct ListOptions {
395 pub is_pack: bool,
396}