Parse APK manifest to extract version etc

Add libapkmanifest to encapsulate the parsing, with a small C++
library on top of libandroidfw.

Extract the zip-handling code from libapkverify into a separate
libapkzip, and fix up a bunch of tests tests, to keep the build happy.

We don't do anything with the manifest information except log it; more
to come in another CL.

Bug: 299591171
Test: atest libapkzip.test libapkverify.integration_test
      libapkverify.test
Test: Manual - run VM, inspect logs.
Change-Id: I56d3bb7309d43ecb598a33320705d31948710f83
diff --git a/libs/apkzip/Android.bp b/libs/apkzip/Android.bp
new file mode 100644
index 0000000..dc35b5e
--- /dev/null
+++ b/libs/apkzip/Android.bp
@@ -0,0 +1,35 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libapkzip.defaults",
+    crate_name: "apkzip",
+    defaults: ["avf_build_flags_rust"],
+    edition: "2021",
+    srcs: ["src/ziputil.rs"],
+    rustlibs: [
+        "libanyhow",
+        "libbytes",
+        "liblog_rust",
+        "libanyhow",
+        "libzip",
+    ],
+}
+
+rust_library {
+    name: "libapkzip",
+    defaults: ["libapkzip.defaults"],
+    host_supported: true,
+    apex_available: ["com.android.virt"],
+}
+
+rust_test {
+    name: "libapkzip.test",
+    defaults: ["libapkzip.defaults"],
+    prefer_rlib: true,
+    test_suites: ["general-tests"],
+    rustlibs: [
+        "libbyteorder",
+    ],
+}
diff --git a/libs/apkzip/src/ziputil.rs b/libs/apkzip/src/ziputil.rs
new file mode 100644
index 0000000..708bbcc
--- /dev/null
+++ b/libs/apkzip/src/ziputil.rs
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! Utilities for zip handling of APK files.
+
+use anyhow::{ensure, Result};
+use bytes::{Buf, BufMut};
+use std::io::{Read, Seek};
+use zip::ZipArchive;
+
+const EOCD_SIZE_WITHOUT_COMMENT: usize = 22;
+const EOCD_CENTRAL_DIRECTORY_SIZE_FIELD_OFFSET: usize = 12;
+const EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET: usize = 16;
+/// End of Central Directory signature
+const EOCD_SIGNATURE: u32 = 0x06054b50;
+const ZIP64_MARK: u32 = 0xffffffff;
+
+/// Information about the layout of a zip file.
+#[derive(Debug, PartialEq, Eq)]
+pub struct ZipSections {
+    /// Offset within the file of the central directory.
+    pub central_directory_offset: u32,
+    /// Size of the central directory.
+    pub central_directory_size: u32,
+    /// Offset within the file of end of central directory marker.
+    pub eocd_offset: u32,
+    /// Size of the end of central directory marker.
+    pub eocd_size: u32,
+}
+
+/// Discover the layout of a zip file.
+pub fn zip_sections<R: Read + Seek>(mut reader: R) -> Result<ZipSections> {
+    // open a zip to parse EOCD
+    let archive = ZipArchive::new(reader)?;
+    let eocd_size = archive.comment().len() + EOCD_SIZE_WITHOUT_COMMENT;
+    ensure!(archive.offset() == 0, "Invalid ZIP: offset should be 0, but {}.", archive.offset());
+    // retrieve reader back
+    reader = archive.into_inner();
+    // the current position should point EOCD offset
+    let eocd_offset = reader.stream_position()? as u32;
+    let mut eocd = vec![0u8; eocd_size];
+    reader.read_exact(&mut eocd)?;
+    ensure!(
+        (&eocd[0..]).get_u32_le() == EOCD_SIGNATURE,
+        "Invalid ZIP: ZipArchive::new() should point EOCD after reading."
+    );
+    let (central_directory_size, central_directory_offset) = get_central_directory(&eocd)?;
+    ensure!(
+        central_directory_offset != ZIP64_MARK && central_directory_size != ZIP64_MARK,
+        "Unsupported ZIP: ZIP64 is not supported."
+    );
+    ensure!(
+        central_directory_offset + central_directory_size == eocd_offset,
+        "Invalid ZIP: EOCD should follow CD with no extra data or overlap."
+    );
+
+    Ok(ZipSections {
+        central_directory_offset,
+        central_directory_size,
+        eocd_offset,
+        eocd_size: eocd_size as u32,
+    })
+}
+
+fn get_central_directory(buf: &[u8]) -> Result<(u32, u32)> {
+    ensure!(buf.len() >= EOCD_SIZE_WITHOUT_COMMENT, "Invalid EOCD size: {}", buf.len());
+    let mut buf = &buf[EOCD_CENTRAL_DIRECTORY_SIZE_FIELD_OFFSET..];
+    let size = buf.get_u32_le();
+    let offset = buf.get_u32_le();
+    Ok((size, offset))
+}
+
+/// Update EOCD's central_directory_offset field.
+pub fn set_central_directory_offset(buf: &mut [u8], value: u32) -> Result<()> {
+    ensure!(buf.len() >= EOCD_SIZE_WITHOUT_COMMENT, "Invalid EOCD size: {}", buf.len());
+    (&mut buf[EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET..]).put_u32_le(value);
+    Ok(())
+}
+
+/// Read an entire file from a .zip file into memory and return it.
+pub fn read_file<R: Read + Seek>(reader: R, file_name: &str) -> Result<Vec<u8>> {
+    let mut archive = ZipArchive::new(reader)?;
+    let mut file = archive.by_name(file_name)?;
+    let mut bytes = Vec::with_capacity(file.size() as usize);
+    file.read_to_end(&mut bytes)?;
+    Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{Cursor, Write};
+    use zip::{write::FileOptions, ZipWriter};
+
+    const FILE_CONTENT: &[u8] = b"testcontent";
+    const FILE_NAME: &str = "testfile";
+
+    fn create_test_zip() -> Cursor<Vec<u8>> {
+        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
+        writer.start_file(FILE_NAME, FileOptions::default()).unwrap();
+        writer.write_all(FILE_CONTENT).unwrap();
+        writer.finish().unwrap()
+    }
+
+    fn assert_contains(haystack: &str, needle: &str) {
+        assert!(haystack.contains(needle), "{} is not found in {}", needle, haystack);
+    }
+
+    #[test]
+    fn test_zip_sections() {
+        let mut cursor = create_test_zip();
+        let sections = zip_sections(&mut cursor).unwrap();
+        assert_eq!(
+            sections.eocd_offset,
+            (cursor.get_ref().len() - EOCD_SIZE_WITHOUT_COMMENT) as u32
+        );
+    }
+
+    #[test]
+    fn test_read_file() {
+        let file = read_file(create_test_zip(), FILE_NAME).unwrap();
+        assert_eq!(file.as_slice(), FILE_CONTENT);
+    }
+
+    #[test]
+    fn test_reject_if_extra_data_between_cd_and_eocd() {
+        // prepare normal zip
+        let buf = create_test_zip().into_inner();
+
+        // insert garbage between CD and EOCD.
+        // by the way, to mock zip-rs, use CD as garbage. This is implementation detail of zip-rs,
+        // which reads CD at (eocd_offset - cd_size) instead of at cd_offset from EOCD.
+        let (pre_eocd, eocd) = buf.split_at(buf.len() - EOCD_SIZE_WITHOUT_COMMENT);
+        let (_, cd_offset) = get_central_directory(eocd).unwrap();
+        let cd = &pre_eocd[cd_offset as usize..];
+
+        // ZipArchive::new() succeeds, but we should reject
+        let res = zip_sections(Cursor::new([pre_eocd, cd, eocd].concat()));
+        assert!(res.is_err());
+        assert_contains(&res.err().unwrap().to_string(), "Invalid ZIP: offset should be 0");
+    }
+}