Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 1 | /* |
| 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 | */ |
| 16 | use anyhow::{anyhow, bail, Result}; |
| 17 | use std::collections::HashMap; |
Jiyong Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 18 | use std::ffi::{CStr, CString}; |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 19 | use std::io; |
Jiyong Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 20 | use std::os::unix::ffi::OsStrExt; |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 21 | |
| 22 | /// `InodeTable` is a table of `InodeData` indexed by `Inode`. |
| 23 | #[derive(Debug)] |
| 24 | pub struct InodeTable { |
| 25 | table: Vec<InodeData>, |
| 26 | } |
| 27 | |
| 28 | /// `Inode` is the handle (or index in the table) to `InodeData` which represents an inode. |
| 29 | pub type Inode = u64; |
| 30 | |
| 31 | const INVALID: Inode = 0; |
| 32 | const ROOT: Inode = 1; |
| 33 | |
Alan Stokes | ab12736 | 2023-10-30 10:54:22 +0000 | [diff] [blame^] | 34 | #[cfg(multi_tenant)] |
| 35 | const READ_MODE: u32 = libc::S_IRUSR | libc::S_IRGRP; |
| 36 | #[cfg(multi_tenant)] |
| 37 | const EXECUTE_MODE: u32 = libc::S_IXUSR | libc::S_IXGRP; |
| 38 | |
| 39 | #[cfg(not(multi_tenant))] |
| 40 | const READ_MODE: u32 = libc::S_IRUSR; |
| 41 | #[cfg(not(multi_tenant))] |
| 42 | const EXECUTE_MODE: u32 = libc::S_IXUSR; |
| 43 | |
| 44 | const DEFAULT_DIR_MODE: u32 = READ_MODE | EXECUTE_MODE; |
Jiyong Park | dc81c11 | 2023-01-09 17:41:57 +0900 | [diff] [blame] | 45 | // 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 Stokes | ab12736 | 2023-10-30 10:54:22 +0000 | [diff] [blame^] | 47 | const DEFAULT_FILE_MODE: u32 = READ_MODE; |
| 48 | const EXECUTABLE_FILE_MODE: u32 = DEFAULT_FILE_MODE | EXECUTE_MODE; |
Jiyong Park | dc81c11 | 2023-01-09 17:41:57 +0900 | [diff] [blame] | 49 | |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 50 | /// `InodeData` represents an inode which has metadata about a file or a directory |
| 51 | #[derive(Debug)] |
| 52 | pub 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 | |
| 63 | type 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)] |
| 70 | enum InodeDataData { |
Jiyong Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 71 | Directory(HashMap<CString, DirectoryEntry>), |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 72 | File(ZipIndex), |
| 73 | } |
| 74 | |
| 75 | #[derive(Debug, Clone)] |
| 76 | pub struct DirectoryEntry { |
| 77 | pub inode: Inode, |
| 78 | pub kind: InodeKind, |
| 79 | } |
| 80 | |
Chris Wailes | 6f5a9b5 | 2022-08-11 15:01:54 -0700 | [diff] [blame] | 81 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 82 | pub enum InodeKind { |
| 83 | Directory, |
| 84 | File, |
| 85 | } |
| 86 | |
| 87 | impl InodeData { |
| 88 | pub fn is_dir(&self) -> bool { |
| 89 | matches!(&self.data, InodeDataData::Directory(_)) |
| 90 | } |
| 91 | |
Jiyong Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 92 | pub fn get_directory(&self) -> Option<&HashMap<CString, DirectoryEntry>> { |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 93 | 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 Ioffe | a7cb367 | 2023-02-24 23:10:34 +0000 | [diff] [blame] | 113 | 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 Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 115 | } |
| 116 | |
Jiyong Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 117 | fn add_to_directory(&mut self, name: CString, entry: DirectoryEntry) { |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 118 | 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 | |
| 130 | impl 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 154 | fn find(&self, parent: Inode, name: &CStr) -> Option<Inode> { |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 155 | 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 164 | fn add(&mut self, parent: Inode, name: CString, data: InodeData) -> Inode { |
| 165 | assert!(self.find(parent, &name).is_none()); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 166 | |
| 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 172 | self.get_mut(parent).unwrap().add_to_directory(name, DirectoryEntry { inode, kind }); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 173 | 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 Park | dc81c11 | 2023-01-09 17:41:57 +0900 | [diff] [blame] | 184 | assert_eq!(ROOT, table.put(InodeData::new_dir(DEFAULT_DIR_MODE))); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 185 | |
| 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 Ioffe | a7cb367 | 2023-02-24 23:10:34 +0000 | [diff] [blame] | 198 | |
| 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 Stokes | 1294f94 | 2023-08-21 14:34:12 +0100 | [diff] [blame] | 205 | file_mode = EXECUTABLE_FILE_MODE; |
Nikita Ioffe | a7cb367 | 2023-02-24 23:10:34 +0000 | [diff] [blame] | 206 | } |
| 207 | |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 208 | 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 219 | let name = CString::new(name.as_bytes()).unwrap(); |
| 220 | if let Some(found) = table.find(parent, &name) { |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 221 | parent = found; |
| 222 | // Update the mode if this is a directory leaf. |
| 223 | if !is_file && is_leaf { |
Chris Wailes | 321e128 | 2023-07-12 17:01:44 -0700 | [diff] [blame] | 224 | let inode = table.get_mut(parent).unwrap(); |
Jiyong Park | dc81c11 | 2023-01-09 17:41:57 +0900 | [diff] [blame] | 225 | inode.mode = file.unix_mode().unwrap_or(DEFAULT_DIR_MODE); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 226 | } |
| 227 | continue; |
| 228 | } |
| 229 | |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 230 | // No inode found. Create a new inode and add it to the inode table. |
Nikita Ioffe | a7cb367 | 2023-02-24 23:10:34 +0000 | [diff] [blame] | 231 | // 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 Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 234 | let inode = if is_file { |
Nikita Ioffe | a7cb367 | 2023-02-24 23:10:34 +0000 | [diff] [blame] | 235 | InodeData::new_file(i, file.unix_mode().unwrap_or(file_mode), &file) |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 236 | } 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)] |
| 250 | mod 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 271 | let name = CString::new(name.as_bytes()).unwrap(); |
| 272 | let inode = it.find(parent, &name); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 273 | 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 Park | 851f68a | 2021-05-11 21:41:25 +0900 | [diff] [blame] | 284 | let name = CString::new(name.as_bytes()).unwrap(); |
| 285 | let inode = it.find(parent, &name); |
Jiyong Park | 331d1ea | 2021-05-10 11:01:23 +0900 | [diff] [blame] | 286 | 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 | } |