1b62286e60c079936a5f1e5b0befd32d2d9bf1b3fMark Salyzyn#include <dirent.h> 24600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath#include <inttypes.h> 3b62286e60c079936a5f1e5b0befd32d2d9bf1b3fMark Salyzyn#include <sys/stat.h> 4b62286e60c079936a5f1e5b0befd32d2d9bf1b3fMark Salyzyn 513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include "idmap.h" 613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 70881a7445f9106e835486c5a697c01f4f18e619bDan Albert#include <memory> 813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <androidfw/ResourceTypes.h> 913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <androidfw/StreamingZipInflater.h> 1013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <androidfw/ZipFileRO.h> 1113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <private/android_filesystem_config.h> // for AID_SYSTEM 1213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <utils/SortedVector.h> 1313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <utils/String16.h> 1413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#include <utils/String8.h> 1513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 1613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross#define NO_OVERLAY_TAG (-1000) 1713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 1813221c9cffdb289851411bdc73a9a5236fcb9291Colin Crossusing namespace android; 1913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 2013221c9cffdb289851411bdc73a9a5236fcb9291Colin Crossnamespace { 2113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross struct Overlay { 2213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross Overlay() {} 2313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross Overlay(const String8& a, const String8& i, int p) : 2413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross apk_path(a), idmap_path(i), priority(p) {} 2513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 2613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross bool operator<(Overlay const& rhs) const 2713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 287197d91eb7dfbd0f5b065b56d4f391019e71d03aMårten Kongstad return rhs.priority > priority; 2913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 3013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 3113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String8 apk_path; 3213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String8 idmap_path; 3313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross int priority; 3413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross }; 3513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 3613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross bool writePackagesList(const char *filename, const SortedVector<Overlay>& overlayVector) 3713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 3813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross FILE* fout = fopen(filename, "w"); 3913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (fout == NULL) { 4013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return false; 4113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 4213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 4313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross for (size_t i = 0; i < overlayVector.size(); ++i) { 4413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross const Overlay& overlay = overlayVector[i]; 4513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross fprintf(fout, "%s %s\n", overlay.apk_path.string(), overlay.idmap_path.string()); 4613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 4713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 4813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross fclose(fout); 4913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 5013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross // Make file world readable since Zygote (running as root) will read 5113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross // it when creating the initial AssetManger object 5213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross const mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; // 0644 5313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (chmod(filename, mode) == -1) { 5413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross unlink(filename); 5513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return false; 5613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 5713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 5813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return true; 5913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 6013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 6113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String8 flatten_path(const char *path) 6213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 6313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String16 tmp(path); 6413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross tmp.replaceAll('/', '@'); 6513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return String8(tmp); 6613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 6713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 6813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross int parse_overlay_tag(const ResXMLTree& parser, const char *target_package_name) 6913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 7013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross const size_t N = parser.getAttributeCount(); 7113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String16 target; 7213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross int priority = -1; 7313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross for (size_t i = 0; i < N; ++i) { 7413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross size_t len; 7513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String16 key(parser.getAttributeName(i, &len)); 7613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (key == String16("targetPackage")) { 776698749dd4d4d6513b26aa9071af290b956b68a7Dan Albert const char16_t *p = parser.getAttributeStringValue(i, &len); 78cfedceb8c180a2e176154d461659e0c3569dc931Andreas Gampe if (p != NULL) { 7913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross target = String16(p, len); 8013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 8113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } else if (key == String16("priority")) { 8213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross Res_value v; 8313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (parser.getAttributeValue(i, &v) == sizeof(Res_value)) { 8413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross priority = v.data; 8513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (priority < 0 || priority > 9999) { 8613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 8713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 8813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 8913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 9013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 9113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (target == String16(target_package_name)) { 9213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return priority; 9313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 9413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return NO_OVERLAY_TAG; 9513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 9613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 9713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross int parse_manifest(const void *data, size_t size, const char *target_package_name) 9813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 99de898ff42912bd7ca1bfb099cd439562496765a4Adam Lesinski ResXMLTree parser; 100de898ff42912bd7ca1bfb099cd439562496765a4Adam Lesinski parser.setTo(data, size); 10113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (parser.getError() != NO_ERROR) { 10213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ALOGD("%s failed to init xml parser, error=0x%08x\n", __FUNCTION__, parser.getError()); 10313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 10413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 10513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 10613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ResXMLParser::event_code_t type; 10713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross do { 10813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross type = parser.next(); 10913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (type == ResXMLParser::START_TAG) { 11013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross size_t len; 11113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String16 tag(parser.getElementName(&len)); 11213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (tag == String16("overlay")) { 11313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return parse_overlay_tag(parser, target_package_name); 11413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 11513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 11613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } while (type != ResXMLParser::BAD_DOCUMENT && type != ResXMLParser::END_DOCUMENT); 11713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 11813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return NO_OVERLAY_TAG; 11913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 12013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 12113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross int parse_apk(const char *path, const char *target_package_name) 12213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross { 1230881a7445f9106e835486c5a697c01f4f18e619bDan Albert std::unique_ptr<ZipFileRO> zip(ZipFileRO::open(path)); 12413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (zip.get() == NULL) { 12513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ALOGW("%s: failed to open zip %s\n", __FUNCTION__, path); 12613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 12713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 12813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ZipEntryRO entry; 12913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if ((entry = zip->findEntryByName("AndroidManifest.xml")) == NULL) { 13013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ALOGW("%s: failed to find entry AndroidManifest.xml\n", __FUNCTION__); 13113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 13213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 1334600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath uint32_t uncompLen = 0; 1344600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath uint16_t method; 13513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (!zip->getEntryInfo(entry, &method, &uncompLen, NULL, NULL, NULL, NULL)) { 13613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ALOGW("%s: failed to read entry info\n", __FUNCTION__); 13713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 13813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 13913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (method != ZipFileRO::kCompressDeflated) { 1404600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath ALOGW("%s: cannot handle zip compression method %" PRIu16 "\n", __FUNCTION__, method); 14113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 14213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 14313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross FileMap *dataMap = zip->createEntryFileMap(entry); 144cfedceb8c180a2e176154d461659e0c3569dc931Andreas Gampe if (dataMap == NULL) { 14513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross ALOGW("%s: failed to create FileMap\n", __FUNCTION__); 14613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 14713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 14813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross char *buf = new char[uncompLen]; 14913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (NULL == buf) { 1504600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath ALOGW("%s: failed to allocate %" PRIu32 " byte\n", __FUNCTION__, uncompLen); 151688ff4cf0f93d1a886437884f23874e5595d59a3Narayan Kamath delete dataMap; 15213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 15313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 15413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross StreamingZipInflater inflater(dataMap, uncompLen); 15513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (inflater.read(buf, uncompLen) < 0) { 1564600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath ALOGW("%s: failed to inflate %" PRIu32 " byte\n", __FUNCTION__, uncompLen); 15713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross delete[] buf; 158688ff4cf0f93d1a886437884f23874e5595d59a3Narayan Kamath delete dataMap; 15913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return -1; 16013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 16113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 1624600dd053dbdbd4b95f3b11057a1cc55b99f9c77Narayan Kamath int priority = parse_manifest(buf, static_cast<size_t>(uncompLen), target_package_name); 16313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross delete[] buf; 164688ff4cf0f93d1a886437884f23874e5595d59a3Narayan Kamath delete dataMap; 16513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return priority; 16613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 16713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross} 16813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 16983ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstadint idmap_scan(const char *target_package_name, const char *target_apk_path, 17083ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad const char *idmap_dir, const android::Vector<const char *> *overlay_dirs) 17113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross{ 17213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross String8 filename = String8(idmap_dir); 17313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross filename.appendPath("overlays.list"); 17413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (unlink(filename.string()) != 0 && errno != ENOENT) { 17513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return EXIT_FAILURE; 17613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 17713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 17813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross SortedVector<Overlay> overlayVector; 17983ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad const size_t N = overlay_dirs->size(); 18083ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad for (size_t i = 0; i < N; ++i) { 18183ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad const char *overlay_dir = overlay_dirs->itemAt(i); 18283ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad DIR *dir = opendir(overlay_dir); 18383ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad if (dir == NULL) { 18483ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad return EXIT_FAILURE; 18583ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad } 18683ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad 18783ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad struct dirent *dirent; 18883ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad while ((dirent = readdir(dir)) != NULL) { 18983ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad struct stat st; 19083ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad char overlay_apk_path[PATH_MAX + 1]; 19183ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad snprintf(overlay_apk_path, PATH_MAX, "%s/%s", overlay_dir, dirent->d_name); 19283ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad if (stat(overlay_apk_path, &st) < 0) { 19383ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad continue; 19483ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad } 19583ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad if (!S_ISREG(st.st_mode)) { 19683ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad continue; 19783ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad } 19813221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 19983ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad int priority = parse_apk(overlay_apk_path, target_package_name); 20083ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad if (priority < 0) { 20183ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad continue; 20283ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad } 20313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 20483ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad String8 idmap_path(idmap_dir); 20583ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad idmap_path.appendPath(flatten_path(overlay_apk_path + 1)); 20683ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad idmap_path.append("@idmap"); 20713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 20883ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad if (idmap_create_path(target_apk_path, overlay_apk_path, idmap_path.string()) != 0) { 20983ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad ALOGE("error: failed to create idmap for target=%s overlay=%s idmap=%s\n", 21083ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad target_apk_path, overlay_apk_path, idmap_path.string()); 21183ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad continue; 21283ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad } 21383ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad 21483ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad Overlay overlay(String8(overlay_apk_path), idmap_path, priority); 21583ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad overlayVector.add(overlay); 21613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 21713221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 21883ab0d7c1824f3a29e41af8a95a954c73f48c287Mårten Kongstad closedir(dir); 21913221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 22013221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 22113221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross if (!writePackagesList(filename.string(), overlayVector)) { 22213221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return EXIT_FAILURE; 22313221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross } 22413221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross 22513221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross return EXIT_SUCCESS; 22613221c9cffdb289851411bdc73a9a5236fcb9291Colin Cross} 227