Rewrite libpackagelistparser.

I was happy to ignore this as long as it wasn't being touched, but it's
been modified twice in the last year. Time to just throw it out and
rewrite it.

Also add some basic tests and put them in presubmit.

Bug: http://b/127686429
Test: new tests
Change-Id: Ie7e9406521291de0eab3138f55068cee6aaf365a
This commit is contained in:
Elliott Hughes 2019-08-07 22:59:00 -07:00
parent a022e47a0b
commit d32e7e2c05
7 changed files with 341 additions and 359 deletions

View file

@ -18,6 +18,9 @@
{
"name": "libbase_test"
},
{
"name": "libpackagelistparser_test"
},
{
"name": "libprocinfo_test"
},

View file

@ -0,0 +1 @@
../.clang-format-2

View file

@ -1,12 +1,7 @@
cc_library {
name: "libpackagelistparser",
recovery_available: true,
srcs: ["packagelistparser.c"],
cflags: [
"-Wall",
"-Werror",
],
srcs: ["packagelistparser.cpp"],
shared_libs: ["liblog"],
local_include_dirs: ["include"],
export_include_dirs: ["include"],
@ -15,3 +10,13 @@ cc_library {
misc_undefined: ["integer"],
},
}
cc_test {
name: "libpackagelistparser_test",
srcs: ["packagelistparser_test.cpp"],
shared_libs: [
"libbase",
"libpackagelistparser",
],
test_suites: ["device-tests"],
}

View file

@ -1,94 +1,81 @@
/*
* Copyright 2015, Intel Corporation
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2019 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
* 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.
*
* Written by William Roberts <william.c.roberts@intel.com>
*
* This is a parser library for parsing the packages.list file generated
* by PackageManager service.
*
* This simple parser is sensitive to format changes in
* frameworks/base/services/core/java/com/android/server/pm/Settings.java
* A dependency note has been added to that file to correct
* this parser.
*/
#ifndef PACKAGELISTPARSER_H_
#define PACKAGELISTPARSER_H_
#pragma once
#include <stdbool.h>
#include <sys/cdefs.h>
#include <sys/types.h>
__BEGIN_DECLS
/** The file containing the list of installed packages on the system */
#define PACKAGES_LIST_FILE "/data/system/packages.list"
typedef struct gid_list {
/** Number of gids. */
size_t cnt;
typedef struct pkg_info pkg_info;
typedef struct gid_list gid_list;
/** Array of gids. */
gid_t* gids;
} gid_list;
struct gid_list {
size_t cnt;
gid_t *gids;
};
typedef struct pkg_info {
/** Package name like "com.android.blah". */
char* name;
struct pkg_info {
char *name;
uid_t uid;
bool debuggable;
char *data_dir;
char *seinfo;
gid_list gids;
void *private_data;
bool profileable_from_shell;
long version_code;
};
/** Package uid like 10014. */
uid_t uid;
/** Package's AndroidManifest.xml debuggable flag. */
bool debuggable;
/** Package data directory like "/data/user/0/com.android.blah" */
char* data_dir;
/** Package SELinux info. */
char* seinfo;
/** Package's list of gids. */
gid_list gids;
/** Spare pointer for the caller to stash extra data off. */
void* private_data;
/** Package's AndroidManifest.xml profileable flag. */
bool profileable_from_shell;
/** Package's AndroidManifest.xml version code. */
long version_code;
} pkg_info;
/**
* Callback function to be used by packagelist_parse() routine.
* @param info
* The parsed package information
* @param userdata
* The supplied userdata pointer to packagelist_parse()
* @return
* true to keep processing, false to stop.
* Parses the system's default package list.
* Invokes `callback` once for each package.
* The callback owns the `pkg_info*` and should call packagelist_free().
* The callback should return `false` to exit early or `true` to continue.
*/
typedef bool (*pfn_on_package)(pkg_info *info, void *userdata);
bool packagelist_parse(bool (*callback)(pkg_info* info, void* user_data), void* user_data);
/**
* Parses the file specified by PACKAGES_LIST_FILE and invokes the callback on
* each entry found. Once the callback is invoked, ownership of the pkg_info pointer
* is passed to the callback routine, thus they are required to perform any cleanup
* desired.
* @param callback
* The callback function called on each parsed line of the packages list.
* @param userdata
* An optional userdata supplied pointer to pass to the callback function.
* @return
* true on success false on failure.
* Parses the given package list.
* Invokes `callback` once for each package.
* The callback owns the `pkg_info*` and should call packagelist_free().
* The callback should return `false` to exit early or `true` to continue.
*/
extern bool packagelist_parse(pfn_on_package callback, void *userdata);
bool packagelist_parse_file(const char* path, bool (*callback)(pkg_info* info, void* user_data),
void* user_data);
/**
* Frees a pkg_info structure.
* @param info
* The struct to free
*/
extern void packagelist_free(pkg_info *info);
/** Frees the given `pkg_info`. */
void packagelist_free(pkg_info* info);
__END_DECLS
#endif /* PACKAGELISTPARSER_H_ */

