forensic_mount/
fs_sevenz.rs1#![forbid(unsafe_code)]
2
3use crate::archive_tree::ArchiveTree;
14use crate::{not_supported, ForensicFs, FsDirEntry, FsError, FsMetadata, FsResult, FsTimestamp};
15use sevenz_rust::{Password, SevenZReader};
16use std::io::{Read, Seek, SeekFrom};
17
18const FILETIME_TO_UNIX_SECS: i64 = 11_644_473_600;
21
22pub struct SevenZForensicFs {
24 tree: ArchiveTree,
25 data: Vec<Vec<u8>>,
27}
28
29impl SevenZForensicFs {
30 pub fn new<R: Read + Seek>(mut source: R) -> Result<Self, FsError> {
36 let len = source.seek(SeekFrom::End(0)).map_err(FsError::Io)?;
37 source.seek(SeekFrom::Start(0)).map_err(FsError::Io)?;
38 let mut reader = SevenZReader::new(source, len, Password::empty())
39 .map_err(|e| FsError::Corrupt(format!("not a 7z: {e}")))?;
40
41 let mut tree = ArchiveTree::new();
42 let mut data: Vec<Vec<u8>> = Vec::new();
43 reader
44 .for_each_entries(|entry, rdr| {
45 let name = entry.name().to_string();
46 let mtime = filetime_to_ts(entry.last_modified_date().to_raw());
47 if entry.is_directory() {
48 tree.insert(&name, true, 0, mtime, None);
49 } else {
50 let mut buf = Vec::new();
51 rdr.read_to_end(&mut buf)?;
52 let id = data.len();
53 if tree
54 .insert(&name, false, buf.len() as u64, mtime, Some(id))
55 .is_some()
56 {
57 data.push(buf);
58 }
59 }
60 Ok(true)
61 })
62 .map_err(|e| FsError::Corrupt(format!("7z: {e}")))?;
63
64 Ok(Self { tree, data })
65 }
66}
67
68fn filetime_to_ts(raw: u64) -> FsTimestamp {
71 if raw == 0 {
72 return FsTimestamp::default();
73 }
74 FsTimestamp {
75 seconds: (raw / 10_000_000) as i64 - FILETIME_TO_UNIX_SECS,
76 nanoseconds: ((raw % 10_000_000) * 100) as u32,
77 }
78}
79
80impl ForensicFs for SevenZForensicFs {
81 fn root_ino(&self) -> u64 {
82 self.tree.root_ino()
83 }
84
85 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
86 self.tree.read_dir(ino)
87 }
88
89 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
90 self.tree.lookup(parent_ino, name)
91 }
92
93 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
94 self.tree.metadata(ino)
95 }
96
97 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
98 let id = self
99 .tree
100 .payload_id(ino)
101 .ok_or_else(|| FsError::NotFound(format!("inode {ino} is not a file")))?;
102 self.data
103 .get(id)
104 .cloned()
105 .ok_or_else(|| FsError::NotFound(format!("payload {id}")))
106 }
107
108 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
109 let data = self.read_file(ino)?;
110 let start = (offset as usize).min(data.len());
111 let end = start.saturating_add(len as usize).min(data.len());
112 Ok(data[start..end].to_vec())
113 }
114
115 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
116 Err(not_supported("symlinks not surfaced for 7z archives"))
117 }
118
119 fn fs_info(&self) -> FsResult<serde_json::Value> {
120 Ok(serde_json::json!({
121 "type": "7z",
122 "entries": self.data.len(),
123 }))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use std::io::Cursor;
131
132 fn make_7z() -> Option<Vec<u8>> {
135 use std::sync::atomic::{AtomicU32, Ordering};
136 static SEQ: AtomicU32 = AtomicU32::new(0);
137 let uniq = SEQ.fetch_add(1, Ordering::Relaxed);
138 let dir = std::env::temp_dir().join(format!("4n67z_{}_{uniq}", std::process::id()));
139 let _ = std::fs::remove_dir_all(&dir);
140 std::fs::create_dir_all(dir.join("sub")).ok()?;
141 std::fs::write(dir.join("hello.txt"), b"hello 7z\n").ok()?;
142 std::fs::write(dir.join("sub/deep.txt"), b"deep content\n").ok()?;
143 let out = dir.join("test.7z");
144 let mut made = false;
145 for bin in ["7z", "7za", "7zz"] {
146 let status = std::process::Command::new(bin)
147 .current_dir(&dir)
148 .arg("a")
149 .arg(&out)
150 .args(["hello.txt", "sub"])
151 .stdout(std::process::Stdio::null())
152 .status();
153 if matches!(status, Ok(s) if s.success()) {
154 made = true;
155 break;
156 }
157 }
158 let bytes = if made { std::fs::read(&out).ok() } else { None };
159 let _ = std::fs::remove_dir_all(&dir);
160 bytes
161 }
162
163 fn open() -> Option<SevenZForensicFs> {
164 SevenZForensicFs::new(Cursor::new(make_7z()?)).ok()
165 }
166
167 #[test]
168 fn root_ino_is_2() {
169 let Some(fs) = open() else {
170 eprintln!("skip: no 7z tool");
171 return;
172 };
173 assert_eq!(fs.root_ino(), 2);
174 }
175
176 #[test]
177 fn read_dir_root_lists_entries() {
178 let Some(mut fs) = open() else {
179 eprintln!("skip");
180 return;
181 };
182 let names: Vec<String> = fs
183 .read_dir(2)
184 .unwrap()
185 .iter()
186 .map(FsDirEntry::name_str)
187 .collect();
188 assert!(names.contains(&"hello.txt".to_string()), "got {names:?}");
189 assert!(names.contains(&"sub".to_string()), "got {names:?}");
190 }
191
192 #[test]
193 fn read_file_returns_content() {
194 let Some(mut fs) = open() else {
195 eprintln!("skip");
196 return;
197 };
198 let ino = fs.lookup(2, b"hello.txt").unwrap().unwrap();
199 assert_eq!(fs.read_file(ino).unwrap(), b"hello 7z\n");
200 }
201
202 #[test]
203 fn nested_file_reachable() {
204 let Some(mut fs) = open() else {
205 eprintln!("skip");
206 return;
207 };
208 let sub = fs.lookup(2, b"sub").unwrap().unwrap();
209 let deep = fs.lookup(sub, b"deep.txt").unwrap().unwrap();
210 assert_eq!(fs.read_file(deep).unwrap(), b"deep content\n");
211 }
212
213 #[test]
214 fn fs_info_reports_7z() {
215 let Some(fs) = open() else {
216 eprintln!("skip");
217 return;
218 };
219 assert_eq!(fs.fs_info().unwrap()["type"], "7z");
220 }
221
222 #[test]
223 fn filetime_zero_is_default() {
224 assert_eq!(filetime_to_ts(0), FsTimestamp::default());
225 }
226
227 #[test]
228 fn filetime_unix_epoch_converts() {
229 let raw = (FILETIME_TO_UNIX_SECS as u64) * 10_000_000;
231 assert_eq!(filetime_to_ts(raw).seconds, 0);
232 }
233}