scrollcase_consumer/
filesystem.rs1use std::collections::BTreeSet;
8use std::path::{Path, PathBuf};
9
10use sha2::{Digest, Sha256};
11
12use crate::contract::links::{find_entry_through_link, find_unresolvable_link, EntryKind, PayloadEntry};
13use crate::error::{fail, Error, Result};
14
15const IGNORED_NAMES: &[&str] = &["__pycache__", ".DS_Store"];
17
18pub fn sha256_file(path: &Path) -> Result<String> {
24 use std::io::Read as _;
25 let mut file = std::fs::File::open(path)
26 .map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
27 let mut hasher = Sha256::new();
28 let mut buffer = vec![0u8; 128 * 1024];
29 loop {
30 let read = file
31 .read(&mut buffer)
32 .map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
33 if read == 0 {
34 break;
35 }
36 hasher.update(&buffer[..read]);
37 }
38 let digest = hasher.finalize();
39 let mut hex = String::with_capacity(digest.len() * 2);
40 for byte in digest {
41 use std::fmt::Write as _;
42 let _ = write!(hex, "{byte:02x}");
43 }
44 Ok(hex)
45}
46
47pub fn collect_entries(root: &Path) -> Result<Vec<PayloadEntry>> {
57 let mut entries = Vec::new();
58 collect_into(root, root, &mut entries)?;
59 entries.sort_by(|left, right| left.path.cmp(&right.path));
60 Ok(entries)
61}
62
63fn collect_into(root: &Path, current: &Path, entries: &mut Vec<PayloadEntry>) -> Result<()> {
64 let mut names: Vec<PathBuf> = std::fs::read_dir(current)
65 .map_err(|error| Error::new(format!("cannot read {}: {error}", current.display())))?
66 .filter_map(std::result::Result::ok)
67 .map(|entry| entry.path())
68 .collect();
69 names.sort();
70
71 for path in names {
72 let name = path
73 .file_name()
74 .and_then(std::ffi::OsStr::to_str)
75 .unwrap_or_default()
76 .to_string();
77 #[allow(clippy::case_sensitive_file_extension_comparisons)]
81 if IGNORED_NAMES.contains(&name.as_str()) || name.ends_with(".pyc") {
82 continue;
83 }
84 let relative = relative_forward_slash(root, &path)?;
85 let metadata = std::fs::symlink_metadata(&path)
86 .map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
87 if metadata.is_symlink() {
90 let target = std::fs::read_link(&path)
91 .map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
92 entries.push(PayloadEntry::link(
93 relative,
94 target.to_string_lossy().replace('\\', "/"),
95 ));
96 } else if metadata.is_dir() {
97 collect_into(root, &path, entries)?;
98 } else if metadata.is_file() {
99 entries.push(PayloadEntry::file(relative));
100 } else {
101 fail!("box special entries are not allowed: {relative}");
102 }
103 }
104 Ok(())
105}
106
107fn relative_forward_slash(root: &Path, path: &Path) -> Result<String> {
108 let Ok(relative) = path.strip_prefix(root) else {
109 fail!("Unsafe relative path: {}", path.display());
110 };
111 Ok(relative
112 .components()
113 .map(|component| component.as_os_str().to_string_lossy())
114 .collect::<Vec<_>>()
115 .join("/"))
116}
117
118pub fn collect_files(root: &Path) -> Result<BTreeSet<String>> {
129 Ok(collect_entries(root)?
130 .into_iter()
131 .filter(|entry| entry.kind != EntryKind::Directory)
132 .map(|entry| entry.path)
133 .collect())
134}
135
136pub fn payload_size(root: &Path) -> Result<u64> {
146 let mut total = 0u64;
147 for entry in collect_entries(root)? {
148 if entry.kind == EntryKind::Directory {
149 continue;
150 }
151 let path = crate::path::join_relative(root, &entry.path);
152 let metadata = std::fs::symlink_metadata(&path)
153 .map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
154 total = total.saturating_add(metadata.len());
155 }
156 Ok(total)
157}
158
159pub fn validate_extracted_tree(root: &Path, allow_links: bool) -> Result<()> {
169 let entries = collect_entries(root)?;
170 if !allow_links {
171 if let Some(link) = entries.iter().find(|entry| entry.kind == EntryKind::Link) {
172 fail!("Archive links and special entries are not allowed: {}", link.path);
173 }
174 return Ok(());
175 }
176 if let Some(path) = find_unresolvable_link(&entries) {
177 fail!("Extracted link does not resolve to a file inside the payload: {path}");
178 }
179 if let Some(path) = find_entry_through_link(&entries) {
180 fail!("Extracted entry would be written through a link: {path}");
181 }
182 Ok(())
183}