View file

@ -1,291 +0,0 @@
/*
* Copyright 2015, Intel Corporation
* Copyright (C) 2015 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.
*
* Written by William Roberts <william.c.roberts@intel.com>
*
*/
#define LOG_TAG "packagelistparser"
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/limits.h>
#include <log/log.h>
#include <packagelistparser/packagelistparser.h>
#define CLOGE(fmt, ...) \
do {\
IF_ALOGE() {\
ALOGE(fmt, ##__VA_ARGS__);\
}\
} while(0)
static size_t get_gid_cnt(const char *gids)
{
size_t cnt;
if (*gids == '\0') {
return 0;
}
if (!strcmp(gids, "none")) {
return 0;
}
for (cnt = 1; gids[cnt]; gids[cnt] == ',' ? cnt++ : *gids++)
;
return cnt;
}
static bool parse_gids(char *gids, gid_t *gid_list, size_t *cnt)
{
gid_t gid;
char* token;
char *endptr;
size_t cmp = 0;
while ((token = strsep(&gids, ",\r\n"))) {
if (cmp > *cnt) {
return false;
}
gid = strtoul(token, &endptr, 10);
if (*endptr != '\0') {
return false;
}
/*
* if unsigned long is greater than size of gid_t,
* prevent a truncation based roll-over
*/
if (gid > GID_MAX) {
CLOGE("A gid in field \"gid list\" greater than GID_MAX");
return false;
}
gid_list[cmp++] = gid;
}
return true;
}
extern bool packagelist_parse(pfn_on_package callback, void *userdata)
{
FILE *fp;
char *cur;
char *next;
char *endptr;
unsigned long tmp;
ssize_t bytesread;
bool rc = false;
char *buf = NULL;
size_t buflen = 0;
unsigned long lineno = 1;
const char *errmsg = NULL;
struct pkg_info *pkg_info = NULL;
fp = fopen(PACKAGES_LIST_FILE, "re");
if (!fp) {
CLOGE("Could not open: \"%s\", error: \"%s\"\n", PACKAGES_LIST_FILE,
strerror(errno));
return false;
}
while ((bytesread = getline(&buf, &buflen, fp)) > 0) {
pkg_info = calloc(1, sizeof(*pkg_info));
if (!pkg_info) {
goto err;
}
next = buf;
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for \"package name\"";
goto err;
}
pkg_info->name = strdup(cur);
if (!pkg_info->name) {
goto err;
}
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for field \"uid\"";
goto err;
}
tmp = strtoul(cur, &endptr, 10);
if (*endptr != '\0') {
errmsg = "Could not convert field \"uid\" to integer value";
goto err;
}
/*
* if unsigned long is greater than size of uid_t,
* prevent a truncation based roll-over
*/
if (tmp > UID_MAX) {
errmsg = "Field \"uid\" greater than UID_MAX";
goto err;
}
pkg_info->uid = (uid_t) tmp;
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for field \"debuggable\"";
goto err;
}
tmp = strtoul(cur, &endptr, 10);
if (*endptr != '\0') {
errmsg = "Could not convert field \"debuggable\" to integer value";
goto err;
}
/* should be a valid boolean of 1 or 0 */
if (!(tmp == 0 || tmp == 1)) {
errmsg = "Field \"debuggable\" is not 0 or 1 boolean value";
goto err;
}
pkg_info->debuggable = (bool) tmp;
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for field \"data dir\"";
goto err;
}
pkg_info->data_dir = strdup(cur);
if (!pkg_info->data_dir) {
goto err;
}
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for field \"seinfo\"";
goto err;
}
pkg_info->seinfo = strdup(cur);
if (!pkg_info->seinfo) {
goto err;
}
cur = strsep(&next, " \t\r\n");
if (!cur) {
errmsg = "Could not get next token for field \"gid(s)\"";
goto err;
}
/*
* Parse the gid list, could be in the form of none, single gid or list:
* none
* gid
* gid, gid ...
*/
pkg_info->gids.cnt = get_gid_cnt(cur);
if (pkg_info->gids.cnt > 0) {
pkg_info->gids.gids = calloc(pkg_info->gids.cnt, sizeof(gid_t));
if (!pkg_info->gids.gids) {
goto err;
}
rc = parse_gids(cur, pkg_info->gids.gids, &pkg_info->gids.cnt);
if (!rc) {
errmsg = "Could not parse field \"gid list\"";
goto err;
}
}
cur = strsep(&next, " \t\r\n");
if (cur) {
tmp = strtoul(cur, &endptr, 10);
if (*endptr != '\0') {
errmsg = "Could not convert field \"profileable_from_shell\" to integer value";
goto err;
}
/* should be a valid boolean of 1 or 0 */
if (!(tmp == 0 || tmp == 1)) {
errmsg = "Field \"profileable_from_shell\" is not 0 or 1 boolean value";
goto err;
}
pkg_info->profileable_from_shell = (bool)tmp;
}
cur = strsep(&next, " \t\r\n");
if (cur) {
tmp = strtoul(cur, &endptr, 10);
if (*endptr != '\0') {
errmsg = "Could not convert field \"versionCode\" to integer value";
goto err;
}
pkg_info->version_code = tmp;
}
rc = callback(pkg_info, userdata);
if (rc == false) {
/*
* We do not log this as this can be intentional from
* callback to abort processing. We go to out to not
* free the pkg_info
*/
rc = true;
goto out;
}
lineno++;
}
rc = true;
out:
free(buf);
fclose(fp);
return rc;
err:
if (errmsg) {
CLOGE("Error Parsing \"%s\" on line: %lu for reason: %s",
PACKAGES_LIST_FILE, lineno, errmsg);
}
rc = false;
packagelist_free(pkg_info);
goto out;
}
void packagelist_free(pkg_info *info)
{
if (info) {
free(info->name);
free(info->data_dir);
free(info->seinfo);
free(info->gids.gids);
free(info);
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (C) 2019 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.
*/
#define LOG_TAG "packagelistparser"
#include <packagelistparser/packagelistparser.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <sys/limits.h>
#include <memory>
#include <log/log.h>
static bool parse_gids(const char* path, size_t line_number, const char* gids, pkg_info* info) {
// Nothing to do?
if (!gids || !strcmp(gids, "none")) return true;
// How much space do we need?
info->gids.cnt = 1;
for (const char* p = gids; *p; ++p) {
if (*p == ',') ++info->gids.cnt;
}
// Allocate the space.
info->gids.gids = new gid_t[info->gids.cnt];
if (!info->gids.gids) return false;
// And parse the individual gids.
size_t i = 0;
while (true) {
char* end;
unsigned long gid = strtoul(gids, &end, 10);
if (gid > GID_MAX) {
ALOGE("%s:%zu: gid %lu > GID_MAX", path, line_number, gid);
return false;
}
if (i >= info->gids.cnt) return false;
info->gids.gids[i++] = gid;
if (*end == '\0') return true;
if (*end != ',') return false;
gids = end + 1;
}
return true;
}
static bool parse_line(const char* path, size_t line_number, const char* line, pkg_info* info) {
unsigned long uid;
int debuggable;
char* gid_list;
int profileable_from_shell = 0;
int fields =
sscanf(line, "%ms %lu %d %ms %ms %ms %d %ld", &info->name, &uid, &debuggable, &info->data_dir,
&info->seinfo, &gid_list, &profileable_from_shell, &info->version_code);
// Handle the more complicated gids field and free the temporary string.
bool gids_okay = parse_gids(path, line_number, gid_list, info);
free(gid_list);
if (!gids_okay) return false;
// Did we see enough fields to be getting on with?
// The final fields are optional (and not usually present).
if (fields < 6) {
ALOGE("%s:%zu: too few fields in line", path, line_number);
return false;
}
// Extra validation.
if (uid > UID_MAX) {
ALOGE("%s:%zu: uid %lu > UID_MAX", path, line_number, uid);
return false;
}
info->uid = uid;
// Integer to bool conversions.
if (debuggable != 0 && debuggable != 1) return false;
info->debuggable = debuggable;
if (profileable_from_shell != 0 && profileable_from_shell != 1) return false;
info->profileable_from_shell = profileable_from_shell;
return true;
}
bool packagelist_parse_file(const char* path, bool (*callback)(pkg_info*, void*), void* user_data) {
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(path, "re"), &fclose);
if (!fp) {
ALOGE("couldn't open '%s': %s", path, strerror(errno));
return false;
}
size_t line_number = 0;
char* line = nullptr;
size_t allocated_length = 0;
while (getline(&line, &allocated_length, fp.get()) > 0) {
++line_number;
std::unique_ptr<pkg_info, decltype(&packagelist_free)> info(
static_cast<pkg_info*>(calloc(1, sizeof(pkg_info))), &packagelist_free);
if (!info) {
ALOGE("%s:%zu: couldn't allocate pkg_info", path, line_number);
return false;
}
if (!parse_line(path, line_number, line, info.get())) return false;
if (!callback(info.release(), user_data)) break;
}
free(line);
return true;
}
bool packagelist_parse(bool (*callback)(pkg_info*, void*), void* user_data) {
return packagelist_parse_file("/data/system/packages.list", callback, user_data);
}
void packagelist_free(pkg_info* info) {
if (!info) return;
free(info->name);
free(info->data_dir);
free(info->seinfo);
delete[] info->gids.gids;
free(info);
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (C) 2019 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.
*/
#include <packagelistparser/packagelistparser.h>
#include <memory>
#include <android-base/file.h>
#include <gtest/gtest.h>
TEST(packagelistparser, smoke) {
TemporaryFile tf;
android::base::WriteStringToFile(
// No gids.
"com.test.a0 10014 0 /data/user/0/com.test.a0 platform:privapp:targetSdkVersion=19 none\n"
// One gid.
"com.test.a1 10007 1 /data/user/0/com.test.a1 platform:privapp:targetSdkVersion=21 1023\n"
// Multiple gids.
"com.test.a2 10011 0 /data/user/0/com.test.a2 media:privapp:targetSdkVersion=30 "
"2001,1065,1023,3003,3007,1024\n"
// The two new fields (profileable flag and version code).
"com.test.a3 10022 0 /data/user/0/com.test.a3 selabel:blah none 1 123\n",
tf.path);
std::vector<pkg_info*> packages;
packagelist_parse_file(
tf.path,
[](pkg_info* info, void* user_data) -> bool {
reinterpret_cast<std::vector<pkg_info*>*>(user_data)->push_back(info);
return true;
},
&packages);
ASSERT_EQ(4U, packages.size());
ASSERT_STREQ("com.test.a0", packages[0]->name);
ASSERT_EQ(10014, packages[0]->uid);
ASSERT_FALSE(packages[0]->debuggable);
ASSERT_STREQ("/data/user/0/com.test.a0", packages[0]->data_dir);
ASSERT_STREQ("platform:privapp:targetSdkVersion=19", packages[0]->seinfo);
ASSERT_EQ(0U, packages[0]->gids.cnt);
ASSERT_FALSE(packages[0]->profileable_from_shell);
ASSERT_EQ(0, packages[0]->version_code);
ASSERT_STREQ("com.test.a1", packages[1]->name);
ASSERT_EQ(10007, packages[1]->uid);
ASSERT_TRUE(packages[1]->debuggable);
ASSERT_STREQ("/data/user/0/com.test.a1", packages[1]->data_dir);
ASSERT_STREQ("platform:privapp:targetSdkVersion=21", packages[1]->seinfo);
ASSERT_EQ(1U, packages[1]->gids.cnt);
ASSERT_EQ(1023U, packages[1]->gids.gids[0]);
ASSERT_FALSE(packages[0]->profileable_from_shell);
ASSERT_EQ(0, packages[0]->version_code);
ASSERT_STREQ("com.test.a2", packages[2]->name);
ASSERT_EQ(10011, packages[2]->uid);
ASSERT_FALSE(packages[2]->debuggable);
ASSERT_STREQ("/data/user/0/com.test.a2", packages[2]->data_dir);
ASSERT_STREQ("media:privapp:targetSdkVersion=30", packages[2]->seinfo);
ASSERT_EQ(6U, packages[2]->gids.cnt);
ASSERT_EQ(2001U, packages[2]->gids.gids[0]);
ASSERT_EQ(1024U, packages[2]->gids.gids[5]);
ASSERT_FALSE(packages[0]->profileable_from_shell);
ASSERT_EQ(0, packages[0]->version_code);
ASSERT_STREQ("com.test.a3", packages[3]->name);
ASSERT_EQ(10022, packages[3]->uid);
ASSERT_FALSE(packages[3]->debuggable);
ASSERT_STREQ("/data/user/0/com.test.a3", packages[3]->data_dir);
ASSERT_STREQ("selabel:blah", packages[3]->seinfo);
ASSERT_EQ(0U, packages[3]->gids.cnt);
ASSERT_TRUE(packages[3]->profileable_from_shell);
ASSERT_EQ(123, packages[3]->version_code);
for (auto& package : packages) packagelist_free(package);
}
TEST(packagelistparser, early_exit) {
TemporaryFile tf;
android::base::WriteStringToFile(
"com.test.a0 1 0 / a none\n"
"com.test.a1 1 0 / a none\n"
"com.test.a2 1 0 / a none\n",
tf.path);
std::vector<pkg_info*> packages;
packagelist_parse_file(
tf.path,
[](pkg_info* info, void* user_data) -> bool {
std::vector<pkg_info*>* p = reinterpret_cast<std::vector<pkg_info*>*>(user_data);
p->push_back(info);
return p->size() < 2;
},
&packages);
ASSERT_EQ(2U, packages.size());
ASSERT_STREQ("com.test.a0", packages[0]->name);
ASSERT_STREQ("com.test.a1", packages[1]->name);
for (auto& package : packages) packagelist_free(package);
}
TEST(packagelistparser, system_package_list) {
// Check that we can actually read the packages.list installed on the device.
std::vector<pkg_info*> packages;
packagelist_parse(
[](pkg_info* info, void* user_data) -> bool {
reinterpret_cast<std::vector<pkg_info*>*>(user_data)->push_back(info);
return true;
},
&packages);
// Not much we can say for sure about what we expect, other than that there
// are likely to be lots of packages...
ASSERT_GT(packages.size(), 10U);
}
TEST(packagelistparser, packagelist_free_nullptr) {
packagelist_free(nullptr);
}