android/filesystems/ramdisk_extractor.h: new file.
This patch introduces a new function that can be used to
extract single data file content from an Android ramdisk.img
image.
This will be used to extract fstab.goldfish in a future patch
to reliably determine the format of each partition image before
the emulator is run.
Change-Id: I4e8178ce1cedf0e93b852c632003d089bcd41701
diff --git a/Makefile.common b/Makefile.common
index 17f886e..6d8fcca 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -124,6 +124,7 @@
android/emulation/CpuAccelerator.cpp \
android/filesystems/ext4_utils.cpp \
android/filesystems/partition_types.cpp \
+ android/filesystems/ramdisk_extractor.cpp \
android/kernel/kernel_utils.cpp \
android/utils/assert.c \
android/utils/bufprint.c \
diff --git a/Makefile.tests b/Makefile.tests
index b9a5a28..2daabff 100644
--- a/Makefile.tests
+++ b/Makefile.tests
@@ -24,6 +24,7 @@
android/emulation/CpuAccelerator_unittest.cpp \
android/filesystems/ext4_utils_unittest.cpp \
android/filesystems/partition_types_unittest.cpp \
+ android/filesystems/ramdisk_extractor_unittest.cpp \
android/filesystems/testing/TestSupport.cpp \
android/kernel/kernel_utils_unittest.cpp \
diff --git a/android/filesystems/ramdisk_extractor.cpp b/android/filesystems/ramdisk_extractor.cpp
new file mode 100644
index 0000000..62e4445
--- /dev/null
+++ b/android/filesystems/ramdisk_extractor.cpp
@@ -0,0 +1,315 @@
+// Copyright 2014 The Android Open Source Project
+//
+// This software is licensed under the terms of the GNU General Public
+// License version 2, as published by the Free Software Foundation, and
+// may be copied, distributed, and modified under those terms.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+#include "android/filesystems/ramdisk_extractor.h"
+
+#include "android/base/Compiler.h"
+#include "android/base/Log.h"
+#include "android/base/String.h"
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <zlib.h>
+
+#define DEBUG 0
+
+#if DEBUG
+# define D(...) printf(__VA_ARGS__), fflush(stdout)
+#else
+# define D(...) ((void)0)
+#endif
+
+// Ramdisk images are gzipped cpio archives using the new ASCII
+// format as described at [1]. Hence this source file first implements
+// a gzip-based input stream class, then
+//
+// [1] http://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt
+
+namespace {
+
+// Helper class used to implement a gzip-based input stream.
+// Usage is as follows:
+//
+// GZipInputStream input(filePath);
+// if (input.error()) {
+// fprintf(stderr, "Could not open file: %s\n",
+// filePath, strerror(input.error()));
+// } else {
+// uint8_t header[32];
+// if (!doRead(&input, header, sizeof header)) {
+// ... error, could not read header.
+// }
+// }
+// .. stream is closed automatically on scope exit.
+class GZipInputStream {
+public:
+ // Open a new input stream to read from file |filePath|.
+ // The constructor can never fail, so call error() after
+ // this to see if an error occured.
+ explicit GZipInputStream(const char* filePath) {
+ mFile = gzopen(filePath, "rb");
+ if (mFile == Z_NULL) {
+ mFile = NULL;
+ mError = errno;
+ } else {
+ mError = 0;
+ }
+ }
+
+ // Return the last error that occured on this stream,
+ // or 0 if everything's well. Note that as soon as an
+ // error occurs, the stream cannot be used anymore.
+ int error() const { return mError; }
+
+ // Close the stream, note that this is called automatically
+ // from the destructor, but clients might want to do this
+ // before.
+ void close() {
+ if (mFile) {
+ gzclose(mFile);
+ mFile = NULL;
+ }
+ }
+
+ ~GZipInputStream() {
+ close();
+ }
+
+ // Try to read up to |len| bytes of data from the input
+ // stream into |buffer|. On success, return true and sets
+ // |*readBytes| to the number of bytes that were read.
+ // On failure, return false and set error().
+ bool tryRead(void* buffer, size_t len, size_t* readBytes) {
+ *readBytes = 0;
+
+ if (mError) {
+ return false;
+ }
+
+ if (len == 0) {
+ return true;
+ }
+
+ // Warning, gzread() takes an unsigned int parameter for
+ // the length, but will return an error if its value
+ // exceeds INT_MAX anyway.
+ char* buff = reinterpret_cast<char*>(buffer);
+
+ while (len > 0) {
+ size_t avail = len;
+ if (avail > INT_MAX)
+ avail = INT_MAX;
+
+ int ret = gzread(mFile, buff, static_cast<unsigned int>(avail));
+ if (ret < 0) {
+ gzerror(mFile, &mError);
+ break;
+ }
+ if (ret == 0) {
+ if (gzeof(mFile)) {
+ break;
+ }
+ gzerror(mFile, &mError);
+ break;
+ }
+ len -= ret;
+ buff += ret;
+ *readBytes += ret;
+ }
+
+ return (!mError && *readBytes > 0);
+ }
+
+ // Read exactly |len| bytes of data into |buffer|. On success,
+ // return true, on failure, return false and set error().
+ bool doRead(void* buffer, size_t len) {
+ size_t readCount = 0;
+ if (!tryRead(buffer, len, &readCount)) {
+ return false;
+ }
+ if (readCount < len) {
+ mError = EIO;
+ return false;
+ }
+ return true;
+ }
+
+ bool doSkip(size_t len) {
+ if (mError) {
+ return false;
+ }
+ if (gzseek(mFile, len, SEEK_CUR) < 0) {
+ gzerror(mFile, &mError);
+ return false;
+ }
+ return true;
+ }
+
+private:
+ DISALLOW_COPY_AND_ASSIGN(GZipInputStream);
+
+ gzFile mFile;
+ int mError;
+};
+
+// Parse an hexadecimal string of 8 characters. On success,
+// return true and sets |*value| to its value. On failure,
+// return false.
+bool parse_hex8(const char* input, uint32_t* value) {
+ uint32_t result = 0;
+ for (int n = 0; n < 8; ++n) {
+ int c = input[n];
+ unsigned d = static_cast<unsigned>(c - '0');
+ if (d >= 10) {
+ d = static_cast<unsigned>(c - 'a');
+ if (d >= 6) {
+ d = static_cast<unsigned>(c - 'A');
+ if (d >= 6) {
+ return false;
+ }
+ }
+ d += 10;
+ }
+ result = (result << 4) | d;
+ }
+ *value = result;
+ return true;
+}
+
+} // namespace
+
+bool android_extractRamdiskFile(const char* ramdiskPath,
+ const char* fileName,
+ char** out,
+ size_t* outSize) {
+ *out = NULL;
+ *outSize = 0;
+
+ GZipInputStream input(ramdiskPath);
+ if (input.error()) {
+ errno = input.error();
+ return false;
+ }
+
+ // Type of cpio new ASCII header.
+ struct cpio_newc_header {
+ char c_magic[6];
+ char c_ino[8];
+ char c_mode[8];
+ char c_uid[8];
+ char c_gid[8];
+ char c_nlink[8];
+ char c_mtime[8];
+ char c_filesize[8];
+ char c_devmajor[8];
+ char c_devminor[8];
+ char c_rdevmajor[8];
+ char c_rdevminor[8];
+ char c_namesize[8];
+ char c_check[8];
+ };
+
+ size_t fileNameLen = strlen(fileName);
+
+ for (;;) {
+ // Read the header then check it.
+ cpio_newc_header header;
+ if (!input.doRead(&header, sizeof header)) {
+ // Assume end of input here.
+ D("Could not find %s in ramdisk image at %s\n",
+ fileName, ramdiskPath);
+ return false;
+ }
+
+ D("HEADER %.6s\n", header.c_magic);
+ if (memcmp(header.c_magic, "070701", 6) != 0) {
+ D("Not a valid ramdisk image file: %s\n", ramdiskPath);
+ errno = EINVAL;
+ return false;
+ }
+
+ // Compare file names, note that files with a size of 0 are
+ // hard links and should be ignored.
+ uint32_t nameSize;
+ uint32_t entrySize;
+ if (!parse_hex8(header.c_namesize, &nameSize) ||
+ !parse_hex8(header.c_filesize, &entrySize)) {
+ D("Could not parse ramdisk file entry header!");
+ break;
+ }
+
+ D("---- %d nameSize=%d entrySize=%d\n", __LINE__, nameSize, entrySize);
+
+ // The header is followed by the name, followed by 4-byte padding
+ // with NUL bytes. Compute the number of bytes to skip over the
+ // name.
+ size_t skipName =
+ ((sizeof header + nameSize + 3) & ~3) - sizeof header;
+
+ // The file data is 4-byte padded with NUL bytes too.
+ size_t skipFile = (entrySize + 3) & ~3;
+ size_t skipCount = 0;
+
+ // Last record is named 'TRAILER!!!' and indicates end of archive.
+ static const char kTrailer[] = "TRAILER!!!";
+ static const size_t kTrailerSize = sizeof(kTrailer) - 1U;
+
+ if ((entrySize == 0 || nameSize != fileNameLen + 1U) &&
+ nameSize != kTrailerSize + 1U) {
+ D("---- %d Skipping\n", __LINE__);
+ skipCount = skipName + skipFile;
+ } else {
+ // Read the name and compare it.
+ nameSize -= 1U;
+ android::base::String entryName;
+ entryName.resize(nameSize);
+ if (!input.doRead(&entryName[0], nameSize)) {
+ D("Could not read ramdisk file entry name!");
+ break;
+ }
+ D("---- %d Name=[%s]\n", __LINE__, entryName.c_str());
+ skipCount -= nameSize;
+
+ // Check for last entry.
+ if (nameSize == kTrailerSize &&
+ !strcmp(entryName.c_str(), kTrailer)) {
+ D("End of archive reached. Could not find %s in ramdisk image at %s",
+ fileName, ramdiskPath);
+ return false;
+ }
+
+ // Check for the search file name.
+ if (nameSize == entryName.size() &&
+ !strcmp(entryName.c_str(), fileName)) {
+ // Found it !! Skip over padding.
+ if (!input.doSkip(skipName - nameSize)) {
+ D("Could not skip ramdisk name entry!");
+ break;
+ }
+
+ // Then read data into head-allocated buffer.
+ *out = reinterpret_cast<char*>(malloc(entrySize));
+ *outSize = entrySize;
+
+ return input.doRead(*out, entrySize);
+ }
+ }
+ if (!input.doSkip(skipCount)) {
+ D("Could not skip ramdisk entry!");
+ break;
+ }
+ }
+
+ errno = input.error();
+ return false;
+}
diff --git a/android/filesystems/ramdisk_extractor.h b/android/filesystems/ramdisk_extractor.h
new file mode 100644
index 0000000..840d4de
--- /dev/null
+++ b/android/filesystems/ramdisk_extractor.h
@@ -0,0 +1,35 @@
+// Copyright 2014 The Android Open Source Project
+//
+// This software is licensed under the terms of the GNU General Public
+// License version 2, as published by the Free Software Foundation, and
+// may be copied, distributed, and modified under those terms.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+#ifndef ANDROID_FILESYSTEMS_RAMDISK_EXTRACTOR_H
+#define ANDROID_FILESYSTEMS_RAMDISK_EXTRACTOR_H
+
+#include "android/utils/compiler.h"
+
+#include <stddef.h>
+
+
+ANDROID_BEGIN_HEADER
+
+// Extract the content of a given file from a ramdisk image.
+// |ramdisk_path| is the path to the ramdisk.img file.
+// |file_path| is the path of the file within the ramdisk.
+// On success, returns true and sets |*out| to point to a heap allocated
+// block containing the extracted content, of size |*out_size| bytes.
+// On failure, return false.
+bool android_extractRamdiskFile(const char* ramdisk_path,
+ const char* file_path,
+ char** out,
+ size_t* out_size);
+
+ANDROID_END_HEADER
+
+#endif // ANDROID_FILESYSTEMS_RAMDISK_EXTRACTOR_H
diff --git a/android/filesystems/ramdisk_extractor_unittest.cpp b/android/filesystems/ramdisk_extractor_unittest.cpp
new file mode 100644
index 0000000..2462b5d
--- /dev/null
+++ b/android/filesystems/ramdisk_extractor_unittest.cpp
@@ -0,0 +1,88 @@
+// Copyright 2014 The Android Open Source Project
+//
+// This software is licensed under the terms of the GNU General Public
+// License version 2, as published by the Free Software Foundation, and
+// may be copied, distributed, and modified under those terms.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+#include "android/filesystems/ramdisk_extractor.h"
+
+#include "android/base/EintrWrapper.h"
+#include "android/filesystems/testing/TestSupport.h"
+
+#include <gtest/gtest.h>
+
+#include <stdio.h>
+
+namespace {
+
+#include "android/filesystems/testing/TestRamdiskImage.h"
+
+class RamdiskExtractorTest : public ::testing::Test {
+public:
+ RamdiskExtractorTest() :
+ mTempFilePath(android::testing::CreateTempFilePath()) {}
+
+ bool fillData(const void* data, size_t dataSize) {
+ FILE* file = ::fopen(mTempFilePath.c_str(), "wb");
+ if (!file) {
+ return false;
+ }
+
+ bool result = (fwrite(data, dataSize, 1, file) == 1);
+ fclose(file);
+ return result;
+ }
+
+ ~RamdiskExtractorTest() {
+ if (!mTempFilePath.empty()) {
+ HANDLE_EINTR(unlink(mTempFilePath.c_str()));
+ }
+ }
+
+ const char* path() const { return mTempFilePath.c_str(); }
+
+private:
+ std::string mTempFilePath;
+};
+
+} // namespace
+
+TEST_F(RamdiskExtractorTest, FindFoo) {
+ static const char kExpected[] = "Hello World!\n";
+ static const size_t kExpectedSize = sizeof(kExpected) - 1U;
+ char* out = NULL;
+ size_t outSize = 0;
+
+ EXPECT_TRUE(fillData(kTestRamdiskImage, kTestRamdiskImageSize));
+ EXPECT_TRUE(android_extractRamdiskFile(path(), "foo", &out, &outSize));
+ EXPECT_EQ(kExpectedSize, outSize);
+ EXPECT_TRUE(out);
+ EXPECT_TRUE(!memcmp(out, kExpected, outSize));
+ free(out);
+}
+
+TEST_F(RamdiskExtractorTest, FindBar2) {
+ static const char kExpected[] = "La vie est un long fleuve tranquille\n";
+ static const size_t kExpectedSize = sizeof(kExpected) - 1U;
+ char* out = NULL;
+ size_t outSize = 0;
+
+ EXPECT_TRUE(fillData(kTestRamdiskImage, kTestRamdiskImageSize));
+ EXPECT_TRUE(android_extractRamdiskFile(path(), "bar2", &out, &outSize));
+ EXPECT_EQ(kExpectedSize, outSize);
+ EXPECT_TRUE(out);
+ EXPECT_TRUE(!memcmp(out, kExpected, outSize));
+ free(out);
+}
+
+TEST_F(RamdiskExtractorTest, MissingFile) {
+ char* out = NULL;
+ size_t outSize = 0;
+ EXPECT_TRUE(fillData(kTestRamdiskImage, kTestRamdiskImageSize));
+ EXPECT_FALSE(android_extractRamdiskFile(path(), "zoolander", &out, &outSize));
+}
diff --git a/android/filesystems/testing/TestRamdiskImage.h b/android/filesystems/testing/TestRamdiskImage.h
new file mode 100644
index 0000000..baec302
--- /dev/null
+++ b/android/filesystems/testing/TestRamdiskImage.h
@@ -0,0 +1,37 @@
+// Copyright 2014 The Android Open Source Project
+//
+// This software is licensed under the terms of the GNU General Public
+// License version 2, as published by the Free Software Foundation, and
+// may be copied, distributed, and modified under those terms.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+#ifndef ANDROID_FILESYSTEMS_TESTING_TEST_RAMDISK_IMAGE_H
+#define ANDROID_FILESYSTEMS_TESTING_TEST_RAMDISK_IMAGE_H
+
+/* Auto-generated by create_ramdisk_test_data.sh on 2014-06-23 - DO NOT EDIT!! */
+
+static const unsigned char kTestRamdiskImage[] = {
+ 0x1f, 0x8b, 0x08, 0x00, 0x6d, 0x1a, 0xa8, 0x53, 0x02, 0x03, 0xbd, 0x8e,
+ 0x41, 0x0a, 0x83, 0x30, 0x10, 0x45, 0xb3, 0xee, 0x29, 0x26, 0x37, 0x98,
+ 0xc4, 0x46, 0xd3, 0x65, 0xac, 0x86, 0x16, 0x5c, 0x49, 0xa1, 0x6b, 0x4b,
+ 0x63, 0x11, 0x06, 0x43, 0xad, 0x7a, 0xfe, 0x2a, 0x41, 0x90, 0x2e, 0x8a,
+ 0xdd, 0xf4, 0x6d, 0xfe, 0x1f, 0x66, 0x60, 0x1e, 0x26, 0x98, 0xa0, 0x40,
+ 0x14, 0xb9, 0xd4, 0x07, 0x8b, 0x13, 0x5a, 0x98, 0x39, 0x30, 0xb6, 0x26,
+ 0x9d, 0x53, 0x44, 0x5a, 0x63, 0x40, 0xa8, 0xc8, 0x4c, 0xeb, 0x38, 0x0b,
+ 0xa3, 0x54, 0x21, 0xed, 0x11, 0xbf, 0xa3, 0x96, 0x72, 0xab, 0x3a, 0xc9,
+ 0x58, 0x51, 0xc1, 0xd8, 0x38, 0x70, 0xaf, 0x1e, 0x86, 0x16, 0xc8, 0xb7,
+ 0x0f, 0xa8, 0xc9, 0x0d, 0xa3, 0x83, 0xbe, 0xab, 0xda, 0xe7, 0xd0, 0x10,
+ 0xb9, 0x1d, 0x63, 0x0c, 0xd7, 0x6e, 0xd9, 0x6f, 0x6e, 0x98, 0x6d, 0x74,
+ 0xdb, 0x2f, 0xa5, 0xf6, 0x7e, 0xfa, 0x79, 0x72, 0x44, 0x1e, 0xae, 0xbe,
+ 0xa3, 0x3b, 0x5f, 0x3b, 0x6c, 0x62, 0xeb, 0xdd, 0x27, 0xe9, 0x52, 0x2e,
+ 0xa5, 0x39, 0x17, 0x79, 0xc9, 0x39, 0x67, 0x7f, 0xe2, 0x0d, 0xb9, 0x32,
+ 0x87, 0xaa, 0x00, 0x02, 0x00, 0x00
+};
+
+static const size_t kTestRamdiskImageSize = sizeof(kTestRamdiskImage);
+
+#endif // ANDROID_FILESYSTEMS_TESTING_TEST_RAMDISK_IMAGE_H
diff --git a/android/filesystems/testing/create_ramdisk_test_data.sh b/android/filesystems/testing/create_ramdisk_test_data.sh
new file mode 100755
index 0000000..1bd3e42
--- /dev/null
+++ b/android/filesystems/testing/create_ramdisk_test_data.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+# This script is used to generate souce files containing test
+# data for the ramdisk_extractor unit tests.
+
+set -e
+
+export LANG=C
+export LC_ALL=C
+
+PROGNAME=$(basename "$0")
+DATE=$(date +%Y-%m-%d)
+
+# $1: Root directory
+create_ramdisk_header_from () {
+ local FILE_LIST
+ FILE_LIST=$(cd $2 && find . -type f 2>/dev/null | sed -e 's|^./||g')
+ echo "$FILE_LIST" | cpio --create --format=newc --quiet | gzip -9c | xxd -i -
+}
+
+TMPDIR=/tmp/$USER-ramdisk-test-data
+rm -rf $TMPDIR/dir1 && mkdir -p $TMPDIR/dir1
+cd $TMPDIR/dir1
+cat > foo <<EOF
+Hello World!
+EOF
+cat > bar2 <<EOF
+La vie est un long fleuve tranquille
+EOF
+
+echo "/* Auto-generated by $PROGNAME on $DATE - DO NOT EDIT!! */"
+echo ""
+echo "static const unsigned char kTestRamdiskImage[] = {"
+create_ramdisk_header_from $TMPDIR/ramdisk1.img.h $TMPDIR/dir1
+echo "};"
+echo ""
+echo "static const size_t kTestRamdiskImageSize = sizeof(kTestRamdiskImage);"
+echo ""
+