Skip to main content

asar_rust/
filesystem.rs

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/// Represents a file entry in an ASAR archive.
11///
12/// Files are stored sequentially in the archive body. The `offset`
13/// field records the byte position within the data section.
14#[derive(Debug, Clone)]
15pub struct FileEntry {
16    /// Byte offset of this file's content within the archive data section.
17    pub offset: String,
18    /// Size of the file in bytes.
19    pub size: u64,
20    /// Whether the file has the executable permission bit set.
21    pub executable: bool,
22    /// Whether this file is stored unpacked alongside the archive.
23    pub unpacked: bool,
24    /// SHA256 integrity hashes for this file.
25    pub integrity: Option<FileIntegrity>,
26}
27
28/// Represents a directory entry in an ASAR archive.
29#[derive(Debug, Clone)]
30pub struct DirectoryEntry {
31    /// Child entries in this directory, keyed by name.
32    pub files: IndexMap<String, FilesystemEntry>,
33    /// Whether this directory is stored unpacked.
34    pub unpacked: bool,
35}
36
37/// Represents a symbolic link entry in an ASAR archive.
38#[derive(Debug, Clone)]
39pub struct LinkEntry {
40    /// The target path of the symbolic link.
41    pub link: String,
42    /// Whether this link is stored unpacked.
43    pub unpacked: bool,
44}
45
46/// An entry in the ASAR filesystem tree.
47#[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
390/// Options for listing files in an ASAR archive.
391///
392/// When `is_pack` is true, each entry includes a prefix
393/// indicating whether the file is packed or unpacked.
394pub struct ListOptions {
395    pub is_pack: bool,
396}