blob: 6a61924819821aaa2d7a959c3e8d523d25141ce3 [file] [log] [blame]
Jiyong Park331d1ea2021-05-10 11:01:23 +09001/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16use anyhow::{anyhow, bail, Result};
17use std::collections::HashMap;
Jiyong Park851f68a2021-05-11 21:41:25 +090018use std::ffi::{CStr, CString};
Jiyong Park331d1ea2021-05-10 11:01:23 +090019use std::io;
Jiyong Park851f68a2021-05-11 21:41:25 +090020use std::os::unix::ffi::OsStrExt;
Jiyong Park331d1ea2021-05-10 11:01:23 +090021
22/// `InodeTable` is a table of `InodeData` indexed by `Inode`.
23#[derive(Debug)]
24pub struct InodeTable {
25 table: Vec<InodeData>,
26}
27
28/// `Inode` is the handle (or index in the table) to `InodeData` which represents an inode.
29pub type Inode = u64;
30
31const INVALID: Inode = 0;
32const ROOT: Inode = 1;
33
34/// `InodeData` represents an inode which has metadata about a file or a directory
35#[derive(Debug)]
36pub struct InodeData {
37 /// Size of the file that this inode represents. In case when the file is a directory, this
38 // is zero.
39 pub size: u64,
40 /// unix mode of this inode. It may not have `S_IFDIR` and `S_IFREG` in case the original zip
41 /// doesn't have the information in the external_attributes fields. To test if this inode
42 /// is for a regular file or a directory, use `is_dir`.
43 pub mode: u32,
44 data: InodeDataData,
45}
46
47type ZipIndex = usize;
48
49/// `InodeDataData` is the actual data (or a means to access the data) of the file or the directory
50/// that an inode is representing. In case of a directory, this data is the hash table of the
51/// directory entries. In case of a file, this data is the index of the file in `ZipArchive` which
52/// can be used to retrieve `ZipFile` that provides access to the content of the file.
53#[derive(Debug)]
54enum InodeDataData {
Jiyong Park851f68a2021-05-11 21:41:25 +090055 Directory(HashMap<CString, DirectoryEntry>),
Jiyong Park331d1ea2021-05-10 11:01:23 +090056 File(ZipIndex),
57}
58
59#[derive(Debug, Clone)]
60pub struct DirectoryEntry {
61 pub inode: Inode,
62 pub kind: InodeKind,
63}
64
Chris Wailes6f5a9b52022-08-11 15:01:54 -070065#[derive(Debug, Clone, PartialEq, Eq, Copy)]
Jiyong Park331d1ea2021-05-10 11:01:23 +090066pub enum InodeKind {
67 Directory,
68 File,
69}
70
71impl InodeData {
72 pub fn is_dir(&self) -> bool {
73 matches!(&self.data, InodeDataData::Directory(_))
74 }
75
Jiyong Park851f68a2021-05-11 21:41:25 +090076 pub fn get_directory(&self) -> Option<&HashMap<CString, DirectoryEntry>> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090077 match &self.data {
78 InodeDataData::Directory(hash) => Some(hash),
79 _ => None,
80 }
81 }
82
83 pub fn get_zip_index(&self) -> Option<ZipIndex> {
84 match &self.data {
85 InodeDataData::File(zip_index) => Some(*zip_index),
86 _ => None,
87 }
88 }
89
90 // Below methods are used to construct the inode table when initializing the filesystem. Once
91 // the initialization is done, these are not used because this is a read-only filesystem.
92
93 fn new_dir(mode: u32) -> InodeData {
94 InodeData { mode, size: 0, data: InodeDataData::Directory(HashMap::new()) }
95 }
96
97 fn new_file(zip_index: ZipIndex, zip_file: &zip::read::ZipFile) -> InodeData {
Jiyong Park7581dc32023-01-09 16:30:07 +090098 // b/264668376 some files in APK don't have unix permissions specified. Default to 400
99 // otherwise those files won't be readable even by the owner.
100 const DEFAULT_FILE_MODE: u32 = libc::S_IRUSR;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900101 InodeData {
Jiyong Park7581dc32023-01-09 16:30:07 +0900102 mode: zip_file.unix_mode().unwrap_or(DEFAULT_FILE_MODE),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900103 size: zip_file.size(),
104 data: InodeDataData::File(zip_index),
105 }
106 }
107
Jiyong Park851f68a2021-05-11 21:41:25 +0900108 fn add_to_directory(&mut self, name: CString, entry: DirectoryEntry) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900109 match &mut self.data {
110 InodeDataData::Directory(hashtable) => {
111 let existing = hashtable.insert(name, entry);
112 assert!(existing.is_none());
113 }
114 _ => {
115 panic!("can't add a directory entry to a file inode");
116 }
117 }
118 }
119}
120
121impl InodeTable {
122 /// Gets `InodeData` at a specific index.
123 pub fn get(&self, inode: Inode) -> Option<&InodeData> {
124 match inode {
125 INVALID => None,
126 _ => self.table.get(inode as usize),
127 }
128 }
129
130 fn get_mut(&mut self, inode: Inode) -> Option<&mut InodeData> {
131 match inode {
132 INVALID => None,
133 _ => self.table.get_mut(inode as usize),
134 }
135 }
136
137 fn put(&mut self, data: InodeData) -> Inode {
138 let inode = self.table.len() as Inode;
139 self.table.push(data);
140 inode
141 }
142
143 /// Finds the inode number of a file named `name` in the `parent` inode. The `parent` inode
144 /// must exist and be a directory.
Jiyong Park851f68a2021-05-11 21:41:25 +0900145 fn find(&self, parent: Inode, name: &CStr) -> Option<Inode> {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900146 let data = self.get(parent).unwrap();
147 match data.get_directory().unwrap().get(name) {
148 Some(DirectoryEntry { inode, .. }) => Some(*inode),
149 _ => None,
150 }
151 }
152
153 // Adds the inode `data` to the inode table and also links it to the `parent` inode as a file
154 // named `name`. The `parent` inode must exist and be a directory.
Jiyong Park851f68a2021-05-11 21:41:25 +0900155 fn add(&mut self, parent: Inode, name: CString, data: InodeData) -> Inode {
156 assert!(self.find(parent, &name).is_none());
Jiyong Park331d1ea2021-05-10 11:01:23 +0900157
158 let kind = if data.is_dir() { InodeKind::Directory } else { InodeKind::File };
159 // Add the inode to the table
160 let inode = self.put(data);
161
162 // ... and then register it to the directory of the parent inode
Jiyong Park851f68a2021-05-11 21:41:25 +0900163 self.get_mut(parent).unwrap().add_to_directory(name, DirectoryEntry { inode, kind });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900164 inode
165 }
166
167 /// Constructs `InodeTable` from a zip archive `archive`.
168 pub fn from_zip<R: io::Read + io::Seek>(
169 archive: &mut zip::ZipArchive<R>,
170 ) -> Result<InodeTable> {
171 let mut table = InodeTable { table: Vec::new() };
172
173 // Add the inodes for the invalid and the root directory
174 assert_eq!(INVALID, table.put(InodeData::new_dir(0)));
175 assert_eq!(ROOT, table.put(InodeData::new_dir(0)));
176
177 // For each zip file in the archive, create an inode and add it to the table. If the file's
178 // parent directories don't have corresponding inodes in the table, handle them too.
179 for i in 0..archive.len() {
180 let file = archive.by_index(i)?;
181 let path = file
182 .enclosed_name()
183 .ok_or_else(|| anyhow!("{} is an invalid name", file.name()))?;
184 // TODO(jiyong): normalize this (e.g. a/b/c/../d -> a/b/d). We can't use
185 // fs::canonicalize as this is a non-existing path yet.
186
187 let mut parent = ROOT;
188 let mut iter = path.iter().peekable();
189 while let Some(name) = iter.next() {
190 // TODO(jiyong): remove this check by canonicalizing `path`
191 if name == ".." {
192 bail!(".. is not allowed");
193 }
194
195 let is_leaf = iter.peek().is_none();
196 let is_file = file.is_file() && is_leaf;
197
198 // The happy path; the inode for `name` is already in the `parent` inode. Move on
199 // to the next path element.
Jiyong Park851f68a2021-05-11 21:41:25 +0900200 let name = CString::new(name.as_bytes()).unwrap();
201 if let Some(found) = table.find(parent, &name) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900202 parent = found;
203 // Update the mode if this is a directory leaf.
204 if !is_file && is_leaf {
205 let mut inode = table.get_mut(parent).unwrap();
206 inode.mode = file.unix_mode().unwrap_or(0);
207 }
208 continue;
209 }
210
211 const DEFAULT_DIR_MODE: u32 = libc::S_IRUSR | libc::S_IXUSR;
212
213 // No inode found. Create a new inode and add it to the inode table.
214 let inode = if is_file {
215 InodeData::new_file(i, &file)
216 } else if is_leaf {
217 InodeData::new_dir(file.unix_mode().unwrap_or(DEFAULT_DIR_MODE))
218 } else {
219 InodeData::new_dir(DEFAULT_DIR_MODE)
220 };
221 let new = table.add(parent, name, inode);
222 parent = new;
223 }
224 }
225 Ok(table)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use crate::inode::*;
232 use std::io::{Cursor, Write};
233 use zip::write::FileOptions;
234
235 // Creates an in-memory zip buffer, adds some files to it, and converts it to InodeTable
236 fn setup(add: fn(&mut zip::ZipWriter<&mut std::io::Cursor<Vec<u8>>>)) -> InodeTable {
237 let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
238 let mut writer = zip::ZipWriter::new(&mut buf);
239 add(&mut writer);
240 assert!(writer.finish().is_ok());
241 drop(writer);
242
243 let zip = zip::ZipArchive::new(buf);
244 assert!(zip.is_ok());
245 let it = InodeTable::from_zip(&mut zip.unwrap());
246 assert!(it.is_ok());
247 it.unwrap()
248 }
249
250 fn check_dir(it: &InodeTable, parent: Inode, name: &str) -> Inode {
Jiyong Park851f68a2021-05-11 21:41:25 +0900251 let name = CString::new(name.as_bytes()).unwrap();
252 let inode = it.find(parent, &name);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900253 assert!(inode.is_some());
254 let inode = inode.unwrap();
255 let inode_data = it.get(inode);
256 assert!(inode_data.is_some());
257 let inode_data = inode_data.unwrap();
258 assert_eq!(0, inode_data.size);
259 assert!(inode_data.is_dir());
260 inode
261 }
262
263 fn check_file<'a>(it: &'a InodeTable, parent: Inode, name: &str) -> &'a InodeData {
Jiyong Park851f68a2021-05-11 21:41:25 +0900264 let name = CString::new(name.as_bytes()).unwrap();
265 let inode = it.find(parent, &name);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900266 assert!(inode.is_some());
267 let inode = inode.unwrap();
268 let inode_data = it.get(inode);
269 assert!(inode_data.is_some());
270 let inode_data = inode_data.unwrap();
271 assert!(!inode_data.is_dir());
272 inode_data
273 }
274
275 #[test]
276 fn empty_zip_has_two_inodes() {
277 let it = setup(|_| {});
278 assert_eq!(2, it.table.len());
279 assert!(it.get(INVALID).is_none());
280 assert!(it.get(ROOT).is_some());
281 }
282
283 #[test]
284 fn one_file() {
285 let it = setup(|zip| {
286 zip.start_file("foo", FileOptions::default()).unwrap();
287 zip.write_all(b"0123456789").unwrap();
288 });
289 let inode_data = check_file(&it, ROOT, "foo");
290 assert_eq!(b"0123456789".len() as u64, inode_data.size);
291 }
292
293 #[test]
294 fn one_dir() {
295 let it = setup(|zip| {
296 zip.add_directory("foo", FileOptions::default()).unwrap();
297 });
298 let inode = check_dir(&it, ROOT, "foo");
299 // The directory doesn't have any entries
300 assert_eq!(0, it.get(inode).unwrap().get_directory().unwrap().len());
301 }
302
303 #[test]
304 fn one_file_in_subdirs() {
305 let it = setup(|zip| {
306 zip.start_file("a/b/c/d", FileOptions::default()).unwrap();
307 zip.write_all(b"0123456789").unwrap();
308 });
309
310 assert_eq!(6, it.table.len());
311 let a = check_dir(&it, ROOT, "a");
312 let b = check_dir(&it, a, "b");
313 let c = check_dir(&it, b, "c");
314 let d = check_file(&it, c, "d");
315 assert_eq!(10, d.size);
316 }
317
318 #[test]
319 fn complex_hierarchy() {
320 // root/
321 // a/
322 // b1/
323 // b2/
324 // c1 (file)
325 // c2/
326 // d1 (file)
327 // d2 (file)
328 // d3 (file)
329 // x/
330 // y1 (file)
331 // y2 (file)
332 // y3/
333 //
334 // foo (file)
335 // bar (file)
336 let it = setup(|zip| {
337 let opt = FileOptions::default();
338 zip.add_directory("a/b1", opt).unwrap();
339
340 zip.start_file("a/b2/c1", opt).unwrap();
341
342 zip.start_file("a/b2/c2/d1", opt).unwrap();
343 zip.start_file("a/b2/c2/d2", opt).unwrap();
344 zip.start_file("a/b2/c2/d3", opt).unwrap();
345
346 zip.start_file("x/y1", opt).unwrap();
347 zip.start_file("x/y2", opt).unwrap();
348 zip.add_directory("x/y3", opt).unwrap();
349
350 zip.start_file("foo", opt).unwrap();
351 zip.start_file("bar", opt).unwrap();
352 });
353
354 assert_eq!(16, it.table.len()); // 8 files, 6 dirs, and 2 (for root and the invalid inode)
355 let a = check_dir(&it, ROOT, "a");
356 let _b1 = check_dir(&it, a, "b1");
357 let b2 = check_dir(&it, a, "b2");
358 let _c1 = check_file(&it, b2, "c1");
359
360 let c2 = check_dir(&it, b2, "c2");
361 let _d1 = check_file(&it, c2, "d1");
362 let _d2 = check_file(&it, c2, "d3");
363 let _d3 = check_file(&it, c2, "d3");
364
365 let x = check_dir(&it, ROOT, "x");
366 let _y1 = check_file(&it, x, "y1");
367 let _y2 = check_file(&it, x, "y2");
368 let _y3 = check_dir(&it, x, "y3");
369
370 let _foo = check_file(&it, ROOT, "foo");
371 let _bar = check_file(&it, ROOT, "bar");
372 }
373
374 #[test]
375 fn file_size() {
376 let it = setup(|zip| {
377 let opt = FileOptions::default();
378 zip.start_file("empty", opt).unwrap();
379
380 zip.start_file("10bytes", opt).unwrap();
381 zip.write_all(&[0; 10]).unwrap();
382
383 zip.start_file("1234bytes", opt).unwrap();
384 zip.write_all(&[0; 1234]).unwrap();
385
386 zip.start_file("2^20bytes", opt).unwrap();
387 zip.write_all(&[0; 2 << 20]).unwrap();
388 });
389
390 let f = check_file(&it, ROOT, "empty");
391 assert_eq!(0, f.size);
392
393 let f = check_file(&it, ROOT, "10bytes");
394 assert_eq!(10, f.size);
395
396 let f = check_file(&it, ROOT, "1234bytes");
397 assert_eq!(1234, f.size);
398
399 let f = check_file(&it, ROOT, "2^20bytes");
400 assert_eq!(2 << 20, f.size);
401 }
402
403 #[test]
404 fn rejects_invalid_paths() {
405 let invalid_paths = [
406 "a/../../b", // escapes the root
407 "a/..", // escapes the root
408 "a/../../b/c", // escape the root
409 "a/b/../c", // doesn't escape the root, but not normalized
410 ];
411 for path in invalid_paths.iter() {
412 let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
413 let mut writer = zip::ZipWriter::new(&mut buf);
414 writer.start_file(*path, FileOptions::default()).unwrap();
415 assert!(writer.finish().is_ok());
416 drop(writer);
417
418 let zip = zip::ZipArchive::new(buf);
419 assert!(zip.is_ok());
420 let it = InodeTable::from_zip(&mut zip.unwrap());
421 assert!(it.is_err());
422 }
423 }
424}