blob: 1f74f64c7700d7a8bcaab408dfc1d12aabb1a9a7 [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
Alan Stokesab127362023-10-30 10:54:22 +000034#[cfg(multi_tenant)]
35const READ_MODE: u32 = libc::S_IRUSR | libc::S_IRGRP;
36#[cfg(multi_tenant)]
37const EXECUTE_MODE: u32 = libc::S_IXUSR | libc::S_IXGRP;
38
39#[cfg(not(multi_tenant))]
40const READ_MODE: u32 = libc::S_IRUSR;
41#[cfg(not(multi_tenant))]
42const EXECUTE_MODE: u32 = libc::S_IXUSR;
43
44const DEFAULT_DIR_MODE: u32 = READ_MODE | EXECUTE_MODE;
Jiyong Parkdc81c112023-01-09 17:41:57 +090045// b/264668376 some files in APK don't have unix permissions specified. Default to 400
46// otherwise those files won't be readable even by the owner.
Alan Stokesab127362023-10-30 10:54:22 +000047const DEFAULT_FILE_MODE: u32 = READ_MODE;
48const EXECUTABLE_FILE_MODE: u32 = DEFAULT_FILE_MODE | EXECUTE_MODE;
Jiyong Parkdc81c112023-01-09 17:41:57 +090049
Jiyong Park331d1ea2021-05-10 11:01:23 +090050/// `InodeData` represents an inode which has metadata about a file or a directory
51#[derive(Debug)]
52pub struct InodeData {
53 /// Size of the file that this inode represents. In case when the file is a directory, this
54 // is zero.
55 pub size: u64,
56 /// unix mode of this inode. It may not have `S_IFDIR` and `S_IFREG` in case the original zip
57 /// doesn't have the information in the external_attributes fields. To test if this inode
58 /// is for a regular file or a directory, use `is_dir`.
59 pub mode: u32,
60 data: InodeDataData,
61}
62
63type ZipIndex = usize;
64
65/// `InodeDataData` is the actual data (or a means to access the data) of the file or the directory
66/// that an inode is representing. In case of a directory, this data is the hash table of the
67/// directory entries. In case of a file, this data is the index of the file in `ZipArchive` which
68/// can be used to retrieve `ZipFile` that provides access to the content of the file.
69#[derive(Debug)]
70enum InodeDataData {
Jiyong Park851f68a2021-05-11 21:41:25 +090071 Directory(HashMap<CString, DirectoryEntry>),
Jiyong Park331d1ea2021-05-10 11:01:23 +090072 File(ZipIndex),
73}
74
75#[derive(Debug, Clone)]
76pub struct DirectoryEntry {
77 pub inode: Inode,
78 pub kind: InodeKind,
79}
80
Chris Wailes6f5a9b52022-08-11 15:01:54 -070081#[derive(Debug, Clone, PartialEq, Eq, Copy)]
Jiyong Park331d1ea2021-05-10 11:01:23 +090082pub enum InodeKind {
83 Directory,
84 File,
85}
86
87impl InodeData {
88 pub fn is_dir(&self) -> bool {
89 matches!(&self.data, InodeDataData::Directory(_))
90 }
91
Jiyong Park851f68a2021-05-11 21:41:25 +090092 pub fn get_directory(&self) -> Option<&HashMap<CString, DirectoryEntry>> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090093 match &self.data {
94 InodeDataData::Directory(hash) => Some(hash),
95 _ => None,
96 }
97 }
98
99 pub fn get_zip_index(&self) -> Option<ZipIndex> {
100 match &self.data {
101 InodeDataData::File(zip_index) => Some(*zip_index),
102 _ => None,
103 }
104 }
105
106 // Below methods are used to construct the inode table when initializing the filesystem. Once
107 // the initialization is done, these are not used because this is a read-only filesystem.
108
109 fn new_dir(mode: u32) -> InodeData {
110 InodeData { mode, size: 0, data: InodeDataData::Directory(HashMap::new()) }
111 }
112
Nikita Ioffea7cb3672023-02-24 23:10:34 +0000113 fn new_file(zip_index: ZipIndex, mode: u32, zip_file: &zip::read::ZipFile) -> InodeData {
114 InodeData { mode, size: zip_file.size(), data: InodeDataData::File(zip_index) }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900115 }
116
Jiyong Park851f68a2021-05-11 21:41:25 +0900117 fn add_to_directory(&mut self, name: CString, entry: DirectoryEntry) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900118 match &mut self.data {
119 InodeDataData::Directory(hashtable) => {
120 let existing = hashtable.insert(name, entry);
121 assert!(existing.is_none());
122 }
123 _ => {
124 panic!("can't add a directory entry to a file inode");
125 }
126 }
127 }
128}
129
130impl InodeTable {
131 /// Gets `InodeData` at a specific index.
132 pub fn get(&self, inode: Inode) -> Option<&InodeData> {
133 match inode {
134 INVALID => None,
135 _ => self.table.get(inode as usize),
136 }
137 }
138
139 fn get_mut(&mut self, inode: Inode) -> Option<&mut InodeData> {
140 match inode {
141 INVALID => None,
142 _ => self.table.get_mut(inode as usize),
143 }
144 }
145
146 fn put(&mut self, data: InodeData) -> Inode {
147 let inode = self.table.len() as Inode;
148 self.table.push(data);
149 inode
150 }
151
152 /// Finds the inode number of a file named `name` in the `parent` inode. The `parent` inode
153 /// must exist and be a directory.
Jiyong Park851f68a2021-05-11 21:41:25 +0900154 fn find(&self, parent: Inode, name: &CStr) -> Option<Inode> {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900155 let data = self.get(parent).unwrap();
156 match data.get_directory().unwrap().get(name) {
157 Some(DirectoryEntry { inode, .. }) => Some(*inode),
158 _ => None,
159 }
160 }
161
162 // Adds the inode `data` to the inode table and also links it to the `parent` inode as a file
163 // named `name`. The `parent` inode must exist and be a directory.
Jiyong Park851f68a2021-05-11 21:41:25 +0900164 fn add(&mut self, parent: Inode, name: CString, data: InodeData) -> Inode {
165 assert!(self.find(parent, &name).is_none());
Jiyong Park331d1ea2021-05-10 11:01:23 +0900166
167 let kind = if data.is_dir() { InodeKind::Directory } else { InodeKind::File };
168 // Add the inode to the table
169 let inode = self.put(data);
170
171 // ... and then register it to the directory of the parent inode
Jiyong Park851f68a2021-05-11 21:41:25 +0900172 self.get_mut(parent).unwrap().add_to_directory(name, DirectoryEntry { inode, kind });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900173 inode
174 }
175
176 /// Constructs `InodeTable` from a zip archive `archive`.
177 pub fn from_zip<R: io::Read + io::Seek>(
178 archive: &mut zip::ZipArchive<R>,
179 ) -> Result<InodeTable> {
180 let mut table = InodeTable { table: Vec::new() };
181
182 // Add the inodes for the invalid and the root directory
183 assert_eq!(INVALID, table.put(InodeData::new_dir(0)));
Jiyong Parkdc81c112023-01-09 17:41:57 +0900184 assert_eq!(ROOT, table.put(InodeData::new_dir(DEFAULT_DIR_MODE)));
Jiyong Park331d1ea2021-05-10 11:01:23 +0900185
186 // For each zip file in the archive, create an inode and add it to the table. If the file's
187 // parent directories don't have corresponding inodes in the table, handle them too.
188 for i in 0..archive.len() {
189 let file = archive.by_index(i)?;
190 let path = file
191 .enclosed_name()
192 .ok_or_else(|| anyhow!("{} is an invalid name", file.name()))?;
193 // TODO(jiyong): normalize this (e.g. a/b/c/../d -> a/b/d). We can't use
194 // fs::canonicalize as this is a non-existing path yet.
195
196 let mut parent = ROOT;
197 let mut iter = path.iter().peekable();
Nikita Ioffea7cb3672023-02-24 23:10:34 +0000198
199 let mut file_mode = DEFAULT_FILE_MODE;
200 if path.starts_with("bin/") {
201 // Allow files under bin to have execute permission, this enables payloads to bundle
202 // additional binaries that they might want to execute.
203 // An example of such binary is measure_io one used in the authfs performance tests.
204 // More context available at b/265261525 and b/270955654.
Alan Stokes1294f942023-08-21 14:34:12 +0100205 file_mode = EXECUTABLE_FILE_MODE;
Nikita Ioffea7cb3672023-02-24 23:10:34 +0000206 }
207
Jiyong Park331d1ea2021-05-10 11:01:23 +0900208 while let Some(name) = iter.next() {
209 // TODO(jiyong): remove this check by canonicalizing `path`
210 if name == ".." {
211 bail!(".. is not allowed");
212 }
213
214 let is_leaf = iter.peek().is_none();
215 let is_file = file.is_file() && is_leaf;
216
217 // The happy path; the inode for `name` is already in the `parent` inode. Move on
218 // to the next path element.
Jiyong Park851f68a2021-05-11 21:41:25 +0900219 let name = CString::new(name.as_bytes()).unwrap();
220 if let Some(found) = table.find(parent, &name) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900221 parent = found;
222 // Update the mode if this is a directory leaf.
223 if !is_file && is_leaf {
Chris Wailes321e1282023-07-12 17:01:44 -0700224 let inode = table.get_mut(parent).unwrap();
Jiyong Parkdc81c112023-01-09 17:41:57 +0900225 inode.mode = file.unix_mode().unwrap_or(DEFAULT_DIR_MODE);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900226 }
227 continue;
228 }
229
Jiyong Park331d1ea2021-05-10 11:01:23 +0900230 // No inode found. Create a new inode and add it to the inode table.
Nikita Ioffea7cb3672023-02-24 23:10:34 +0000231 // At the moment of writing this comment the apk file doesn't specify any
232 // permissions (apart from the ones on lib/), but it might change in the future.
233 // TODO(b/270955654): should we control the file permissions ourselves?
Jiyong Park331d1ea2021-05-10 11:01:23 +0900234 let inode = if is_file {
Nikita Ioffea7cb3672023-02-24 23:10:34 +0000235 InodeData::new_file(i, file.unix_mode().unwrap_or(file_mode), &file)
Jiyong Park331d1ea2021-05-10 11:01:23 +0900236 } else if is_leaf {
237 InodeData::new_dir(file.unix_mode().unwrap_or(DEFAULT_DIR_MODE))
238 } else {
239 InodeData::new_dir(DEFAULT_DIR_MODE)
240 };
241 let new = table.add(parent, name, inode);
242 parent = new;
243 }
244 }
245 Ok(table)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use crate::inode::*;
252 use std::io::{Cursor, Write};
253 use zip::write::FileOptions;
254
255 // Creates an in-memory zip buffer, adds some files to it, and converts it to InodeTable
256 fn setup(add: fn(&mut zip::ZipWriter<&mut std::io::Cursor<Vec<u8>>>)) -> InodeTable {
257 let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
258 let mut writer = zip::ZipWriter::new(&mut buf);
259 add(&mut writer);
260 assert!(writer.finish().is_ok());
261 drop(writer);
262
263 let zip = zip::ZipArchive::new(buf);
264 assert!(zip.is_ok());
265 let it = InodeTable::from_zip(&mut zip.unwrap());
266 assert!(it.is_ok());
267 it.unwrap()
268 }
269
270 fn check_dir(it: &InodeTable, parent: Inode, name: &str) -> Inode {
Jiyong Park851f68a2021-05-11 21:41:25 +0900271 let name = CString::new(name.as_bytes()).unwrap();
272 let inode = it.find(parent, &name);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900273 assert!(inode.is_some());
274 let inode = inode.unwrap();
275 let inode_data = it.get(inode);
276 assert!(inode_data.is_some());
277 let inode_data = inode_data.unwrap();
278 assert_eq!(0, inode_data.size);
279 assert!(inode_data.is_dir());
280 inode
281 }
282
283 fn check_file<'a>(it: &'a InodeTable, parent: Inode, name: &str) -> &'a InodeData {
Jiyong Park851f68a2021-05-11 21:41:25 +0900284 let name = CString::new(name.as_bytes()).unwrap();
285 let inode = it.find(parent, &name);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900286 assert!(inode.is_some());
287 let inode = inode.unwrap();
288 let inode_data = it.get(inode);
289 assert!(inode_data.is_some());
290 let inode_data = inode_data.unwrap();
291 assert!(!inode_data.is_dir());
292 inode_data
293 }
294
295 #[test]
296 fn empty_zip_has_two_inodes() {
297 let it = setup(|_| {});
298 assert_eq!(2, it.table.len());
299 assert!(it.get(INVALID).is_none());
300 assert!(it.get(ROOT).is_some());
301 }
302
303 #[test]
304 fn one_file() {
305 let it = setup(|zip| {
306 zip.start_file("foo", FileOptions::default()).unwrap();
307 zip.write_all(b"0123456789").unwrap();
308 });
309 let inode_data = check_file(&it, ROOT, "foo");
310 assert_eq!(b"0123456789".len() as u64, inode_data.size);
311 }
312
313 #[test]
314 fn one_dir() {
315 let it = setup(|zip| {
316 zip.add_directory("foo", FileOptions::default()).unwrap();
317 });
318 let inode = check_dir(&it, ROOT, "foo");
319 // The directory doesn't have any entries
320 assert_eq!(0, it.get(inode).unwrap().get_directory().unwrap().len());
321 }
322
323 #[test]
324 fn one_file_in_subdirs() {
325 let it = setup(|zip| {
326 zip.start_file("a/b/c/d", FileOptions::default()).unwrap();
327 zip.write_all(b"0123456789").unwrap();
328 });
329
330 assert_eq!(6, it.table.len());
331 let a = check_dir(&it, ROOT, "a");
332 let b = check_dir(&it, a, "b");
333 let c = check_dir(&it, b, "c");
334 let d = check_file(&it, c, "d");
335 assert_eq!(10, d.size);
336 }
337
338 #[test]
339 fn complex_hierarchy() {
340 // root/
341 // a/
342 // b1/
343 // b2/
344 // c1 (file)
345 // c2/
346 // d1 (file)
347 // d2 (file)
348 // d3 (file)
349 // x/
350 // y1 (file)
351 // y2 (file)
352 // y3/
353 //
354 // foo (file)
355 // bar (file)
356 let it = setup(|zip| {
357 let opt = FileOptions::default();
358 zip.add_directory("a/b1", opt).unwrap();
359
360 zip.start_file("a/b2/c1", opt).unwrap();
361
362 zip.start_file("a/b2/c2/d1", opt).unwrap();
363 zip.start_file("a/b2/c2/d2", opt).unwrap();
364 zip.start_file("a/b2/c2/d3", opt).unwrap();
365
366 zip.start_file("x/y1", opt).unwrap();
367 zip.start_file("x/y2", opt).unwrap();
368 zip.add_directory("x/y3", opt).unwrap();
369
370 zip.start_file("foo", opt).unwrap();
371 zip.start_file("bar", opt).unwrap();
372 });
373
374 assert_eq!(16, it.table.len()); // 8 files, 6 dirs, and 2 (for root and the invalid inode)
375 let a = check_dir(&it, ROOT, "a");
376 let _b1 = check_dir(&it, a, "b1");
377 let b2 = check_dir(&it, a, "b2");
378 let _c1 = check_file(&it, b2, "c1");
379
380 let c2 = check_dir(&it, b2, "c2");
381 let _d1 = check_file(&it, c2, "d1");
382 let _d2 = check_file(&it, c2, "d3");
383 let _d3 = check_file(&it, c2, "d3");
384
385 let x = check_dir(&it, ROOT, "x");
386 let _y1 = check_file(&it, x, "y1");
387 let _y2 = check_file(&it, x, "y2");
388 let _y3 = check_dir(&it, x, "y3");
389
390 let _foo = check_file(&it, ROOT, "foo");
391 let _bar = check_file(&it, ROOT, "bar");
392 }
393
394 #[test]
395 fn file_size() {
396 let it = setup(|zip| {
397 let opt = FileOptions::default();
398 zip.start_file("empty", opt).unwrap();
399
400 zip.start_file("10bytes", opt).unwrap();
401 zip.write_all(&[0; 10]).unwrap();
402
403 zip.start_file("1234bytes", opt).unwrap();
404 zip.write_all(&[0; 1234]).unwrap();
405
406 zip.start_file("2^20bytes", opt).unwrap();
407 zip.write_all(&[0; 2 << 20]).unwrap();
408 });
409
410 let f = check_file(&it, ROOT, "empty");
411 assert_eq!(0, f.size);
412
413 let f = check_file(&it, ROOT, "10bytes");
414 assert_eq!(10, f.size);
415
416 let f = check_file(&it, ROOT, "1234bytes");
417 assert_eq!(1234, f.size);
418
419 let f = check_file(&it, ROOT, "2^20bytes");
420 assert_eq!(2 << 20, f.size);
421 }
422
423 #[test]
424 fn rejects_invalid_paths() {
425 let invalid_paths = [
426 "a/../../b", // escapes the root
427 "a/..", // escapes the root
428 "a/../../b/c", // escape the root
429 "a/b/../c", // doesn't escape the root, but not normalized
430 ];
431 for path in invalid_paths.iter() {
432 let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
433 let mut writer = zip::ZipWriter::new(&mut buf);
434 writer.start_file(*path, FileOptions::default()).unwrap();
435 assert!(writer.finish().is_ok());
436 drop(writer);
437
438 let zip = zip::ZipArchive::new(buf);
439 assert!(zip.is_ok());
440 let it = InodeTable::from_zip(&mut zip.unwrap());
441 assert!(it.is_err());
442 }
443 }
444}