From cf5b17055b1276c187537cf5a054cf6ebe753113 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn <> Date: Tue, 24 Mar 2009 18:36:43 -0700 Subject: [PATCH 01/24] Automated import from //branches/donutburger/...@140818,140818 --- NOTICE | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + From bc012de46e99fc936e6c06d19416b8414835c7b6 Mon Sep 17 00:00:00 2001 From: Doug Zongker <> Date: Tue, 24 Mar 2009 21:30:32 -0700 Subject: [PATCH 02/24] Automated import from //branches/donutburger/...@142141,142141 --- tools/ota/make-update-script.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/ota/make-update-script.c b/tools/ota/make-update-script.c index 225dc52..1e1148b 100644 --- a/tools/ota/make-update-script.c +++ b/tools/ota/make-update-script.c @@ -185,16 +185,21 @@ int main(int argc, char *argv[]) { } // The lines we're looking for look like: - // version-bootloader=x.yy.zzzz + // version-bootloader=x.yy.zzzz|x.yy.zzzz|... // or: - // require version-bootloader=x.yy.zzzz + // require version-bootloader=x.yy.zzzz|x.yy.zzzz|... char line[256]; while (fgets(line, sizeof(line), fp)) { - const char *name = strtok(line, "="), *value = strtok(NULL, "\n"); + const char *name = strtok(line, "="), *value = strtok(NULL, "|\n"); if (value != NULL && (!strcmp(name, "version-bootloader") || !strcmp(name, "require version-bootloader"))) { - printf("assert getprop(\"ro.bootloader\") == \"%s\"\n", value); + printf("assert getprop(\"ro.bootloader\") == \"%s\"", value); + + while ((value = strtok(NULL, "|\n")) != NULL) { + printf(" || getprop(\"ro.bootloader\") == \"%s\"", value); + } + printf("\n"); } // We also used to check version-baseband, but we update radio.img // ourselves, so there's no need. From 58bde316e22e392885de71d2391f2bc7f438ff1f Mon Sep 17 00:00:00 2001 From: Doug Zongker <> Date: Fri, 27 Mar 2009 13:25:30 -0700 Subject: [PATCH 03/24] AI 143128: Use PNG instead of BMP for recovery image icons. This saves about 60k from the recovery and system images. Automated import of CL 143128 --- Android.mk | 2 +- minui/resources.c | 167 +++++++----------- res/images/icon_error.bmp | Bin 91076 -> 0 bytes res/images/icon_error.png | Bin 0 -> 9616 bytes res/images/icon_firmware_error.bmp | Bin 91076 -> 0 bytes res/images/icon_firmware_error.png | Bin 0 -> 8088 bytes res/images/icon_firmware_install.bmp | Bin 91076 -> 0 bytes res/images/icon_firmware_install.png | Bin 0 -> 11986 bytes res/images/icon_installing.bmp | Bin 91076 -> 0 bytes res/images/icon_installing.png | Bin 0 -> 10138 bytes res/images/icon_unpacking.bmp | Bin 91076 -> 0 bytes res/images/icon_unpacking.png | Bin 0 -> 7180 bytes res/images/indeterminate1.bmp | Bin 20214 -> 0 bytes res/images/indeterminate1.png | Bin 0 -> 2249 bytes res/images/indeterminate2.bmp | Bin 20214 -> 0 bytes res/images/indeterminate2.png | Bin 0 -> 2251 bytes res/images/indeterminate3.bmp | Bin 20214 -> 0 bytes res/images/indeterminate3.png | Bin 0 -> 2254 bytes res/images/indeterminate4.bmp | Bin 20214 -> 0 bytes res/images/indeterminate4.png | Bin 0 -> 2249 bytes res/images/indeterminate5.bmp | Bin 20214 -> 0 bytes res/images/indeterminate5.png | Bin 0 -> 2246 bytes res/images/indeterminate6.bmp | Bin 20214 -> 0 bytes res/images/indeterminate6.png | Bin 0 -> 2262 bytes res/images/progress_bar_empty.bmp | Bin 136 -> 0 bytes res/images/progress_bar_empty.png | Bin 0 -> 148 bytes res/images/progress_bar_empty_left_round.bmp | Bin 294 -> 0 bytes res/images/progress_bar_empty_left_round.png | Bin 0 -> 220 bytes res/images/progress_bar_empty_right_round.bmp | Bin 294 -> 0 bytes res/images/progress_bar_empty_right_round.png | Bin 0 -> 211 bytes res/images/progress_bar_fill.bmp | Bin 136 -> 0 bytes res/images/progress_bar_fill.png | Bin 0 -> 117 bytes res/images/progress_bar_left_round.bmp | Bin 294 -> 0 bytes res/images/progress_bar_left_round.png | Bin 0 -> 195 bytes res/images/progress_bar_right_round.bmp | Bin 294 -> 0 bytes res/images/progress_bar_right_round.png | Bin 0 -> 192 bytes 36 files changed, 68 insertions(+), 101 deletions(-) delete mode 100644 res/images/icon_error.bmp create mode 100644 res/images/icon_error.png delete mode 100644 res/images/icon_firmware_error.bmp create mode 100644 res/images/icon_firmware_error.png delete mode 100644 res/images/icon_firmware_install.bmp create mode 100644 res/images/icon_firmware_install.png delete mode 100644 res/images/icon_installing.bmp create mode 100644 res/images/icon_installing.png delete mode 100644 res/images/icon_unpacking.bmp create mode 100644 res/images/icon_unpacking.png delete mode 100644 res/images/indeterminate1.bmp create mode 100644 res/images/indeterminate1.png delete mode 100644 res/images/indeterminate2.bmp create mode 100644 res/images/indeterminate2.png delete mode 100644 res/images/indeterminate3.bmp create mode 100644 res/images/indeterminate3.png delete mode 100644 res/images/indeterminate4.bmp create mode 100644 res/images/indeterminate4.png delete mode 100644 res/images/indeterminate5.bmp create mode 100644 res/images/indeterminate5.png delete mode 100644 res/images/indeterminate6.bmp create mode 100644 res/images/indeterminate6.png delete mode 100644 res/images/progress_bar_empty.bmp create mode 100644 res/images/progress_bar_empty.png delete mode 100644 res/images/progress_bar_empty_left_round.bmp create mode 100644 res/images/progress_bar_empty_left_round.png delete mode 100644 res/images/progress_bar_empty_right_round.bmp create mode 100644 res/images/progress_bar_empty_right_round.png delete mode 100644 res/images/progress_bar_fill.bmp create mode 100644 res/images/progress_bar_fill.png delete mode 100644 res/images/progress_bar_left_round.bmp create mode 100644 res/images/progress_bar_left_round.png delete mode 100644 res/images/progress_bar_right_round.bmp create mode 100644 res/images/progress_bar_right_round.png diff --git a/Android.mk b/Android.mk index 816d143..6198ab3 100644 --- a/Android.mk +++ b/Android.mk @@ -30,7 +30,7 @@ LOCAL_FORCE_STATIC_EXECUTABLE := true LOCAL_MODULE_TAGS := eng LOCAL_STATIC_LIBRARIES := libminzip libunz libamend libmtdutils libmincrypt -LOCAL_STATIC_LIBRARIES += libminui libpixelflinger_static libcutils +LOCAL_STATIC_LIBRARIES += libminui libpixelflinger_static libpng libcutils LOCAL_STATIC_LIBRARIES += libstdc++ libc # Specify a C-includable file containing the OTA public keys. diff --git a/minui/resources.c b/minui/resources.c index 5beb6a6..7ecfeef 100644 --- a/minui/resources.c +++ b/minui/resources.c @@ -29,97 +29,84 @@ #include +#include + #include "minui.h" -// File signature for BMP files. -// The letters 'BM' as a little-endian unsigned short. - -#define BMP_SIGNATURE 0x4d42 - -typedef struct { - // constant, value should equal BMP_SIGNATURE - unsigned short bfType; - // size of the file in bytes. - unsigned long bfSize; - // must always be set to zero. - unsigned short bfReserved1; - // must always be set to zero. - unsigned short bfReserved2; - // offset from the beginning of the file to the bitmap data. - unsigned long bfOffBits; - - // The BITMAPINFOHEADER: - // size of the BITMAPINFOHEADER structure, in bytes. - unsigned long biSize; - // width of the image, in pixels. - unsigned long biWidth; - // height of the image, in pixels. - unsigned long biHeight; - // number of planes of the target device, must be set to 1. - unsigned short biPlanes; - // number of bits per pixel. - unsigned short biBitCount; - // type of compression, zero means no compression. - unsigned long biCompression; - // size of the image data, in bytes. If there is no compression, - // it is valid to set this member to zero. - unsigned long biSizeImage; - // horizontal pixels per meter on the designated targer device, - // usually set to zero. - unsigned long biXPelsPerMeter; - // vertical pixels per meter on the designated targer device, - // usually set to zero. - unsigned long biYPelsPerMeter; - // number of colors used in the bitmap, if set to zero the - // number of colors is calculated using the biBitCount member. - unsigned long biClrUsed; - // number of color that are 'important' for the bitmap, - // if set to zero, all colors are important. - unsigned long biClrImportant; -} __attribute__((packed)) BitMapFileHeader; +// libpng gives "undefined reference to 'pow'" errors, and I have no +// idea how to convince the build system to link with -lm. We don't +// need this functionality (it's used for gamma adjustment) so provide +// a dummy implementation to satisfy the linker. +double pow(double x, double y) { + return x; +} int res_create_surface(const char* name, gr_surface* pSurface) { char resPath[256]; - BitMapFileHeader header; GGLSurface* surface = NULL; int result = 0; - - snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.bmp", name); + unsigned char header[8]; + png_structp png_ptr = NULL; + png_infop info_ptr = NULL; + + snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.png", name); resPath[sizeof(resPath)-1] = '\0'; - int fd = open(resPath, O_RDONLY); - if (fd == -1) { + FILE* fp = fopen(resPath, "rb"); + if (fp == NULL) { result = -1; goto exit; } - size_t bytesRead = read(fd, &header, sizeof(header)); + + size_t bytesRead = fread(header, 1, sizeof(header), fp); if (bytesRead != sizeof(header)) { result = -2; goto exit; } - if (header.bfType != BMP_SIGNATURE) { - result = -3; // Not a legal header + + if (png_sig_cmp(header, 0, sizeof(header))) { + result = -3; goto exit; } - if (header.biPlanes != 1) { + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_ptr) { result = -4; goto exit; } - if (!(header.biBitCount == 24 || header.biBitCount == 32)) { + + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { result = -5; goto exit; } - if (header.biCompression != 0) { + + if (setjmp(png_jmpbuf(png_ptr))) { result = -6; goto exit; } - size_t width = header.biWidth; - size_t height = header.biHeight; + + png_init_io(png_ptr, fp); + png_set_sig_bytes(png_ptr, sizeof(header)); + png_read_info(png_ptr, info_ptr); + + size_t width = info_ptr->width; + size_t height = info_ptr->height; size_t stride = 4 * width; size_t pixelSize = stride * height; - + + int color_type = info_ptr->color_type; + int bit_depth = info_ptr->bit_depth; + int channels = info_ptr->channels; + if (bit_depth != 8 || (channels != 3 && channels != 4) || + (color_type != PNG_COLOR_TYPE_RGB && + color_type != PNG_COLOR_TYPE_RGBA)) { + return -7; + goto exit; + } + surface = malloc(sizeof(GGLSurface) + pixelSize); if (surface == NULL) { - result = -7; + result = -8; goto exit; } unsigned char* pData = (unsigned char*) (surface + 1); @@ -128,63 +115,43 @@ int res_create_surface(const char* name, gr_surface* pSurface) { surface->height = height; surface->stride = width; /* Yes, pixels, not bytes */ surface->data = pData; - surface->format = (header.biBitCount == 24) ? + surface->format = (channels == 3) ? GGL_PIXEL_FORMAT_RGBX_8888 : GGL_PIXEL_FORMAT_RGBA_8888; - // Source pixel bytes are stored B G R {A} - - lseek(fd, header.bfOffBits, SEEK_SET); - size_t y; - if (header.biBitCount == 24) { // RGB - size_t inputStride = (((3 * width + 3) >> 2) << 2); - for (y = 0; y < height; y++) { - unsigned char* pRow = pData + (height - (y + 1)) * stride; - bytesRead = read(fd, pRow, inputStride); - if (bytesRead != inputStride) { - result = -8; - goto exit; - } + int y; + if (channels == 3) { + for (y = 0; y < height; ++y) { + unsigned char* pRow = pData + y * stride; + png_read_row(png_ptr, pRow, NULL); + int x; for(x = width - 1; x >= 0; x--) { int sx = x * 3; int dx = x * 4; - unsigned char b = pRow[sx]; + unsigned char r = pRow[sx]; unsigned char g = pRow[sx + 1]; - unsigned char r = pRow[sx + 2]; + unsigned char b = pRow[sx + 2]; unsigned char a = 0xff; pRow[dx ] = r; // r pRow[dx + 1] = g; // g - pRow[dx + 2] = b; // b; + pRow[dx + 2] = b; // b pRow[dx + 3] = a; } } - } else { // RGBA - for (y = 0; y < height; y++) { - unsigned char* pRow = pData + (height - (y + 1)) * stride; - bytesRead = read(fd, pRow, stride); - if (bytesRead != stride) { - result = -9; - goto exit; - } - size_t x; - for(x = 0; x < width; x++) { - size_t xx = x * 4; - unsigned char b = pRow[xx]; - unsigned char g = pRow[xx + 1]; - unsigned char r = pRow[xx + 2]; - unsigned char a = pRow[xx + 3]; - pRow[xx ] = r; - pRow[xx + 1] = g; - pRow[xx + 2] = b; - pRow[xx + 3] = a; - } + } else { + for (y = 0; y < height; ++y) { + unsigned char* pRow = pData + y * stride; + png_read_row(png_ptr, pRow, NULL); } } + *pSurface = (gr_surface) surface; exit: - if (fd >= 0) { - close(fd); + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + + if (fp != NULL) { + fclose(fp); } if (result < 0) { if (surface) { diff --git a/res/images/icon_error.bmp b/res/images/icon_error.bmp deleted file mode 100644 index 7eb2bbc778f7fe98f15ffb3438ecd191196a3416..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91076 zcmeI52YeM(*1*->wE*ghxacad3j#_Pl@ZW1!+=5mu=sE`+fhHtJh)Xy~%wg5R!SnA;00hc~kEHopaAU_ndP-^vUS? z)9@7(tJh@ij%1qwXW8?r8z4J`n~p543e z+r10Kj#%&beCwK(E9%#)mkrmqPWg~pph5jlHmqHPy3UgI_N`mCZP`3EVoHe;#UIkm zS(oup3skCDF*bT0`WY>)&U)*njaxQu=-jDef&9!cSg)Mk0)-0~Mm)D|-h`;S#(MLH z_1JIKt5wVCkD7rLy|q5LuD3jgU(dALP$=%Y7n(j+YgZ zCajMfJn+rAb2o3^xcB3aY~NqL{Pl?w$Ue52=&ywi=L~}N`ZcRllb1AY;*&$4Iq%(8 zwW?L(W1`osUbSJ(Y7^F%E?l^N{rZ;8nE6B5C%<;Yn$xab zwQ_pY)N$`UK>brODFd{R`wP3w^#qw3l(|dOBMmLdzo8VRiSnt}l?ee$Zo;r1E@`MRJx_0Z)wW~Sn{(brk z=+h^0;lkUuZ+~&*i0rR*tXHHhgEcj?-_OBXHc5rKgbQ>M^aj~g>a=x`y|g7tvDeTNMly6yANZ(P5=)PBLMAaNoXLw{ERSPw(2PQ@75Y zb*%k|3|XytMMgx>U+Y*8?Av!xzkV~KqAp&zaPG_*6V{mMGpA20^Y+`1rbTk&RSOj= z*0Zc8q}-j%%1ItHMj@&@4tTS+UwV@ z?akONY%}_6p~Hn-!#ZVg61U{qxtKfGtkmoHzlrcDy($1hqi--IX zyKlcmpF3i`WKm-5yt&rYiOatXuNpOGe>P)be4IJ!Q(u35_51IqO`Y1Qef!QGI_g+Q zO`R$QIVB|pd)tv6F8gbt!-ZVKnqivnz=3hm(bunEJ96ldUAcz!;>3hS3m1$Z6JE4v zQI{V%4}P(9sZx_Cd^$fSdO=*Q3G00syMMTOGkw{zj_ulYYTv;w>-q8Vk~KR4enVYj zJ!IfOjJl2M*K@@U>(=UYxMYnNJz;))hjwkzNO=&PQ;(eQx#tG->l-(3ZhZ7S6V{A> zZ{E1EbH|QeJ$iI#*G{lTSARNg+=TJt{r&u;4)+^8cu`^^l+)AGahI8lug&Ee)_#Kq zakj`wlg^(zck-*R(C4zh*0JXIIcvuBcPdqK>e=(!!QHxaj-5NlgmwDTl#Abd^YzzX z4-X1z-=|;*!urc&$F5wt!qB)~ z>o)D$v=OX_3?6*{{(TMSUv_zu!Ln}5<%so!apUNV&z?Tbz#IbrvHj}HFPAJ@G|bOW z^w&Dp{=U8=M~pah;J}s3mshV?;fQry^gL`fpGJ)oA3yILR^*L09I<8|>c)-hsY{o( zY1OK2>(*MsPOC-?pt=mo9Bu zwluOHH+HNe)_A}K>n;gxrSg`YU;NaFVZPEVtxFJFOD5KvVP5)F=1h% zzZNe!UJi3OqJCTJ+a-YJX6oMCoG1O0cn0y;_Z?K273dV-aLV z%_^*q9zJ~J;KBJZF+lAQFC)ah`^8v7i#>hNI!ufP6!_O+z5XYSUm8@la(GhhGt<@GEi6!RE`&{H*eU0TQWW_4z1gg&A^(0m3GMNH)M!$eBHKHn|CX}FZs%y@#7~jV!HM{ z8rK!pINJ~G-AlI;796bA;ga?60RQmN(2QNXu3f#lGl4S1!?9!g_RX6%b;NpfSeTae z!2bPptZRAKmP%Vv9yG`oS0|%#CKunVbiki z)2r9CsHoVOm?@Jdck0jqJDeW;Q$%$9_})EwN?(fbo7MZcp5!Zb=;ya=-omuV4-CRR z#Cq@U-NYepTEBkc`0?0gM%E*S4GRt&wszGjF1cpqN=K}tBBo58GP!@>J}hOs#XXcgA~%6vseM*Q)Qe>6Y8;-^i0e2lEq($b_L$HvC$##}nq4eK|M ztgBb|PE1Vtk-!upkg|dG?j1XJZQr(Z@#2u6Akkl=!-ZT6*1>^+V@HoVa`-T&etb*} zwwdU!g$_rq1?$MklL&tJxNcp=R&wXC*Z=(IQ6ob>i<$~+e*4a{{@1_$Nz7j7&Yj)4 z2k~8=*G<;UeN?Xeez|g$h)1}3^(t|L+0B}e25bzbX|T8H`S_D%h82Y-hb`Y zSB4r^Ns>gLT%fsG3vt&?lPdgSn+kt2q0 z+p>jzBzbX?QLY8+NuQ3NIDTBq=FOgc_F0wYa`u=3efywE1nYIHSG&Xdpa1*^ub7ya z7hinQ+1rrE6PdsWRNG(c##|;k9N);@yLb8B{p0U{fB3AoZrVhC5VN15BZf;Iu4NrM zV#L(RlhNnIv(247ORK}RtP#D?;1M-Eopn;8c=4%|CrQ=^_U&_@HMp^L2Mrn|@wu?` zW{DEGk?oGT?6QuEjHDl7jDQ94Xjx;h+2Y9~BeBisuXU`$f=967X{Tpdy>f-@uXU`M z2S-J8@7l$Kx#4YFwaO0G|3I$({`dX+_t&cB?E+}=l|TI95ARj3Y#d)3Su@XNvn2oX zpB%!Pe1r{a)-WLw9u{hs^{9~G31i3X+q38Lci+X%n}=;?mo;`BXQ<;|w@Q7Rs);8Yv?-LDDtXMI5MQ3-2U9I2D$U11)upG!*1SMj7j|vG9{WUsV z$Th5O+sF`{B zI?d|&SlSpi@>eY?&T*3}# zMx$4+z85cE$_f3o(BYCbX(S<_Q>J`mWYo=@Hy@!Lj%{X_^@?T7 zh*_FFV>-B{4wtOym&b&K6062AoEcJ`TreN6jQ$BX`2$9n9jFgBd+mo8n5nLAhNaC&xirjCx(+2OAFNL^r!ljG~JzG~dK zv7;Ny@_q6py}i6dyqT6YelxUs)hg8pU^#R8%)|7hi1BrntVs?cZzX#6EaD&RvPRX< zoECNI;>G=Y_oCbBuRX%ryQY^;hofXr#X^n$D%N)9x!ja%SpWQ!?X_;*I(Jk$MGtVH z>sX6i08;PEmaQ;p(iFxu+3F9+M=JbgPO&CaX>r0rhT(M9wk=bK3%Q2%xY4$aNVaR& zt`W!R5!N_JjjW{_e+aC9`sp5`H*w-bcT{?Xa^>2zY-Kk-yzYk|d3)Dcp1wT$=003z zoszUDc~Rnm*qDjq#@b~)K72F=JaJH%=vgW{l>-bU>eE=qdhX26aBQpCW@J&3r+nnl!5@G8G1E9xwaugs zm#o3fjMUn-Ys!}|@9ZPUK9-LZW;cMxm88!*&Q%yS7Hj$C6as<39vg;2wq5#`3U zYYDx>z!32Sl0I>2uTMFz|t)2UBU<3`pb)TppVaCNLj-3jW3(HFbP znz0hmL$hYha(P^aN`I$fMQsfYZJuW1hK;$Laqhtb*om=i4$p`sl{#naHn**kZZvj z+?2BzK0L^&AF%ddRC_<8^W^Sc9A3`7aJ1W?aVHhl(AJZKjiJ!5V)ETVi5D z(V|6MYXnq!<#*oE)v}Ydahf#tL8Z@}@tMfe^b~6-5xA6}iTj(j_y9l;CX)c&Wp+tSS1(9N)P!r`=~w@GsqKP*9M|We>HC0boHuL?z0XZIkIZi8jTzI5J2u|{(8uuL6s_2kQ5%yPSy9{ z*RdvNfe>PFo69wO<;%s##@J=OGHn^Lwfpz%!NSg*{jmANZBDaHznSo*nCo!K zn)q9`l`B>hDdM_HCtOo2*QDpm+Bl4cDpssabseWzhmRUvt!j-%4I49Daui!gxdt3Q zF8@kB^2I(qdq_LnmI^&@E=diFtc|$ zk0vlM&}Efg=;fE2Hg0UUUJ7NhKB)6yqlP|d%a&z1z8)JsrdqWcjN|-=_&SOiM$Bt_ z*YpT1H&0TnN);XJ_~>XFGfN$gTuU{sy$5EQu{ywNWDRbLOj6~BiCTB$TCfK9xwB{6 zwQZ}tx>Ngal`6&fODos1UJ4a>-mU!pzySldfBrdnnnu4F`To_ac{OU#aH#KKN4Wq2 zetxxT*370Lf2W>Q9E=(_vPR#ku+CKD_EP}Lmn(;_Mkm*THE|jY zJKPa#oOSdITeobfQKN=@G^ci+dFC0U(oxMTM|BlY<~3_P29!lp5P7a+y*M#Zk+pgn z*N1iLkiRDSYjn7fYrz`po%ozX2lgYXHqdUcriK!Q#Hp|H!V4~{^fz9Ay?xuZj%r?M z>nd=JSB>iYLY(?VJ=%jA`sr9NjEiGol+1i7GaZgxXQ{@ukA_Nb)4G+Y?P+99K_Rp~ ztFE}unpt&<)CB|tXs_iinW589E%+)b>$fO`w8` z8kZMhhw%RJQDJsjlm13H7V&Q^uEgAj@P|94!-ZVKni?te3kUY^uTj0a_Uh{Upse@N zM>^KH*73$Xrj*6AQ!q-$8Yxp`t**wk?Of%(_b5%E)!|%Gu*P~PA}eFpPP$-lyTh95 z(^s!tnI9MDva1cOZ@lpaB^KHEm1-a1G2O>ktYe)xe?DqF(+qcLhl_YKr_{K%eZ-sV7Op7)P`anya*;68QgWPra15@)pc|M>0`v@Gp#bU3UDCsSdK8n57nvzPU8 z0)`H?#ZEsc*RW1nxPb6lCRP}iG4~<-;pTR@PKOJ*hBXDw=oj|y+gHE-Cyy^9Pu)2| zwmR0-y(gx~WPB~-NFySqxOp2S;ZjsYtHUL0YJRd(00oHXf^E1n{o&GYCdb!e%w^6R z+*ht#Ubb{83favYeyqRF;~QC1AXJ6*>C>k?^6V{JG^duh=&yCG6XN4||KR?8?z5&? zC7ZwhP?yJL9(mYDdlC}IgfB(taO9dC2Nl-jU%R8mwNK4r3Y6!?HZ!uO#OuPiSSCybD{_gRCR906)8=QWi+Iy6-HQfNtZIC4!UxC(1>0o<%>XZNRq zN+;$M+e|0dux5P?R)oR#a`MCp=04PIGo4%u*56&aL?}h;R;}#bMy`DGIJIKyXl?R)YaMGAh~a%y5JJk_hxC!^ z#@AxZWzL!;CIaADc{4Albez@F4o8P0*EpzESR>j#K0cC{YdiVhc&E7E@-j>AmrDEl zUlw0~v18~{1%9h(j7o3T#0T5V$U0{399)0+UWg2IpEbBCQq-eI57(aJarqVwe;sRl zl?0H0+gyjcsm7mv;k9CoCRPYLQ6b@Pf_3p#|0ufdpRcCh`2Cv?BuAipSdK^Z*FuLQ z*Mc>+9JRaCQc}J-caFIam*pDPq>kbxW;N9^Wpb^RPD>hDlL({28i8`{C;dVR@AAGG zY!xS**RlTd=6}7i`sVLTK7gC6u+AMB>vmbsn>CYQn{8XSqL6WrN*}4Z4wtOK&6bM= zeC+7a(hf(5BiEbOty5u*Pt`RwUb@>d6V`v){_mGI{P5Ik`W_sW-lTD3aO>n+um(5p z6DsLGYjCsPu4_Nz9R4k=ARX(}B}-7_S?X|DyJn3G&^Eaqcg85!ur8SKKlv9OvD} zi;Ao}b?W5ogO_Z-K;kYx`?}UPqrVn%4eRIDeq(-SpGFO%A|vdw1~>0hu#@CMI8rI}!=dz(UYTU@0p{ojO zYU4Po#u;$^#@aWd%$%!?uf>>4E7!38U8f-`KO`b;C@F83HHqglrbezvOCtmV(9W_3 z_YhxSm3w%Mj`8yyEPN^0;pp(pRO>a0tX)v!Z+sl`8*cx7r5E0|%lh{NMyt0Wfyz_@ z)XBAAJv}ml_gQ|$1=h!o9#y}G$Kps*&vdNQmoCjz<5}o%Sh%~FRW*C>SC*K5blohxlK z3 zg>%ZURITzprM&I3rX(-#Z{55}QLdLPUMx?WJ9gfC_gx)pmI&d;|Em(WZ-}gi3aS_ zxs#6d`qiu9=*UN^ttlPiWS>q4S*JG5+G)=xp$z`d-Dch*X8ed8- zbCb#@`b)85I@YU}+iG8^ux3Rzc^;LW;w@$h)^A6Bqh*~xBwpp~P91|yF8gaLtO8n* zb?)<#YTIr0j2Xh0f*pahwG z3(BqIEW2nTCr_4kI654;rs|!FTvI<>W#Lq5gqN9j!^k?{+|TXal~Zq0HC@hgoj876 zoprUURh_!f<9=}CMh$hW*RNSay+ONco$Jr$$5wUy9J-$Tc0A3TxI*HCN-(2rutAj`XS073G~cyHoPbH)UK3i>#@~ zk>=tAAM-Z!Y1Bx^dgHovsPSKnGkd-i>~OK($6Ql2E{*W=V^2Dg>)*ZW`mk%vs4&4g zEhU8zaYfcCd1F9r7dkaXjjWmTRbhSk@@11)VrhhzpM2gC>!y<}GN9JBm3BWkC`kBHu*1>e$TdmsDy&)CrDMmA+EZ!wr4eqD_wjW2eC<~h z_g^kpP{xt&*tShQ?<4OFs2#QBhaY^PW4&qpdSZ!fxJ`99td}ft7?LNA@ba;D9Le=F zG20y7mC5@oK_XZ)L#>|Am65SKZ<=J`ZaET{SL^@CqZ3nXo3u=ZWWk zE2)uXJ2Rk`c^{%wwIb`7Y^(E=3p)Oc3?wOEVIsK%ucZnuVp z%*nOo?ltYI_|&BD$=p2xm%;xPBfd44 zYxUecnWxFJGNPumid<9QOg^KvBU*sD03GYCn>JBOE>p6zSnC`USJ#Zv2(L8bS{AJ1 zl}z!*4I7C3YiesS{8wk4#|P9pF7m%DuqDRVLat#=Pp86~H5MsFD9=NNraJ3Cq-LED z`|`^L2?G|ackkGtUK@w1r1Gw;9c>E?9pbBFy>t6^Mb-lb^p~g74!E+E>!(lrSANma z4vU!rnvEH1^_o{Kh-?k09W`n7YSnbC3G-l3qm2)DwAQ&+jX%J8+7)vh4(li1GApCO z8q=~~3Td|LtgQjHBhcdgWhp6Qe2r~}4u|!LFTYe_tySY%)+RNtsEvaiZev}gj-$KE z_uO-`HqNFE>(%QP#>T`rI;Zu%Xn~=Aemd6lkBY1Z^#4VTYy0c+fd{jc>!${VJ8J(n zty)r}61k?p-M8OfQe^$ri4)d<+EJ6r_lp-Vu4B!z5e#kaXGW!v2jobHBiCpEd7j@U zKB!bV?K?;Q@L%LQc$)m8cBfRSlCr+p(Zh$->yh^C*;9KSYu`}|B+Q>L?QnEBtjUZ} zVa@v2<;#~BrR>F|W@c5fyuBv{s-73=P)$o0kz8|`bU3$MK=bhyn&s+q#hm+!e5 zQBi`maL6mNPEB2^`gqoH54OO(*|UW&Md)zkn&Ja0tP$;+HETY2vM2DaJTMB&B+fg$ zuRD_U<3z1^)^;J@TX7v9%8BRkB{g-;=+u8(G;gkBov~{dE7Y5euZ0eW^_45Fwav!9@tD%3O35`K_-U!H+2lWTsPSypJBlq(xNu=3>x&mJsIaC~pyE|@hm8&$ zDOfW@t-co9CTl>gYe(n*8$D-^@TFjfqr;JFYEY}N{_*bJMvWRdd%=(E)@9v2ShGeO z>7weat5>h??6s`pbuG}cMGGD4y}NhcxqVyaeHeWyC~(_$_wMZ3vvg-O|1XOTk;EZb z@7l3leT~KNa2K-)%@wOqCJG@0VG z2GmZqv&q3xk&)64M~5TV$BrCPVNFJux*D%ssghiG4~trzb$x3a5$gZDVqi)<+&`fshd+bgaKPd{~in|Ni~7_rQRf zgdM?}=@ra8Mb>F)Y1;Ex``Oe2-Zg9LSRX!kkT5WjlPhBjjr}!py>sUd?Ok9dpSA|n9wexKs*)v3>R4l6eShVO3hOJEzZ0w}DXGr7ZQHi$w`U#c z*%nxlmL_~DLWjee8q_MRZ{NPf1jqKRTW;O53AKt`FHcYR?9E$ebE*aWe0_DSQRBEn zZMZY@K5XXMU0$|%E_|dm1)izHVa<_*0jsmN2GmZW@%X_M88fn`Dv=87+qZ79DY70m zY?#My-a4VPEwFO=a^XwC4o8P0tytrpWlaa+>|I&MXGaV4?$t}j`oytgHZ`7=4p*0J zYe1bH;9NN8Yp=a#WXA9@a8e5=!yLLL(Bud}8b<2^D6kiH+rz9z(@Ev z)}>8a;Q8mDH?k(P&o$OZjvTg3GLzPRMuwmUgZGVki!xp=wQHAS zp2ND4tXrUT>C#5l%zRxqf1Votm}+wVTFZLJ_U&F?URj^rIwFU)K+~p8wf!}6Em&W? zaDn<)ln5p-Pec;lx_R^b`E$5fbJ!DDx8p<$G;7*a+GY>1zIc)1mdt_?mPUXv5eSwg z=0vmR)I*61UXh+I`s?51`d8NRadB2mQBFnY;(dKox9;SL6W6R-MSqPBUz(guP4BX0 n%er_v>uXjESS?_+fYkz43s@~+wSd(ERts1yV6{NDw7~xZ3E&Vf diff --git a/res/images/icon_error.png b/res/images/icon_error.png new file mode 100644 index 0000000000000000000000000000000000000000..7064c2e23e0deb59e7b366f4dc7cb40902763c76 GIT binary patch literal 9616 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 z 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 IIJOG>KfFbp%m4rY diff --git a/res/images/icon_firmware_error.png b/res/images/icon_firmware_error.png new file mode 100644 index 0000000000000000000000000000000000000000..0c32c9ede10471bf1b40f37928048ba0edef9ef4 GIT binary patch literal 8088 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 HW*hN;6<-!8 literal 0 HcmV?d00001 diff --git a/res/images/icon_firmware_install.bmp b/res/images/icon_firmware_install.bmp deleted file mode 100644 index b0f5f959b4903d5394280589bb61055cf8933c98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91076 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 AyZ`_I diff --git a/res/images/icon_firmware_install.png b/res/images/icon_firmware_install.png new file mode 100644 index 0000000000000000000000000000000000000000..ee2afac5de2713a98ce7e84bcd2a3284a26c5bd5 GIT binary patch literal 11986 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 literal 0 HcmV?d00001 diff --git a/res/images/icon_installing.bmp b/res/images/icon_installing.bmp deleted file mode 100644 index fff99fd7e58b882ba65521ea6b42c6e303215a70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91076 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 PfYgB0fYiV*Py_!DLb`NA diff --git a/res/images/icon_unpacking.png b/res/images/icon_unpacking.png new file mode 100644 index 0000000000000000000000000000000000000000..9198e2eb1456b5a20c9f47afa941328e424c1c81 GIT binary patch literal 7180 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 literal 0 HcmV?d00001 diff --git a/res/images/indeterminate2.bmp b/res/images/indeterminate2.bmp deleted file mode 100644 index 223cd3c1e05c4c3b20224ac002947f0a4fbda77e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20214 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 Z{{R?v5>|bZYD@qC002ovPDHLkV1g<|>Inb< literal 0 HcmV?d00001 diff --git a/res/images/indeterminate3.bmp b/res/images/indeterminate3.bmp deleted file mode 100644 index fd9086a1fd9d7b6caa78c406889ae4a63b4a1e1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20214 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 diff --git a/res/images/indeterminate3.png b/res/images/indeterminate3.png new file mode 100644 index 0000000000000000000000000000000000000000..891a0009586bc0ebb1b3f0b614c74019fd06ce1d GIT binary patch literal 2254 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 G%zpsCXBAff diff --git a/res/images/indeterminate4.png b/res/images/indeterminate4.png new file mode 100644 index 0000000000000000000000000000000000000000..7a6415149b52da5eef9bb2c15181597a5a8033ac GIT binary patch literal 2249 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 X`8yHwagS?;00000NkvXXu0mjfW!vNf literal 0 HcmV?d00001 diff --git a/res/images/indeterminate5.bmp b/res/images/indeterminate5.bmp deleted file mode 100644 index e16efb04c42464b99b83eb9f7d9e3a591c883c75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20214 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 diff --git a/res/images/indeterminate5.png b/res/images/indeterminate5.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6ab20a780815b8682212f0926f12dfe04540ec GIT binary patch literal 2246 zcmV;%2s!tOP)?Ezl8n?<0b!GpK43`K0c&> z;_-hH;s0y=@cdjv>WBR7mE>deWAmAM>Mdq}UHXgoU$h?f7Fe&a2L;~m|1#6sfin(g zJ<0x9`ux|QU(PSi)p$Hs{a#<~?(OQ(uY0o=`Z87lhiy7e=X}oLSm-nS#heF@>mxs% zOpQLg9-oXY51K_kSs$=^Og<0gxjyt!miyuJ;ChXxfPb#9{g1~5-*ml3e==J$>Pvf2 zSg(V}xin9_U_PVu9r?tuur`_-=XxFWfIQD5{;cy)SQzW72MDb%@F9IRU z-j{z~&$Q-3`8Q{A!tyVlua9unlk>Cl9X;r57J3m+cIVNXw|bJZd=iQ8?OJ0Ee-@$7 z`FxJ?Xl!|)E%X)qP@e0L#un@J-;3l!`V)tLzTSz0f8y~o@%ZUQ=JUx{=9BMl?0Pvb zu)5wxk@XPbL;8~i{PXWBe-6<_fc zeCM>wH0*+`pGil*RLYR@A?LumYZ8KaYy9_=>-Je6n$OL+NO5sAqf6 z)VufZtbk{RVPW{UC3&BIyCi?N;-mSWzL9>r%IAMx&u_oYekJRzbw#85w5!tB=`7yN z0-hP>w%uy}&76gC-N%f-&3LX)c^bC`ee^>#={$z#`T}eDdE;1bE7CvLZ#nYgTW^c` zf9+Xs4Bsk_iPrb!^J!PiO{sc7{&Cl0HIMp&Cucpl_>jIUpRxW!ysMte=3lBGmw(Us zO`5lwYW>!_f)-!HFZjo=j&-E3Qr&hRbqz;b$|DN_&!}!0eeMbRC<`rUNG#>)800yN zet?toU3^F%b0hip<&*n&`JpZMgS_M=r0>fo&5^XfzV-Te%s=%68WIbwE1wb{(x=bz6JQ$JF@(fs@TOXm^!q%%jN<^HL@j}PhNYwn+~mvY>{%MWjWL zM_Cw)enRyS;zRnb{QLa#F){jO>&JJ#P#^la7ayN&|FEsrzFQLm`bNKvtf=^kulTFW z!{5WJt$SN)_4=w9&<6~Sim&*JznXk9I2x$o;ZU_V+iG=XRn=GOYPDgvTbo;Ia5&I0 zJR0g2o@oZgH0sva_D0)q*upce%k{yB@ssfq3-d_&#!t?P`ecmc$MTPF_vO>d|7P3DjmWRW>6gi;w6{8+8*%&V(l69=z`xO7 zDOb;B>&MC^**mX)-+pCYlEJ}j0JK=7CTjB*`cNFBeWz`6B%sg9bA8Gm5FY&0>f#*6 zkBjH}z#n-0uz9@ejnO|4{H#Q*m$NZ`r2JFy52XU;bY{!(uJQPbW(Fp``RB^`g|VHLhm?_;hDbnpX(b>#1HqR^)sI5$K{{v z7yJ<4X1&Bet{HfqN1Vgur?6kh=lR!O@-be0Pd-hK$T+AK>czEQu^x~o{&`;4@2p!a zjD=@~+RW#o^@Ds!zu<@D$LSyI)0Kb0k2x@O<+1(tRkKfA~4c z{J@XpRY38*g3Q?$M>pTs|JrBC`$J(ygQPfCfU zpOp05V&R*IgAeKBq37Rhy#CpFp`Y~naJlN4@Ya5@arjM+?iu}N$y@20qGP`w(iZ`Z`~o0r&Ba01(u)$j5C9(fbu{J|4S2DlFB$Sn3`?I}ei;3% zPjfitXm+uPe)v3EpYlwfd*Xid?@XI*YpryykbkbPe`m^lLyP&L>!rEX7b|3~T0fhU z=P|_deWDjV<`OAFU&<$5BpGe85BefWSg*4lkSB9!KLV>S&d5a}uLr3nkPqqOKA<1E zh8X`*Jh=##_KSMQpPlv0FYy|-fARdz{p=)bH`bPJHCMl6t?K;PQKOe5E8rRQHE%R^ z;A?2Nf7~|Q@ylb~!ZYZj90v3u$MtD&eH;UA8D7$-p12=)4bO0`i}A}70pWAo7_y1evR>Z$OuK0?t`2Q1snLXQqGY)6{ALd;V U&u}YTW&i*H07*qoM6N<$f*&>7e*gdg literal 0 HcmV?d00001 diff --git a/res/images/indeterminate6.bmp b/res/images/indeterminate6.bmp deleted file mode 100644 index 085ad951a853ec76353e9a4ab8adf85b71310621..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20214 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 Oyty9dZ8P3CHvS7=)D;T= diff --git a/res/images/indeterminate6.png b/res/images/indeterminate6.png new file mode 100644 index 0000000000000000000000000000000000000000..ddd9e7384c2342e6fc5a6160ada2d9a919b673e4 GIT binary patch literal 2262 zcmV;{2r2i8P)cNsUJx{#K-i< z0slJ*{=dc#&(AnfKjvqrBA=q4n$J&xdP`YfP5&bPFIo@d3(S}E(tN}I&uzP5+s1G> z%rg5=We@Xz&;^?tU?^Y!|t)7(P~=Xf4V z^Hq57eP_B}xK6mnN%~4YeeU!5;QFfbRq6qGICg$m9`R@NPiv0X109R=F@2n`?{hlk zll&*}q5SiHFgMRe@{c_AFYVv__`N-DWLv$h4cF=SlS!OT+Tp-<>gB>cQb@gg5Q#^& zw^is1*P{BI&pGT<&}ZWm{7_zC-69Mk{#r?%6FkLKNncP`)<&cSsIjfWQE$6dqffODLy`I@i!%g1M{H&=~| zPTtyhyz|(+dHbdiaL%(V9{*U859xO*@;7TfneTif{nvFq|MPl&{cYAOYcID=DQ>jQ z=EF@hdNnG<=A%t_4H?=8t~u^y9+bbqc&<-*=A)!f`hZ4(r99VvN%SQ@kCgc?C;I35 z%SwJi^Ic~CUk2uz!qfGtsvpYdD`9n|AGArFYQcwscjaQ7G2O|xHft|GRM!4T?;{ZXeqz>VAHV}$m8`?ea_)A>SG@W z?w|5h9|JTb7FtPP<74_XH{5?HpWMIZhqKfV%_-_j`IPiS`Gm14%hxwF-w=<-=!bgZ z^+4ys^C|H$ebCVIAM!8t(fmubP_;gJ{zLwy{fK;aJUlk*_t(wh^+gv8*BAUT1Y84V zFdVoD@o?O|-*pW3ffmli^f}LUsXlTT)DItn>f;!iA4xyN$MmW1P=3%q=OHvd5{rJ2 zkL6R+uaZx`zNz!&JUvF9dWzLUjF0JS`49P**qDCR`U&kXy2jjph|dN;+l_2v{fBk4 z@ZEwKa1FVdulbt4ynF`#SBJ57d(F&WofiYHQMTr5zUD64GQYT??^p=A&DeY>{0W?FNWnJxFBJoX9cgO>M`_gTBOR%B;oeeWmdMSXjXvfK|J z$N35HG5v!7*{QCW&jXTAK^t1He-QQ~<|kj{kbWqih5YC19pq=z^sD3(=ga-zSh4lh z^h@;|@lSJFrJk$SkLO9wqy9tdb*p2t&Pr#}X>Z?m#-z5Itzw%ODRS5cH1N8QW`O7V zlwToy-pBFHR(XsN&-L@4RX@U9EBWX8dxD?Yg!vvYKE;oef6DJE{w4jCd`k6@I^R{H z-njp!;yxqdTHpBJ5PYJG6iSF{HXr1KDGP{eyI1<`AYkd){f^B^AA5~uA@DG|G&dc zhJRuS=inL*p|nWKCV+}zEVCz`l0m=`482bmd_MF zJfD2N)N|GR0}mVi9PbqPNdSr&x9|mSN`;+PkgtD z$$q4tEV-`qNl{s^^UQvs-+(Xp_~jnH^utRA7l-+ao?DX_bN z+)IPG=}Q%SSp_c+O8VqQlGo3V-HVA_8(Q>3HR(8>XZjLL{kZQx`8aUS&v!1(Ki7BP zneu$1Z$4ktr(-$&JdZdAoa6J=c+f|lzv#giH!1p3KJn#{e^JYFBh6Q>2juNF%nN$L zdXqf4@2spIoPWU!zIYAk^L4=dr20?d$qO`Ty=Xo2yq4;jzbKz8`WN@_)Xzq?aBZQn zv^f98wr26#qJeh(?z$N~8@M=rcI?{j-LAimEe7z&VZb@gVP8of&QXT;fw7UT!b|#` zrGAiwmd>||f7%C+L5phh{6I^OsXq4M^VN9FQEL7*eJ!6Da8AJ)OxPUSFN9v{PSAS_OCtn9`-tHc=&z##=YBDKX+EX5C5+6qlDi*uKAj; k`TrAtZeznn!*2TQAEx0Er=`O2dI<{>e_0CmZlblDU+sv@tk>c7(jUKvb79LS1o1Ox^64b FL;$0WAW;AS diff --git a/res/images/progress_bar_empty.png b/res/images/progress_bar_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..9013f04ac2e66842cac29887c50ed7053a23c228 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{H!2~1^=N$M3q&N#aB8wRqxP?HN@zUM8KR`h@ zPZ!4!4q4WN3;7Nx2)G7>2iCT5_C44$rjF6*2UngIXWG7JC! literal 0 HcmV?d00001 diff --git a/res/images/progress_bar_empty_left_round.bmp b/res/images/progress_bar_empty_left_round.bmp deleted file mode 100644 index c4e2f44fc6f46b7e054425f58a56f5ce7d07f643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmXw!u?mAQ5Qde$LLZ=0=@aw;y7vVH2MG=$1xx54;wBwR=pdnkh(jR`LY9KBk~eX3 z_T8*MnwRMmkdyF*WHZWhMa;GYNpP)>5miwG7Qq|Io}NAY!Hi zV5CDx#}Fcfv3k*H=ElbA1qRw=V`JF*-q&7L8N2Q7rM7jhUAg{v|M)z8ozDB){rx}b WdRTB|6M*9Y0000| literal 0 HcmV?d00001 diff --git a/res/images/progress_bar_empty_right_round.bmp b/res/images/progress_bar_empty_right_round.bmp deleted file mode 100644 index 1906f62094180e0cb7bc001eafc918164cb92e4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmXw!F$%&!5JeNbf(Nh?Pv8OUy+9y^EEXXMk;NheJF$?(LV|?^3t=sUZG>E9Z;~p- z_s^OPym`#*?6A99&*Lb>l6gTUWJ=;_#{3T%8OB1xT#P@RPX&NTA2diuihP?R&vK|t zd6g<8n*?;9qzMG+1vWgBnpRsP&J^T+@Q!;1^q%_%s+{YE|xAkKA;}49{ls*6e diff --git a/res/images/progress_bar_empty_right_round.png b/res/images/progress_bar_empty_right_round.png new file mode 100644 index 0000000000000000000000000000000000000000..5427088234104c2dda96a8c3f60d64cd6dd1ee56 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^%s?!{!3HEfez(s7QnNf=977}|Sr01uH3tf?9e8-Y zOP8zp0YeOLuj*bFsrbuF+FDjOJYbmWDsWHTOw-uzp1qy_@ynmTR+Sc8$gQ*eyZp`S z<*s>0gJX*rEAA-q%nbRO-yLw}{2vBqRmKf#w|>dW)}8&(CcVvzNl8nXAzH1)C&#HJ zfGuM4l}!?5W!vV;vM$>d9o}6Ne&6)Z*1Fa0zuVXQ-20~g`+B`tL|7~*^V35>S2B3I L`njxgN@xNA*1l0< literal 0 HcmV?d00001 diff --git a/res/images/progress_bar_fill.bmp b/res/images/progress_bar_fill.bmp deleted file mode 100644 index 8d57d81174bc6798c32bccfe1f7d7a1cf965d1b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmZ?r?O=caGa#h_#Ed{J0;VN^Bm+Yb5DRfbFjxRYod3_ja2$wtLGdOiy$wlhACL{g VAayW&8psCe1>wt3d>u$L007pqC364( diff --git a/res/images/progress_bar_fill.png b/res/images/progress_bar_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..37c04b4f4636b004f8c111fe6350318e85e90fb7 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{H!2~1^=N$M3q&N#aB8wRqxP?HN@zUM8KR`is zPZ!4!j_b)k>gP5-{GX~}$A5Hr;|~j=d+bMdFQ|~7a<4_DOlFNZ!xC3zmwVb5&jPhD Nc)I$ztaD0e0szRaBg_B* literal 0 HcmV?d00001 diff --git a/res/images/progress_bar_left_round.bmp b/res/images/progress_bar_left_round.bmp deleted file mode 100644 index 6d2df8d6a44a33706375ce94b4404a363a693379..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmZ?rRbylT12Z700mRHeECR%g3<^LJ0zgWj%D@8u|Nm!jT6L^I;PCgKmh<)&aP0g3 zm*M#T|BB15U1QhDw8Yy+}kcoUEf!Z1FF2C0Sdq52s10mVQV#z&@Mav&OL zAH!iZ3=%^Yhw(x5c_2Ow!2-vDZUeD_VupvGUjxel*+KiR6>yyX|Cix1(1$?jsKfUP J1kQi|2>||Oqy+!~ diff --git a/res/images/progress_bar_left_round.png b/res/images/progress_bar_left_round.png new file mode 100644 index 0000000000000000000000000000000000000000..e72af47d4a9457c666dc3219e13db017724bd248 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^%s?!{!3HEfez(s7QeB=djv*44bNdwem<$D6_pf;) zYQZFQT5!UgMe0sU0vs$&EvCK^fq{-eoliN=rfsh5Sa?(+&PYGsNRqMYXzhxd{#u9i zCfqp`{6hA6tIi3h(l5p@wNo68j%Y}!T?~-^H^tH9h_A_-lFX&+V|lkeR2O&t`sP{O uslw1-TYo1Czj`26&HlwA*SWXu7elMiqV3zi`C0*8#o+1c=d#Wzp$P!XbV#uP literal 0 HcmV?d00001 diff --git a/res/images/progress_bar_right_round.bmp b/res/images/progress_bar_right_round.bmp deleted file mode 100644 index 68bb6fe3727bbbaa81390f996a9ecf4ecb770cff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmZ?rRbylT12Z700mRHeECR%g3<^LJ0zgWj%D@5whrj=HT6L`8|Ns9C49EZf=h*lC zujRbG1q{1@0zj;|?AkSkZ9p~4Z=_{3>u_w9}q+J!)bIe5FdsQ1KA)9 zv>%LNd>9QAhta2j>OlBB6a(1;$ANBx@(m9^zs7JG=-$)+{|4>5Rv>Wx`_HJu_rQJx E00btD zIhDvz$hY|PpMzSP`6JKk<;d(X+QE2xLTZA{hBIn9M)q|De;Q|({q22Jx}mtP=J}8E sH;svgkG~&gvOjNcuC<-#t~Db=MSu9?2?r+I0-eO*>FVdQ&MBb@0BI~wfB*mh literal 0 HcmV?d00001 From 1066d2c31990e3c2eacedbe0a70013da774fcf3e Mon Sep 17 00:00:00 2001 From: Doug Zongker <> Date: Wed, 1 Apr 2009 13:57:40 -0700 Subject: [PATCH 04/24] AI 144070: Add an option to wipe cache (only) to the recovery menu. Automated import of CL 144070 --- recovery.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/recovery.c b/recovery.c index 221ee29..a0bae97 100644 --- a/recovery.c +++ b/recovery.c @@ -302,9 +302,11 @@ prompt_and_wait() #define ITEM_REBOOT 0 #define ITEM_APPLY_SDCARD 1 #define ITEM_WIPE_DATA 2 +#define ITEM_WIPE_CACHE 3 char* items[] = { "reboot system now [Home+Back]", "apply sdcard:update.zip [Alt+S]", "wipe data/factory reset [Alt+W]", + "wipe cache partition", NULL }; ui_start_menu(headers, items); @@ -357,6 +359,13 @@ prompt_and_wait() if (!ui_text_visible()) return; break; + case ITEM_WIPE_CACHE: + ui_print("\n-- Wiping cache...\n"); + erase_root("CACHE:"); + ui_print("Cache wipe complete.\n"); + if (!ui_text_visible()) return; + break; + case ITEM_APPLY_SDCARD: ui_print("\n-- Install from sdcard...\n"); int status = install_package(SDCARD_PACKAGE_FILE); From 49283858fb6565a5da0c5987e614f60254e61804 Mon Sep 17 00:00:00 2001 From: Doug Zongker <> Date: Wed, 1 Apr 2009 14:39:15 -0700 Subject: [PATCH 05/24] AI 144082: Remove the unused "unpacking" recovery icon. Automated import of CL 144082 --- common.h | 1 - res/images/icon_unpacking.png | Bin 7180 -> 0 bytes ui.c | 1 - 3 files changed, 2 deletions(-) delete mode 100644 res/images/icon_unpacking.png diff --git a/common.h b/common.h index e17f76a..98ce868 100644 --- a/common.h +++ b/common.h @@ -47,7 +47,6 @@ void ui_end_menu(); // Set the icon (normally the only thing visible besides the progress bar). enum { BACKGROUND_ICON_NONE, - BACKGROUND_ICON_UNPACKING, BACKGROUND_ICON_INSTALLING, BACKGROUND_ICON_ERROR, BACKGROUND_ICON_FIRMWARE_INSTALLING, diff --git a/res/images/icon_unpacking.png b/res/images/icon_unpacking.png deleted file mode 100644 index 9198e2eb1456b5a20c9f47afa941328e424c1c81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7180 zcmbVxWmuHY_dZA{EFmq@jZy*CLacStuZAR)~z@gWwZ7jT#E zrItqGx8K+QcmKJrdFI74*IZ}LoO9pznTh_Wr9w*loEQfOhg3~fNe4TYVEgYUkFeht z1$esHf#{2>u_q1=fbxGGcM2JFi-W_^t)?We_jU0g@39fJ&&^Pmx}c^ox6g~Gnw_x( zV$tUw8U9+<$?8@bUHa)|PFbR@x)G9h+dc8`m}C@N4&Ua;O$`lhM8`+R~f()&4f3xfgY1fK=3R8Vr&TfvfS0q^9i;bUe<3%sFqm;+nzZA)|c1cArlm?0IX?HHCE zt9LQ2-Qiw7x1JIMKYsLK!kD@4XIde&a>BNrsN=_L`b_!=Ls1)GtI9LvQ%$`=l$l0M z>!I4cBlmI@((H=~jg!Q>Bxr-44{TE0`Q*xE=FU|1}6!O8$Kq zetx}S=XRX0SGajCCmc+h6}D?(WavBAJ|3u}KL?&!nYQ8l>Gdv!IZ#!$W}=_LVq5DI z%Fi<&d=skC4pzn(iW0PV>M1-Ix1n?x0>7x)bko_1%k|4UP5lt})o2Ljcj$F7T_v&x zGM_9j2;yd$+nN+e#6d0nvR z+q@shT}z%ii_2|x3eJ2T{a3fnnEu(bw)XZkLp^;xgN(%<0lW?|;LXPq7JYjX^ctlr zQ4Rx3!7uPa4t{9FuOQeUw1~2yt+BIHCvk;GZoNJEesA)l3Jo0GU=gZcZ z@%QlO&0kN}Qd8Fz3XM2l(#7)i=5Wx>op;!g(8v^U!40?}KX-_6Iyz=j8 z-u<_YnFW1;(9f>`rB||+|D9Z!MS$T6#)Ey5$&I-{x!Bt)Rm7VBXa&!Z+e5Y)9+I6?I?ONzxdmrecj31J0>!=8y7W_Nu7g+$ zNL%?Tl||~~;3W{`-xiMkJ2L!@O}b&2ARpEG`T4$tRpYxSFEd+t77z#;nx5AX;p}HOJ)eMsmDLu(x%%we!^W+BbB#$a3Px z;uzDIKl5CH17h!?@%3C5UD_{y=*8I?s@cg+VNKW_MRSjW_jvhrhSGx&mKC4#W8pBjf4t z9vQv$Mq`eTJ-kl8eEHJSLg>J!tLsTcB}zr5>ysk(VoQDBYRmskV5?X4!f`*U&2A8w zP>8tzB2XO{Xw(4%rN1zV^6kjyf*9M^@$G+};GZoH23-qElH<&hkANMOLl!9s_e;302S z8wnbNsfvl&L$rP8Qop}p;JT~#S&r9M9s6&rn+JV$Rg6OUufUfeGCr=JmtYU?=9UIg ziB^2QSE$ac#??+6&_DRIF%caUb>zXm|4K;k%E>xC2EzAJThAa5(d1K*GzsWo8>{cW z$u2K9H1V_bQ%;vgTMs21ou4lq{-)|em_u4!d^|jwTUx|Kn{ejgIhxNHh%NvlOBKMQ zrK6tZBUV1inx7CQW^a9cQd?TNyI&5sbeL!=9#`Ron4q^}Vv5Y(CMMj_Z|<8iAlR;f zK}%T~ttoXFvN3qDd8~3|=^40?rV(7gw8$z@h@RpTT@)hrfy-tj(9vj}j{^)ipF|XCt$oTl} zIKHvdtuBvOA!(_p9{ksB(r6BqqQ6L)Nr!3z@0-_~nraAat{Wke&f=VX5n^KJ&CQf! zoTMaY4@8#}(x)}C)usw-4gJN#7>Kpin$H1h^y-rUi;o0}^J26C94-RuWXi%yNA%$O-DnW=JE$r%9m_n}(ju4H86dyz8gTo4(v1N|re z{;d9o-NN*AWsM*n19#GErenD)C_wid=QQ_?Clh0CQIU1A_1Y#VGJ?t5eZkgl+urS{ zgbh(C^un#LBS`NJI_ke?S3Gh(s?04Vg=)O~%Z&q{F`vx^HngQr`ftt|IM|COJw{=& zloXq*4Y0vM)ZCnWAml(ZxD(KFR1=!IJ7NlHH36!;ALvuxne`0_B|%3FZVe8mL!oOv z%YoY4s=*gW4?mZcEd|IK%roc5ks+U5T*_S$%%(jZ3>VJGNKOA;UcYlpj3YflQxh^* zQL*#eb9)cp*4oD3snAb zJ5S^k5K~>{WCJXa_N}Tt0Dt^=JX1M66Xm;AV{EDkpNt~FCme+-(-97yiK&=>%R6ht zlnEdlc8%@-N|pwIT|^*Di;LR16=GtdhJkFsw0HK}(5pD7S#8&+C{@4@=ll7{)JKH4_%xJY5AWloVUkM}kIsALz zvvPs+YG$^&$A8=MXgpu@P7&6+>|1`LB#_XE&Xo?C5k`34P}%XinS9Sn%i|NQ$4Bn% zJ;y(psnlKeKMK^D` z{nffQiDh4J@6XgxJ77UYq1MGD7+G#cL*l~9r74(m3gBF|9&F7G0N;2Z#>7b+hB^C*FU%Z4^3ym%!La?!c^!0fL2|FyNo~Wyj;!A7x z{w9PC?_5qp@6}9DYeWt%> zO9L%Sxt7z@_M^$`oyE;@mv?lO+S#Lau3H|-6Qm_06CC;bAueQmeAuZ?#%LBcJ~r}6 zcz2;Gp6_M#EHQ(DE?;!4x*i*vyWa30y9IsNEIz+nn@e$RZ84LW)3di|>*1l*#3c@W za`eT8mZ+$wJ~Sadb{@xhkRUB>wq^zLFEEx0!g4I2<%=-nu%J(Cj_hRqedol-_m+X- zu?|@;4=mX`Yj#ml-^WHRuro*yS87rB2Mcg1QHvF=jiw!?xx=I*Z{3f7%BY_NAG?X z7w_W%ISyE4vPlZ!q;=rXLD@K|EK?&Rk&-cSu}JNbsMq|IsQD;Kw^3(hUrRoF)1tRTMQ- zr7dWB_;#m)@`}>kxAd^L?CbIilo9<>-%ufgg2D3s{`THps33B3a-naMo`Piul;?Vm zs)s1jLfgM(FrBtS&wP3;L)IWL^7mGtc~{F9&cP2qv2;2aueT0)`?h1g*0kRKk+$aO zA2Gxi`B-Y{MyYtl0{u^)(vp++x<-dQhqOuzp82htcU(5MEPXcRKf;**88AuTa$D;- zy7-+i36%v)61&G)PR8~LSkqUE1Lx<}soUYyM2sa~PrdoD)x<6V_ejHw0DE_$$C8mS z)WQ0Glbz)TR;v2nJ-r+09poE+;lS=b@JywO6ao432STAL6ud7G@yiv;?CMi7-IY^9 zQQ#ph#-#5p>3mzoiAErl1nV^_lN^b}(ekY{9HF`o`YJ=9Wp~`BKYl!GCxXuOU8Z1g z?gyZ*L<)6U1+Zi>%v{K!%}wNC`N4qAco*Q@_Rh92OWtDnquCglRd@ou_#-Kv)^|1> zv&`tay8Oppj;iY#K4o-}QbGH-vk3(}4lpTAu+;=y00)WPaDH|uheaC4ETyxo~()fzC3(oxgm@u+A(iC8t~h zauio^LZFzZto8m)B(UT< z<+R4p=!q$XD8k4@o5;^;`Ojf>^%+J>hzfK|$?kOdV*&!}Tt9qMRHmHQ9!rw_b4Rhp|)7u09_G7G$V8xEiowCEQ!@9-vFns-82HGExYwAs7U ziOS2%Q`M`pgk%w^X=@+-`SbLyA}YJ3Mb~JMY@fr~d3qdWp}4%JWO}r{9cexXshy}j?`K}2!zRWUIMjz3wZ zY=n^!$CFZI9j%wS#l^*1($y`HD5Uoub#7e_q@&%>71cSgPUPVMfK;1V7^^!u`S_5_ zo?yj(M8p<@gey+DD^$7I{oHYRbJXzudZJVc_pL{=g8&PWk8m zqQAj~s8;`=4`kF295rRWT%6_p_BgMq>l@_lZL6<8nkpTAh5QCLa;e3CQ*JT!lLI1T z(HU@mOOO}=y8M8U4*eL=-o-R~gOzk%UZgWU>y=XO=X?5`I834O@}QU~YibDnloT-o z)6=5Iuf}uV#JW4uiEo*Ng@ti(m6AB48Ol?IsMFT94eLJMJwG=JaU>S^rW|JMabyy| zB^*P@B+lU>Am5-?_4{rZ&7%J%sL_ppIK=#G zYKsc3*2BUSdn*MH2oRbEBOYd402HkiiU>Ts^<|xuc1{JoBXD?uF0#_fdiQ;iq}2i& z7Xnf(am?|d=ElB{h#uBT)w3hxKe47Zs85B=sUvoRglBOXBq@rgo}ruUCN9ZIfZ7|t z*ipW!Px%yQ2(C^*MP?%@0_Vrd;SPG{nR6VVBb5C8DCv|Rd5&;U)>F0wpp`3;J|8wy z4|62k*sVoN=n)8s*HpZMPt1Ci!y|&u*>27Cm(5NOm_c1m%(aa%o zXC)dEvUZP6;(%HM#C#p;T9?ig)^RjBu=hTMh>AJM=M{}eB;G5H1zRAjy<>uv52ySF z*y;mj&z_7@!!vOMswfTeKIv%tt~;_B_Gh+R_T#6=X!VTB-*aE47G{FC1V3w)M~(6Y zhs(SQp%+IcE)y_BWIf}S+Y5=24-#bdyouUqnH~Ubk z8~VhCMuwWOUIFoBWFMOzbzI~O1qnI|1#b$iF6U}ZRdKe*^V}S*v}$KaBH?5GT6wO9 zY|e4}(1BP4zX)-dF$LoO{_)V)oFo%5hbc}zJ_Z6%1_U%CDm?kEC|z`U=M$y4)bYNA z>(Ak2Hqv74@>G#p$+fi5c-R_da}--jIXZEPK=}Bowey|vGX0{JxFEuSfo>4wi9NN$ zdtiBKsgQX)_?w_p-?wmC{{Sqrvd363CMHs zGeevcE_vc{p@zfJfsBMW0^yr#$EYJ|wbR5!+}G0F?2j4d;n~SZoSm;Ru4Z-q&QzXC z{=>4A2{*C$8**}i;N=(LA42?_>7akST)%zaj~VMY^reTwQx8z6CWP#W_?K@iy{*4~ zd4a)eUikC|z1df7pZW%s|12Xr)m&Jv%L?YAAXsd%3^*wzx~bkSkO2E!>OB=F>5_^n z>NM{J69I^!%#6?X&}e19kTjzHC^u&+mPp6s49owrKPYTD3rMI@e&?CkwX>J!6KLn~ksylL^ ze4C+x5O#JQ;s^bS5U zP&RazLu7skVQ&_@(IaxzPC?>$$^-pe%8Nu z^p=C;w0Yhl=h*Zq@pboI5H^~KzHgeF%Y5PWa9)DA?XmlFux9B53L*3{K9nP{h@C18 z+-|k~e24>TCMgukAC8e1`aMP_>YE zBE%gF-Mxd_uRm^X>W{$E*@Is>b6%OY2K^4W7E$HMV`W(}_i2Rw_dlhD7V~aub#*+r zl>2PE$@?froFZGSY@Xm{$w&MyZqT}6Cm>|EGtfFQogIRQhiB=xTS}x95fO25kwAW09SJt+|* z?JZ#POLEsJ<`8yxc(~B%E4o_xDDZmo!Q+$|Oe-EcFa`CoA8Np+uZAWZOzN$rHGX@2 zO2pJ`KUcHx81EYx*IQ9>ou?o`A*#aMP0%twH@QjxVK<)Z4hD1jniiY%1Stl|Cq;lB zKYpCzkd|I%r>s0aUY8vzKuwIbS98r%!0BX-D^#BUyzlVf;O=6{ExN!gMHIjrQD7|+*ywX-PtMKEd3@?HC#kQWbvR$<$t$6@Dl0$lyrXMQ0UQ5_J*6ZtQ04+aj_j8_XnI%a{PCaICY}K1B z<`fk?ScvlbcW@TRPk4b9J!WQRswn1d Date: Wed, 1 Apr 2009 15:48:46 -0700 Subject: [PATCH 06/24] AI 144130: Don't build OTA package keys into the recovery binary; read them from an external file in the recovery image. Use the test-keys for all builds. Automated import of CL 144130 --- Android.mk | 13 -------- install.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/Android.mk b/Android.mk index 6198ab3..8c1de73 100644 --- a/Android.mk +++ b/Android.mk @@ -33,21 +33,8 @@ LOCAL_STATIC_LIBRARIES := libminzip libunz libamend libmtdutils libmincrypt LOCAL_STATIC_LIBRARIES += libminui libpixelflinger_static libpng libcutils LOCAL_STATIC_LIBRARIES += libstdc++ libc -# Specify a C-includable file containing the OTA public keys. -# This is built in config/Makefile. -# *** THIS IS A TOTAL HACK; EXECUTABLES MUST NOT CHANGE BETWEEN DIFFERENT -# PRODUCTS/BUILD TYPES. *** -# TODO: make recovery read the keys from an external file. -RECOVERY_INSTALL_OTA_KEYS_INC := \ - $(call intermediates-dir-for,PACKAGING,ota_keys_inc)/keys.inc -# Let install.c say #include "keys.inc" -LOCAL_C_INCLUDES += $(dir $(RECOVERY_INSTALL_OTA_KEYS_INC)) - include $(BUILD_EXECUTABLE) -# Depend on the generated keys.inc containing the OTA public keys. -$(intermediates)/install.o: $(RECOVERY_INSTALL_OTA_KEYS_INC) - include $(commands_recovery_local_path)/minui/Android.mk endif # TARGET_ARCH == arm diff --git a/install.c b/install.c index 0691120..4dcfe75 100644 --- a/install.c +++ b/install.c @@ -31,12 +31,8 @@ #include "roots.h" #include "verifier.h" -/* List of public keys */ -static const RSAPublicKey keys[] = { -#include "keys.inc" -}; - #define ASSUMED_UPDATE_SCRIPT_NAME "META-INF/com/google/android/update-script" +#define PUBLIC_KEYS_FILE "/res/keys" static const ZipEntry * find_update_script(ZipArchive *zip) @@ -114,7 +110,8 @@ handle_update_script(ZipArchive *zip, const ZipEntry *update_script_entry) } static int -handle_update_package(const char *path, ZipArchive *zip) +handle_update_package(const char *path, ZipArchive *zip, + const RSAPublicKey *keys, int numKeys) { // Give verification half the progress bar... ui_print("Verifying update package...\n"); @@ -122,7 +119,7 @@ handle_update_package(const char *path, ZipArchive *zip) VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME); - if (!verify_jar_signature(zip, keys, sizeof(keys) / sizeof(keys[0]))) { + if (!verify_jar_signature(zip, keys, numKeys)) { LOGE("Verification failed\n"); return INSTALL_CORRUPT; } @@ -147,6 +144,80 @@ handle_update_package(const char *path, ZipArchive *zip) return ret; } +// Reads a file containing one or more public keys as produced by +// DumpPublicKey: this is an RSAPublicKey struct as it would appear +// as a C source literal, eg: +// +// "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}" +// +// (Note that the braces and commas in this example are actual +// characters the parser expects to find in the file; the ellipses +// indicate more numbers omitted from this example.) +// +// The file may contain multiple keys in this format, separated by +// commas. The last key must not be followed by a comma. +// +// Returns NULL if the file failed to parse, or if it contain zero keys. +static RSAPublicKey* +load_keys(const char* filename, int* numKeys) { + RSAPublicKey* out = NULL; + *numKeys = 0; + + FILE* f = fopen(filename, "r"); + if (f == NULL) { + LOGE("opening %s: %s\n", filename, strerror(errno)); + goto exit; + } + + int i; + bool done = false; + while (!done) { + ++*numKeys; + out = realloc(out, *numKeys * sizeof(RSAPublicKey)); + RSAPublicKey* key = out + (*numKeys - 1); + if (fscanf(f, " { %i , %i , { %i", + &(key->len), &(key->n0inv), &(key->n[0])) != 3) { + goto exit; + } + if (key->len != RSANUMWORDS) { + LOGE("key length (%d) does not match expected size\n", key->len); + goto exit; + } + for (i = 1; i < key->len; ++i) { + if (fscanf(f, " , %i", &(key->n[i])) != 1) goto exit; + } + if (fscanf(f, " } , { %i", &(key->rr[0])) != 1) goto exit; + for (i = 1; i < key->len; ++i) { + if (fscanf(f, " , %i", &(key->rr[i])) != 1) goto exit; + } + fscanf(f, " } } "); + + // if the line ends in a comma, this file has more keys. + switch (fgetc(f)) { + case ',': + // more keys to come. + break; + + case EOF: + done = true; + break; + + default: + LOGE("unexpected character between keys\n"); + goto exit; + } + } + + fclose(f); + return out; + +exit: + if (f) fclose(f); + free(out); + *numKeys = 0; + return NULL; +} + int install_package(const char *root_path) { @@ -169,6 +240,14 @@ install_package(const char *root_path) ui_print("Opening update package...\n"); LOGI("Update file path: %s\n", path); + int numKeys; + RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys); + if (loadedKeys == NULL) { + LOGE("Failed to load keys\n"); + return INSTALL_CORRUPT; + } + LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE); + /* Try to open the package. */ ZipArchive zip; @@ -180,7 +259,8 @@ install_package(const char *root_path) /* Verify and install the contents of the package. */ - int status = handle_update_package(path, &zip); + int status = handle_update_package(path, &zip, loadedKeys, numKeys); mzCloseZipArchive(&zip); + free(loadedKeys); return status; } From 596271fa71d79e3eec03c7cf6ac76cb026dd8578 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Wed, 29 Apr 2009 16:52:04 -0700 Subject: [PATCH 07/24] handle short writes when unzipping files minzip fails if write() doesn't write all the data in one call. Apparently this was good enough before, but it causes OTAs to fail all the time now (maybe due to the recently-submitted kernel)? Change code to attempt continuing after short writes. --- minzip/Zip.c | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/minzip/Zip.c b/minzip/Zip.c index 100c833..ead8993 100644 --- a/minzip/Zip.c +++ b/minzip/Zip.c @@ -41,7 +41,7 @@ enum { CENSIZ = 20, CENLEN = 24, CENNAM = 28, - CENEXT = 30, + CENEXT = 30, CENCOM = 32, CENDSK = 34, CENATT = 36, @@ -66,13 +66,13 @@ enum { LOCSIG = 0x04034b50, // PK34 LOCHDR = 30, - + LOCVER = 4, LOCFLG = 6, LOCHOW = 8, LOCTIM = 10, LOCCRC = 14, - LOCSIZ = 18, + LOCSIZ = 18, LOCLEN = 22, LOCNAM = 26, LOCEXT = 28, @@ -757,7 +757,7 @@ bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry, { CopyProcessArgs args; bool ret; - + args.buf = buf; args.bufLen = bufLen; ret = mzProcessZipEntryContents(pArchive, pEntry, copyProcessFunction, @@ -772,13 +772,29 @@ bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry, static bool writeProcessFunction(const unsigned char *data, int dataLen, void *fd) { - ssize_t n = write((int)fd, data, dataLen); - if (n != dataLen) { - LOGE("Can't write %d bytes (only %ld) from zip file: %s\n", - dataLen, n, strerror(errno)); - return false; - } - return true; + int zeroWrites = 0; + ssize_t soFar = 0; + do { + ssize_t n = write((int)fd, data+soFar, dataLen-soFar); + if (n < 0) { + LOGE("Error writing %ld bytes from zip file: %s\n", + dataLen-soFar, strerror(errno)); + return false; + } else if (n > 0) { + soFar += n; + if (soFar == dataLen) return true; + if (soFar > dataLen) { + LOGE("write overrun? (%ld bytes instead of %d)\n", + soFar, dataLen); + return false; + } + zeroWrites = 0; + } else { + ++zeroWrites; + } + } while (zeroWrites < 5); + LOGE("too many consecutive zero-length writes\n"); + return false; } /* From 683c4628039a8cb6dad1a086fae23a7d71438414 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Tue, 5 May 2009 17:50:21 -0700 Subject: [PATCH 08/24] align data passed to write() on 32k boundaries In donut, OTA installation often encounters the write() system call doing short writes -- which is legal but unexpected -- or failing with ENOSPC when plenty of space is available. Passing aligned memory buffers to write() appears to prevent (or at least reduce the frequency) of these problems. b/1833052 has been filed to look at the underlying problem, but this change aligns buffers we use with write() so we can OTA for now (or see if this problem still occurs). --- minzip/Zip.c | 62 ++++++++++++++++++++++++++++++++++++++-------------- minzip/Zip.h | 10 ++++++++- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/minzip/Zip.c b/minzip/Zip.c index ead8993..a601e74 100644 --- a/minzip/Zip.c +++ b/minzip/Zip.c @@ -12,6 +12,7 @@ #include // for uintptr_t #include #include // for S_ISLNK() +#include #include #define LOG_TAG "minzip" @@ -84,6 +85,12 @@ enum { }; +/* The maximum zipped file write size we will align. */ +#define WRITE_SIZE 32768 +/* The boundary on which we will align it. */ +#define WRITE_ALIGNMENT 32768 + + /* * For debugging, dump the contents of a ZipEntry. */ @@ -770,17 +777,38 @@ bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry, } static bool writeProcessFunction(const unsigned char *data, int dataLen, - void *fd) + void *cookie) { - int zeroWrites = 0; + WriteInfo *wi = (WriteInfo*)cookie; + + if (dataLen <= WRITE_SIZE) { + memcpy(wi->aligned_buffer, data, dataLen); + data = wi->aligned_buffer; + } + ssize_t soFar = 0; - do { - ssize_t n = write((int)fd, data+soFar, dataLen-soFar); - if (n < 0) { - LOGE("Error writing %ld bytes from zip file: %s\n", - dataLen-soFar, strerror(errno)); + while (true) { + ssize_t n = write(wi->fd, data+soFar, dataLen-soFar); + if (n <= 0) { + LOGE("Error writing %ld bytes from zip file from %p: %s\n", + dataLen-soFar, data+soFar, strerror(errno)); + if (errno == ENOSPC) { + struct statfs sf; + if (statfs("/system", &sf) != 0) { + LOGE("failed to statfs /system: %s\n", strerror(errno)); + } else { + LOGE("statfs said: %ld * %ld = %ld\n", + (long)sf.f_bsize, (long)sf.f_bfree, + (long)sf.f_bsize * (long)sf.f_bfree); + } + } return false; } else if (n > 0) { + if (n < dataLen-soFar) { + LOGE("short write: %d bytes of %d from %p\n", + (int)n, (int)(dataLen-soFar), + data+soFar); + } soFar += n; if (soFar == dataLen) return true; if (soFar > dataLen) { @@ -788,23 +816,18 @@ static bool writeProcessFunction(const unsigned char *data, int dataLen, soFar, dataLen); return false; } - zeroWrites = 0; - } else { - ++zeroWrites; } - } while (zeroWrites < 5); - LOGE("too many consecutive zero-length writes\n"); - return false; + } } /* * Uncompress "pEntry" in "pArchive" to "fd" at the current offset. */ bool mzExtractZipEntryToFile(const ZipArchive *pArchive, - const ZipEntry *pEntry, int fd) + const ZipEntry *pEntry, WriteInfo *wi) { bool ret = mzProcessZipEntryContents(pArchive, pEntry, writeProcessFunction, - (void *)fd); + wi); if (!ret) { LOGE("Can't extract entry to file.\n"); return false; @@ -906,6 +929,11 @@ bool mzExtractRecursive(const ZipArchive *pArchive, return false; } + unsigned char* buffer = malloc(WRITE_SIZE+WRITE_ALIGNMENT); + WriteInfo wi; + wi.aligned_buffer = buffer + WRITE_ALIGNMENT - + ((long)buffer % WRITE_ALIGNMENT); + unsigned int zipDirLen; char *zpath; @@ -1086,7 +1114,8 @@ bool mzExtractRecursive(const ZipArchive *pArchive, break; } - bool ok = mzExtractZipEntryToFile(pArchive, pEntry, fd); + wi.fd = fd; + bool ok = mzExtractZipEntryToFile(pArchive, pEntry, &wi); close(fd); if (!ok) { LOGE("Error extracting \"%s\"\n", targetFile); @@ -1109,6 +1138,7 @@ bool mzExtractRecursive(const ZipArchive *pArchive, free(helper.buf); free(zpath); + free(buffer); return ok; } diff --git a/minzip/Zip.h b/minzip/Zip.h index 1c1df2f..57c0abd 100644 --- a/minzip/Zip.h +++ b/minzip/Zip.h @@ -55,6 +55,14 @@ typedef struct { size_t len; } UnterminatedString; +/* + * The information we pass down to writeProcessFunction. + */ +typedef struct { + int fd; + unsigned char* aligned_buffer; +} WriteInfo; + /* * Open a Zip archive. * @@ -166,7 +174,7 @@ bool mzIsZipEntryIntact(const ZipArchive *pArchive, const ZipEntry *pEntry); * Inflate and write an entry to a file. */ bool mzExtractZipEntryToFile(const ZipArchive *pArchive, - const ZipEntry *pEntry, int fd); + const ZipEntry *pEntry, WriteInfo *wi); /* * Inflate all entries under zipDir to the directory specified by From 1c4ceae38f3fd7eb1e451d430acb5d99f257b0f9 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Fri, 8 May 2009 09:43:28 -0700 Subject: [PATCH 09/24] undo temporary alignment hack Remove the memory alignment that mysteriously made OTA installs work, in anticipation of a kernel that fixes the actual problem. Handle EINTR properly. --- minzip/Zip.c | 46 +++++++--------------------------------------- minzip/Zip.h | 10 +--------- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/minzip/Zip.c b/minzip/Zip.c index a601e74..8cdb898 100644 --- a/minzip/Zip.c +++ b/minzip/Zip.c @@ -12,7 +12,6 @@ #include // for uintptr_t #include #include // for S_ISLNK() -#include #include #define LOG_TAG "minzip" @@ -85,12 +84,6 @@ enum { }; -/* The maximum zipped file write size we will align. */ -#define WRITE_SIZE 32768 -/* The boundary on which we will align it. */ -#define WRITE_ALIGNMENT 32768 - - /* * For debugging, dump the contents of a ZipEntry. */ @@ -779,36 +772,18 @@ bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry, static bool writeProcessFunction(const unsigned char *data, int dataLen, void *cookie) { - WriteInfo *wi = (WriteInfo*)cookie; - - if (dataLen <= WRITE_SIZE) { - memcpy(wi->aligned_buffer, data, dataLen); - data = wi->aligned_buffer; - } + int fd = (int)cookie; ssize_t soFar = 0; while (true) { - ssize_t n = write(wi->fd, data+soFar, dataLen-soFar); + ssize_t n = write(fd, data+soFar, dataLen-soFar); if (n <= 0) { LOGE("Error writing %ld bytes from zip file from %p: %s\n", dataLen-soFar, data+soFar, strerror(errno)); - if (errno == ENOSPC) { - struct statfs sf; - if (statfs("/system", &sf) != 0) { - LOGE("failed to statfs /system: %s\n", strerror(errno)); - } else { - LOGE("statfs said: %ld * %ld = %ld\n", - (long)sf.f_bsize, (long)sf.f_bfree, - (long)sf.f_bsize * (long)sf.f_bfree); - } + if (errno != EINTR) { + return false; } - return false; } else if (n > 0) { - if (n < dataLen-soFar) { - LOGE("short write: %d bytes of %d from %p\n", - (int)n, (int)(dataLen-soFar), - data+soFar); - } soFar += n; if (soFar == dataLen) return true; if (soFar > dataLen) { @@ -824,10 +799,10 @@ static bool writeProcessFunction(const unsigned char *data, int dataLen, * Uncompress "pEntry" in "pArchive" to "fd" at the current offset. */ bool mzExtractZipEntryToFile(const ZipArchive *pArchive, - const ZipEntry *pEntry, WriteInfo *wi) + const ZipEntry *pEntry, int fd) { bool ret = mzProcessZipEntryContents(pArchive, pEntry, writeProcessFunction, - wi); + (void*)fd); if (!ret) { LOGE("Can't extract entry to file.\n"); return false; @@ -929,11 +904,6 @@ bool mzExtractRecursive(const ZipArchive *pArchive, return false; } - unsigned char* buffer = malloc(WRITE_SIZE+WRITE_ALIGNMENT); - WriteInfo wi; - wi.aligned_buffer = buffer + WRITE_ALIGNMENT - - ((long)buffer % WRITE_ALIGNMENT); - unsigned int zipDirLen; char *zpath; @@ -1114,8 +1084,7 @@ bool mzExtractRecursive(const ZipArchive *pArchive, break; } - wi.fd = fd; - bool ok = mzExtractZipEntryToFile(pArchive, pEntry, &wi); + bool ok = mzExtractZipEntryToFile(pArchive, pEntry, fd); close(fd); if (!ok) { LOGE("Error extracting \"%s\"\n", targetFile); @@ -1138,7 +1107,6 @@ bool mzExtractRecursive(const ZipArchive *pArchive, free(helper.buf); free(zpath); - free(buffer); return ok; } diff --git a/minzip/Zip.h b/minzip/Zip.h index 57c0abd..1c1df2f 100644 --- a/minzip/Zip.h +++ b/minzip/Zip.h @@ -55,14 +55,6 @@ typedef struct { size_t len; } UnterminatedString; -/* - * The information we pass down to writeProcessFunction. - */ -typedef struct { - int fd; - unsigned char* aligned_buffer; -} WriteInfo; - /* * Open a Zip archive. * @@ -174,7 +166,7 @@ bool mzIsZipEntryIntact(const ZipArchive *pArchive, const ZipEntry *pEntry); * Inflate and write an entry to a file. */ bool mzExtractZipEntryToFile(const ZipArchive *pArchive, - const ZipEntry *pEntry, WriteInfo *wi); + const ZipEntry *pEntry, int fd); /* * Inflate all entries under zipDir to the directory specified by From 07e1dca7068284c4f3013550335029eb72b39b82 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 28 May 2009 19:02:45 -0700 Subject: [PATCH 10/24] don't say "install complete" when it really isn't Change the recovery UI so that when there is a hboot or radio update pending (which the user most do a home+back reboot to actually install), the UI tells them so, instead of saying "Install from sdcard complete." --- firmware.c | 4 ++++ firmware.h | 3 +++ install.c | 2 +- recovery.c | 7 ++++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/firmware.c b/firmware.c index 34b2918..e2e4fe6 100644 --- a/firmware.c +++ b/firmware.c @@ -39,6 +39,10 @@ int remember_firmware_update(const char *type, const char *data, int length) { return 0; } +// Return true if there is a firmware update pending. +int firmware_update_pending() { + return update_data != NULL && update_length > 0; +} /* Bootloader / Recovery Flow * diff --git a/firmware.h b/firmware.h index f3f7aab..aeb8f97 100644 --- a/firmware.h +++ b/firmware.h @@ -23,6 +23,9 @@ */ int remember_firmware_update(const char *type, const char *data, int length); +/* Returns true if a firmware update has been saved. */ +int firmware_update_pending(); + /* If an update was saved, reboot into the bootloader now to install it. * Returns 0 if no radio image was defined, nonzero on error, * doesn't return at all on success... diff --git a/install.c b/install.c index 4dcfe75..e7db2a8 100644 --- a/install.c +++ b/install.c @@ -105,7 +105,7 @@ handle_update_script(ZipArchive *zip, const ZipEntry *update_script_entry) return INSTALL_ERROR; } - ui_print("Installation complete.\n"); + LOGI("Installation complete.\n"); return INSTALL_SUCCESS; } diff --git a/recovery.c b/recovery.c index a0bae97..e329db9 100644 --- a/recovery.c +++ b/recovery.c @@ -375,7 +375,12 @@ prompt_and_wait() } else if (!ui_text_visible()) { return; // reboot if logs aren't visible } else { - ui_print("Install from sdcard complete.\n"); + if (firmware_update_pending()) { + ui_print("\nReboot via home+back or menu\n" + "to complete installation.\n"); + } else { + ui_print("\nInstall from sdcard complete.\n"); + } } break; } From f28c916e73ee9f643c67c70d059c70381d774cb0 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Tue, 2 Jun 2009 15:30:11 -0700 Subject: [PATCH 11/24] remove unused permissions scheme from amend Amend (aka the recovery command language) had a half-implemented scheme of limiting which commands OTA packages were allowed to execute. It's not clear what this was ever supposed to be good for. Remove it. --- amend/Android.mk | 4 +- amend/amend.c | 1 + amend/commands.c | 88 +++------- amend/commands.h | 32 +--- amend/main.c | 6 - amend/permissions.c | 270 ------------------------------ amend/permissions.h | 111 ------------- amend/register.c | 88 ++-------- amend/test_commands.c | 77 +-------- amend/test_permissions.c | 347 --------------------------------------- commands.c | 117 +++---------- recovery.c | 4 - 12 files changed, 69 insertions(+), 1076 deletions(-) delete mode 100644 amend/permissions.c delete mode 100644 amend/permissions.h delete mode 100644 amend/test_permissions.c diff --git a/amend/Android.mk b/amend/Android.mk index ae2d44a..c3b7c32 100644 --- a/amend/Android.mk +++ b/amend/Android.mk @@ -10,13 +10,11 @@ amend_src_files := \ ast.c \ symtab.c \ commands.c \ - permissions.c \ execute.c amend_test_files := \ test_symtab.c \ - test_commands.c \ - test_permissions.c + test_commands.c # "-x c" forces the lex/yacc files to be compiled as c; # the build system otherwise forces them to be c++. diff --git a/amend/amend.c b/amend/amend.c index 49cd64e..6f706d0 100644 --- a/amend/amend.c +++ b/amend/amend.c @@ -17,6 +17,7 @@ #include #include "amend.h" #include "lexer.h" +#include "parser.h" extern const AmCommandList *gCommands; diff --git a/amend/commands.c b/amend/commands.c index 75ff828..78121ad 100644 --- a/amend/commands.c +++ b/amend/commands.c @@ -152,37 +152,30 @@ getCommandArgumentType(Command *cmd) } static int -callCommandInternal(CommandEntry *entry, int argc, const char *argv[], - PermissionRequestList *permissions) +callCommandInternal(CommandEntry *entry, int argc, const char *argv[]) { if (entry != NULL && entry->argType == CMD_ARGS_WORDS && (argc == 0 || (argc > 0 && argv != NULL))) { - if (permissions == NULL) { - int i; - for (i = 0; i < argc; i++) { - if (argv[i] == NULL) { - goto bail; - } + int i; + for (i = 0; i < argc; i++) { + if (argv[i] == NULL) { + goto bail; } } TRACE("calling command %s\n", entry->name); - return entry->hook(entry->name, entry->cookie, argc, argv, permissions); -//xxx if permissions, make sure the entry has added at least one element. + return entry->hook(entry->name, entry->cookie, argc, argv); } bail: return -1; } static int -callBooleanCommandInternal(CommandEntry *entry, bool arg, - PermissionRequestList *permissions) +callBooleanCommandInternal(CommandEntry *entry, bool arg) { if (entry != NULL && entry->argType == CMD_ARGS_BOOLEAN) { TRACE("calling boolean command %s\n", entry->name); - return entry->hook(entry->name, entry->cookie, arg ? 1 : 0, NULL, - permissions); -//xxx if permissions, make sure the entry has added at least one element. + return entry->hook(entry->name, entry->cookie, arg ? 1 : 0, NULL); } return -1; } @@ -190,63 +183,37 @@ callBooleanCommandInternal(CommandEntry *entry, bool arg, int callCommand(Command *cmd, int argc, const char *argv[]) { - return callCommandInternal((CommandEntry *)cmd, argc, argv, NULL); + return callCommandInternal((CommandEntry *)cmd, argc, argv); } int callBooleanCommand(Command *cmd, bool arg) { - return callBooleanCommandInternal((CommandEntry *)cmd, arg, NULL); -} - -int -getCommandPermissions(Command *cmd, int argc, const char *argv[], - PermissionRequestList *permissions) -{ - if (permissions != NULL) { - return callCommandInternal((CommandEntry *)cmd, argc, argv, - permissions); - } - return -1; -} - -int -getBooleanCommandPermissions(Command *cmd, bool arg, - PermissionRequestList *permissions) -{ - if (permissions != NULL) { - return callBooleanCommandInternal((CommandEntry *)cmd, arg, - permissions); - } - return -1; + return callBooleanCommandInternal((CommandEntry *)cmd, arg); } int callFunctionInternal(CommandEntry *entry, int argc, const char *argv[], - char **result, size_t *resultLen, PermissionRequestList *permissions) + char **result, size_t *resultLen) { if (entry != NULL && entry->argType == CMD_ARGS_WORDS && (argc == 0 || (argc > 0 && argv != NULL))) { - if ((permissions == NULL && result != NULL) || - (permissions != NULL && result == NULL)) + if (result != NULL) { - if (permissions == NULL) { - /* This is the actual invocation of the function, - * which means that none of the arguments are allowed - * to be NULL. - */ - int i; - for (i = 0; i < argc; i++) { - if (argv[i] == NULL) { - goto bail; - } + /* This is the actual invocation of the function, + * which means that none of the arguments are allowed + * to be NULL. + */ + int i; + for (i = 0; i < argc; i++) { + if (argv[i] == NULL) { + goto bail; } } TRACE("calling function %s\n", entry->name); return ((FunctionHook)entry->hook)(entry->name, entry->cookie, - argc, argv, result, resultLen, permissions); -//xxx if permissions, make sure the entry has added at least one element. + argc, argv, result, resultLen); } } bail: @@ -258,16 +225,5 @@ callFunction(Function *fn, int argc, const char *argv[], char **result, size_t *resultLen) { return callFunctionInternal((CommandEntry *)fn, argc, argv, - result, resultLen, NULL); -} - -int -getFunctionPermissions(Function *fn, int argc, const char *argv[], - PermissionRequestList *permissions) -{ - if (permissions != NULL) { - return callFunctionInternal((CommandEntry *)fn, argc, argv, - NULL, NULL, permissions); - } - return -1; + result, resultLen); } diff --git a/amend/commands.h b/amend/commands.h index 38931c0..6c97e55 100644 --- a/amend/commands.h +++ b/amend/commands.h @@ -14,31 +14,18 @@ * limitations under the License. */ +#include + #ifndef AMEND_COMMANDS_H_ #define AMEND_COMMANDS_H_ -#include "permissions.h" - -/* Invoke or dry-run a command. If "permissions" is non-NULL, - * the hook should fill it out with the list of files and operations that - * it would need to complete its operation. If "permissions" is NULL, - * the hook should do the actual work specified by its arguments. - * - * When a command is called with non-NULL "permissions", some arguments - * may be NULL. A NULL argument indicates that the argument is actually - * the output of another function, so is not known at permissions time. - * The permissions of leaf-node functions (those that have only literal - * strings as arguments) will get appended to the permissions of the - * functions that call them. However, to be completely safe, functions - * that receive a NULL argument should request the broadest-possible - * permissions for the range of the input argument. +/* Invoke a command. * * When a boolean command is called, "argc" is the boolean value and * "argv" is NULL. */ typedef int (*CommandHook)(const char *name, void *cookie, - int argc, const char *argv[], - PermissionRequestList *permissions); + int argc, const char *argv[]); int commandInit(void); void commandCleanup(void); @@ -66,19 +53,13 @@ CommandArgumentType getCommandArgumentType(Command *cmd); int callCommand(Command *cmd, int argc, const char *argv[]); int callBooleanCommand(Command *cmd, bool arg); -int getCommandPermissions(Command *cmd, int argc, const char *argv[], - PermissionRequestList *permissions); -int getBooleanCommandPermissions(Command *cmd, bool arg, - PermissionRequestList *permissions); - /* * Function management */ typedef int (*FunctionHook)(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions); + char **result, size_t *resultLen); struct Function; typedef struct Function Function; @@ -90,7 +71,4 @@ Function *findFunction(const char *name); int callFunction(Function *fn, int argc, const char *argv[], char **result, size_t *resultLen); -int getFunctionPermissions(Function *fn, int argc, const char *argv[], - PermissionRequestList *permissions); - #endif // AMEND_COMMANDS_H_ diff --git a/amend/main.c b/amend/main.c index 9bb0785..bc9e587 100644 --- a/amend/main.c +++ b/amend/main.c @@ -86,12 +86,6 @@ main(int argc, char *argv[]) fprintf(stderr, "test_cmd_fn() failed: %d\n", ret); exit(ret); } - extern int test_permissions(void); - ret = test_permissions(); - if (ret != 0) { - fprintf(stderr, "test_permissions() failed: %d\n", ret); - exit(ret); - } #endif argc--; diff --git a/amend/permissions.c b/amend/permissions.c deleted file mode 100644 index a642d0b..0000000 --- a/amend/permissions.c +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2007 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 -#include -#include "permissions.h" - -int -initPermissionRequestList(PermissionRequestList *list) -{ - if (list != NULL) { - list->requests = NULL; - list->numRequests = 0; - list->requestsAllocated = 0; - return 0; - } - return -1; -} - -int -addPermissionRequestToList(PermissionRequestList *list, - const char *path, bool recursive, unsigned int permissions) -{ - if (list == NULL || list->numRequests < 0 || - list->requestsAllocated < list->numRequests || path == NULL) - { - return -1; - } - - if (list->numRequests == list->requestsAllocated) { - int newSize; - PermissionRequest *newRequests; - - newSize = list->requestsAllocated * 2; - if (newSize < 16) { - newSize = 16; - } - newRequests = (PermissionRequest *)realloc(list->requests, - newSize * sizeof(PermissionRequest)); - if (newRequests == NULL) { - return -2; - } - list->requests = newRequests; - list->requestsAllocated = newSize; - } - - PermissionRequest *req; - req = &list->requests[list->numRequests++]; - req->path = strdup(path); - if (req->path == NULL) { - list->numRequests--; - return -3; - } - req->recursive = recursive; - req->requested = permissions; - req->allowed = 0; - - return 0; -} - -void -freePermissionRequestListElements(PermissionRequestList *list) -{ - if (list != NULL && list->numRequests >= 0 && - list->requestsAllocated >= list->numRequests) - { - int i; - for (i = 0; i < list->numRequests; i++) { - free((void *)list->requests[i].path); - } - free(list->requests); - initPermissionRequestList(list); - } -} - -/* - * Global permission table - */ - -static struct { - Permission *permissions; - int numPermissionEntries; - int allocatedPermissionEntries; - bool permissionStateInitialized; -} gPermissionState = { -#if 1 - NULL, 0, 0, false -#else - .permissions = NULL, - .numPermissionEntries = 0, - .allocatedPermissionEntries = 0, - .permissionStateInitialized = false -#endif -}; - -int -permissionInit() -{ - if (gPermissionState.permissionStateInitialized) { - return -1; - } - gPermissionState.permissions = NULL; - gPermissionState.numPermissionEntries = 0; - gPermissionState.allocatedPermissionEntries = 0; - gPermissionState.permissionStateInitialized = true; -//xxx maybe add an "namespace root gets no permissions" fallback by default - return 0; -} - -void -permissionCleanup() -{ - if (gPermissionState.permissionStateInitialized) { - gPermissionState.permissionStateInitialized = false; - if (gPermissionState.permissions != NULL) { - int i; - for (i = 0; i < gPermissionState.numPermissionEntries; i++) { - free((void *)gPermissionState.permissions[i].path); - } - free(gPermissionState.permissions); - } - } -} - -int -getPermissionCount() -{ - if (gPermissionState.permissionStateInitialized) { - return gPermissionState.numPermissionEntries; - } - return -1; -} - -const Permission * -getPermissionAt(int index) -{ - if (!gPermissionState.permissionStateInitialized) { - return NULL; - } - if (index < 0 || index >= gPermissionState.numPermissionEntries) { - return NULL; - } - return &gPermissionState.permissions[index]; -} - -int -getAllowedPermissions(const char *path, bool recursive, - unsigned int *outAllowed) -{ - if (!gPermissionState.permissionStateInitialized) { - return -2; - } - if (outAllowed == NULL) { - return -1; - } - *outAllowed = 0; - if (path == NULL) { - return -1; - } - //TODO: implement this for real. - recursive = false; - *outAllowed = PERMSET_ALL; - return 0; -} - -int -countPermissionConflicts(PermissionRequestList *requests, bool updateAllowed) -{ - if (!gPermissionState.permissionStateInitialized) { - return -2; - } - if (requests == NULL || requests->requests == NULL || - requests->numRequests < 0 || - requests->requestsAllocated < requests->numRequests) - { - return -1; - } - int conflicts = 0; - int i; - for (i = 0; i < requests->numRequests; i++) { - PermissionRequest *req; - unsigned int allowed; - int ret; - - req = &requests->requests[i]; - ret = getAllowedPermissions(req->path, req->recursive, &allowed); - if (ret < 0) { - return ret; - } - if ((req->requested & ~allowed) != 0) { - conflicts++; - } - if (updateAllowed) { - req->allowed = allowed; - } - } - return conflicts; -} - -int -registerPermissionSet(int count, Permission *set) -{ - if (!gPermissionState.permissionStateInitialized) { - return -2; - } - if (count < 0 || (count > 0 && set == NULL)) { - return -1; - } - if (count == 0) { - return 0; - } - - if (gPermissionState.numPermissionEntries + count >= - gPermissionState.allocatedPermissionEntries) - { - Permission *newList; - int newSize; - - newSize = (gPermissionState.allocatedPermissionEntries + count) * 2; - if (newSize < 16) { - newSize = 16; - } - newList = (Permission *)realloc(gPermissionState.permissions, - newSize * sizeof(Permission)); - if (newList == NULL) { - return -3; - } - gPermissionState.permissions = newList; - gPermissionState.allocatedPermissionEntries = newSize; - } - - Permission *p = &gPermissionState.permissions[ - gPermissionState.numPermissionEntries]; - int i; - for (i = 0; i < count; i++) { - *p = set[i]; - //TODO: cache the strlen of the path - //TODO: normalize; strip off trailing / - p->path = strdup(p->path); - if (p->path == NULL) { - /* If we can't add all of the entries, we don't - * add any of them. - */ - Permission *pp = &gPermissionState.permissions[ - gPermissionState.numPermissionEntries]; - while (pp != p) { - free((void *)pp->path); - pp++; - } - return -4; - } - p++; - } - gPermissionState.numPermissionEntries += count; - - return 0; -} diff --git a/amend/permissions.h b/amend/permissions.h deleted file mode 100644 index 5b1d14d..0000000 --- a/amend/permissions.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2007 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. - */ - -#ifndef AMEND_PERMISSIONS_H_ -#define AMEND_PERMISSIONS_H_ - -#include - -#define PERM_NONE (0) -#define PERM_STAT (1<<0) -#define PERM_READ (1<<1) -#define PERM_WRITE (1<<2) // including create, delete, mkdir, rmdir -#define PERM_CHMOD (1<<3) -#define PERM_CHOWN (1<<4) -#define PERM_CHGRP (1<<5) -#define PERM_SETUID (1<<6) -#define PERM_SETGID (1<<7) - -#define PERMSET_READ (PERM_STAT | PERM_READ) -#define PERMSET_WRITE (PERMSET_READ | PERM_WRITE) - -#define PERMSET_ALL \ - (PERM_STAT | PERM_READ | PERM_WRITE | PERM_CHMOD | \ - PERM_CHOWN | PERM_CHGRP | PERM_SETUID | PERM_SETGID) - -typedef struct { - unsigned int requested; - unsigned int allowed; - const char *path; - bool recursive; -} PermissionRequest; - -typedef struct { - PermissionRequest *requests; - int numRequests; - int requestsAllocated; -} PermissionRequestList; - -/* Properly clear out a PermissionRequestList. - * - * @return 0 if list is non-NULL, negative otherwise. - */ -int initPermissionRequestList(PermissionRequestList *list); - -/* Add a permission request to the list, allocating more space - * if necessary. - * - * @return 0 on success or a negative value on failure. - */ -int addPermissionRequestToList(PermissionRequestList *list, - const char *path, bool recursive, unsigned int permissions); - -/* Free anything allocated by addPermissionRequestToList(). The caller - * is responsible for freeing the actual PermissionRequestList. - */ -void freePermissionRequestListElements(PermissionRequestList *list); - - -/* - * Global permission table - */ - -typedef struct { - const char *path; - unsigned int allowed; -} Permission; - -int permissionInit(void); -void permissionCleanup(void); - -/* Returns the allowed permissions for the path in "outAllowed". - * Returns 0 if successful, negative if a parameter or global state - * is bad. - */ -int getAllowedPermissions(const char *path, bool recursive, - unsigned int *outAllowed); - -/* More-recently-registered permissions override older permissions. - */ -int registerPermissionSet(int count, Permission *set); - -/* Check to make sure that each request is allowed. - * - * @param requests The list of permission requests - * @param updateAllowed If true, update the "allowed" field in each - * element of the list - * @return the number of requests that were denied, or negative if - * an error occurred. - */ -int countPermissionConflicts(PermissionRequestList *requests, - bool updateAllowed); - -/* Inspection/testing/debugging functions - */ -int getPermissionCount(void); -const Permission *getPermissionAt(int index); - -#endif // AMEND_PERMISSIONS_H_ diff --git a/amend/register.c b/amend/register.c index 167dd32..0f44b74 100644 --- a/amend/register.c +++ b/amend/register.c @@ -39,40 +39,15 @@ if (argc < 0) return -1; \ assert(argc == 0 || argv != NULL); \ if (argc != 0 && argv == NULL) return -1; \ - if (permissions != NULL) { \ - int CW_I_; \ - for (CW_I_ = 0; CW_I_ < argc; CW_I_++) { \ - assert(argv[CW_I_] != NULL); \ - if (argv[CW_I_] == NULL) return -1; \ - } \ - } \ } while (false) #define CHECK_FN() \ do { \ CHECK_WORDS(); \ - if (permissions != NULL) { \ - assert(result == NULL); \ - if (result != NULL) return -1; \ - } else { \ - assert(result != NULL); \ - if (result == NULL) return -1; \ - } \ + assert(result != NULL); \ + if (result == NULL) return -1; \ } while (false) -#define NO_PERMS(perms) \ - do { \ - PermissionRequestList *NP_PRL_ = (perms); \ - if (NP_PRL_ != NULL) { \ - int NP_RET_ = addPermissionRequestToList(NP_PRL_, \ - "", false, PERM_NONE); \ - if (NP_RET_ < 0) { \ - /* Returns from the calling function. \ - */ \ - return NP_RET_; \ - } \ - } \ - } while (false) /* * Command definitions @@ -81,13 +56,11 @@ /* assert */ static int -cmd_assert(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_assert(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); CHECK_BOOL(); - NO_PERMS(permissions); /* If our argument is false, return non-zero (failure) * If our argument is true, return zero (success) @@ -102,8 +75,7 @@ cmd_assert(const char *name, void *cookie, int argc, const char *argv[], /* format */ static int -cmd_format(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_format(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -115,8 +87,7 @@ cmd_format(const char *name, void *cookie, int argc, const char *argv[], /* copy_dir */ static int -cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -128,8 +99,7 @@ cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[], /* mark dirty|clean */ static int -cmd_mark(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_mark(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -144,8 +114,7 @@ cmd_mark(const char *name, void *cookie, int argc, const char *argv[], /* done */ static int -cmd_done(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_done(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -174,11 +143,6 @@ registerUpdateCommands() ret = registerCommand("done", CMD_ARGS_WORDS, cmd_done, NULL); if (ret < 0) return ret; -//xxx some way to fix permissions -//xxx could have "installperms" commands that build the fs_config list -//xxx along with a "commitperms", and any copy_dir etc. needs to see -// a commitperms before it will work - return 0; } @@ -194,13 +158,11 @@ registerUpdateCommands() */ static int fn_update_forced(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 0) { fprintf(stderr, "%s: wrong number of arguments (%d)\n", @@ -228,13 +190,11 @@ fn_update_forced(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_get_mark(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 1) { fprintf(stderr, "%s: wrong number of arguments (%d)\n", @@ -255,8 +215,7 @@ fn_get_mark(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { int ret = -1; @@ -273,23 +232,12 @@ fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], dir = argv[0]; } - if (permissions != NULL) { - if (dir == NULL) { - /* The argument is the result of another function. - * Assume the worst case, where the function returns - * the root. - */ - dir = "/"; - } - ret = addPermissionRequestToList(permissions, dir, true, PERM_READ); - } else { //xxx build and return the string - *result = strdup("hashvalue"); - if (resultLen != NULL) { - *resultLen = strlen(*result); - } - ret = 0; + *result = strdup("hashvalue"); + if (resultLen != NULL) { + *resultLen = strlen(*result); } + ret = 0; return ret; } @@ -302,13 +250,11 @@ fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_matches(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc < 2) { fprintf(stderr, "%s: not enough arguments (%d < 2)\n", @@ -339,13 +285,11 @@ fn_matches(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_concat(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); size_t totalLen = 0; int i; diff --git a/amend/test_commands.c b/amend/test_commands.c index be938ac..452f808 100644 --- a/amend/test_commands.c +++ b/amend/test_commands.c @@ -27,34 +27,30 @@ static struct { void *cookie; int argc; const char **argv; - PermissionRequestList *permissions; int returnValue; char *functionResult; } gTestCommandState; static int -testCommand(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +testCommand(const char *name, void *cookie, int argc, const char *argv[]) { gTestCommandState.called = true; gTestCommandState.name = name; gTestCommandState.cookie = cookie; gTestCommandState.argc = argc; gTestCommandState.argv = argv; - gTestCommandState.permissions = permissions; return gTestCommandState.returnValue; } static int testFunction(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, PermissionRequestList *permissions) + char **result, size_t *resultLen) { gTestCommandState.called = true; gTestCommandState.name = name; gTestCommandState.cookie = cookie; gTestCommandState.argc = argc; gTestCommandState.argv = argv; - gTestCommandState.permissions = permissions; if (result != NULL) { *result = gTestCommandState.functionResult; if (resultLen != NULL) { @@ -187,7 +183,6 @@ test_commands() memset(&gTestCommandState, 0, sizeof(gTestCommandState)); gTestCommandState.called = false; gTestCommandState.returnValue = 25; - gTestCommandState.permissions = (PermissionRequestList *)1; ret = callCommand(cmd, argc, argv); //xxx also try calling with a null argv element (should fail) assert(ret == 25); @@ -196,7 +191,6 @@ test_commands() assert(gTestCommandState.cookie == &gTestCommandState); assert(gTestCommandState.argc == argc); assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == NULL); /* Make a boolean call and make sure that it occurred. */ @@ -206,7 +200,6 @@ test_commands() memset(&gTestCommandState, 0, sizeof(gTestCommandState)); gTestCommandState.called = false; gTestCommandState.returnValue = 12; - gTestCommandState.permissions = (PermissionRequestList *)1; ret = callBooleanCommand(cmd, false); assert(ret == 12); assert(gTestCommandState.called); @@ -214,12 +207,10 @@ test_commands() assert(gTestCommandState.cookie == &gTestCommandState); assert(gTestCommandState.argc == 0); assert(gTestCommandState.argv == NULL); - assert(gTestCommandState.permissions == NULL); memset(&gTestCommandState, 0, sizeof(gTestCommandState)); gTestCommandState.called = false; gTestCommandState.returnValue = 13; - gTestCommandState.permissions = (PermissionRequestList *)1; ret = callBooleanCommand(cmd, true); assert(ret == 13); assert(gTestCommandState.called); @@ -227,45 +218,6 @@ test_commands() assert(gTestCommandState.cookie == &gTestCommandState); assert(gTestCommandState.argc == 1); assert(gTestCommandState.argv == NULL); - assert(gTestCommandState.permissions == NULL); - - /* Try looking up permissions. - */ - PermissionRequestList permissions; - cmd = findCommand("one"); - assert(cmd != NULL); - memset(&gTestCommandState, 0, sizeof(gTestCommandState)); - gTestCommandState.called = false; - gTestCommandState.returnValue = 27; - gTestCommandState.permissions = (PermissionRequestList *)1; - argv[1] = NULL; // null out an arg, which should be ok - ret = getCommandPermissions(cmd, argc, argv, &permissions); - assert(ret == 27); - assert(gTestCommandState.called); - assert(strcmp(gTestCommandState.name, "one") == 0); - assert(gTestCommandState.cookie == &gTestCommandState); - assert(gTestCommandState.argc == argc); - assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == &permissions); - - /* Boolean command permissions - */ - cmd = findCommand("bool"); - assert(cmd != NULL); - memset(&gTestCommandState, 0, sizeof(gTestCommandState)); - gTestCommandState.called = false; - gTestCommandState.returnValue = 55; - gTestCommandState.permissions = (PermissionRequestList *)1; - // argv[1] is still NULL - ret = getBooleanCommandPermissions(cmd, true, &permissions); - assert(ret == 55); - assert(gTestCommandState.called); - assert(strcmp(gTestCommandState.name, "bool") == 0); - assert(gTestCommandState.cookie == &gTestCommandState); - assert(gTestCommandState.argc == 1); - assert(gTestCommandState.argv == NULL); - assert(gTestCommandState.permissions == &permissions); - /* Smoke test commandCleanup(). */ @@ -365,7 +317,6 @@ test_functions() gTestCommandState.called = false; gTestCommandState.returnValue = 25; gTestCommandState.functionResult = "1234"; - gTestCommandState.permissions = (PermissionRequestList *)1; functionResult = NULL; functionResultLen = 55; ret = callFunction(fn, argc, argv, @@ -378,29 +329,9 @@ test_functions() assert(gTestCommandState.cookie == &gTestCommandState); assert(gTestCommandState.argc == argc); assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == NULL); assert(strcmp(functionResult, "1234") == 0); assert(functionResultLen == strlen(functionResult)); - /* Try looking up permissions. - */ - PermissionRequestList permissions; - fn = findFunction("one"); - assert(fn != NULL); - memset(&gTestCommandState, 0, sizeof(gTestCommandState)); - gTestCommandState.called = false; - gTestCommandState.returnValue = 27; - gTestCommandState.permissions = (PermissionRequestList *)1; - argv[1] = NULL; // null out an arg, which should be ok - ret = getFunctionPermissions(fn, argc, argv, &permissions); - assert(ret == 27); - assert(gTestCommandState.called); - assert(strcmp(gTestCommandState.name, "one") == 0); - assert(gTestCommandState.cookie == &gTestCommandState); - assert(gTestCommandState.argc == argc); - assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == &permissions); - /* Smoke test commandCleanup(). */ commandCleanup(); @@ -470,7 +401,6 @@ test_interaction() memset(&gTestCommandState, 0, sizeof(gTestCommandState)); gTestCommandState.called = false; gTestCommandState.returnValue = 123; - gTestCommandState.permissions = (PermissionRequestList *)1; ret = callCommand(cmd, argc, argv); assert(ret == 123); assert(gTestCommandState.called); @@ -478,7 +408,6 @@ test_interaction() assert((int)gTestCommandState.cookie == 0xc1); assert(gTestCommandState.argc == argc); assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == NULL); /* Call the overlapping function and make sure that the cookie is correct. */ @@ -490,7 +419,6 @@ test_interaction() gTestCommandState.called = false; gTestCommandState.returnValue = 125; gTestCommandState.functionResult = "5678"; - gTestCommandState.permissions = (PermissionRequestList *)2; functionResult = NULL; functionResultLen = 66; ret = callFunction(fn, argc, argv, &functionResult, &functionResultLen); @@ -500,7 +428,6 @@ test_interaction() assert((int)gTestCommandState.cookie == 0xf1); assert(gTestCommandState.argc == argc); assert(gTestCommandState.argv == argv); - assert(gTestCommandState.permissions == NULL); assert(strcmp(functionResult, "5678") == 0); assert(functionResultLen == strlen(functionResult)); diff --git a/amend/test_permissions.c b/amend/test_permissions.c deleted file mode 100644 index c389456..0000000 --- a/amend/test_permissions.c +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright (C) 2007 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 -#include -#include -#undef NDEBUG -#include -#include "permissions.h" - -static int -test_permission_list() -{ - PermissionRequestList list; - int ret; - int numRequests; - - /* Bad parameter - */ - ret = initPermissionRequestList(NULL); - assert(ret < 0); - - /* Good parameter - */ - ret = initPermissionRequestList(&list); - assert(ret == 0); - - /* Bad parameters - */ - ret = addPermissionRequestToList(NULL, NULL, false, 0); - assert(ret < 0); - - ret = addPermissionRequestToList(&list, NULL, false, 0); - assert(ret < 0); - - /* Good parameters - */ - numRequests = 0; - - ret = addPermissionRequestToList(&list, "one", false, 1); - assert(ret == 0); - numRequests++; - - ret = addPermissionRequestToList(&list, "two", false, 2); - assert(ret == 0); - numRequests++; - - ret = addPermissionRequestToList(&list, "three", false, 3); - assert(ret == 0); - numRequests++; - - ret = addPermissionRequestToList(&list, "recursive", true, 55); - assert(ret == 0); - numRequests++; - - /* Validate the list - */ - assert(list.requests != NULL); - assert(list.numRequests == numRequests); - assert(list.numRequests <= list.requestsAllocated); - bool sawOne = false; - bool sawTwo = false; - bool sawThree = false; - bool sawRecursive = false; - int i; - for (i = 0; i < list.numRequests; i++) { - PermissionRequest *req = &list.requests[i]; - assert(req->allowed == 0); - - /* Order isn't guaranteed, so we have to switch every time. - */ - if (strcmp(req->path, "one") == 0) { - assert(!sawOne); - assert(req->requested == 1); - assert(!req->recursive); - sawOne = true; - } else if (strcmp(req->path, "two") == 0) { - assert(!sawTwo); - assert(req->requested == 2); - assert(!req->recursive); - sawTwo = true; - } else if (strcmp(req->path, "three") == 0) { - assert(!sawThree); - assert(req->requested == 3); - assert(!req->recursive); - sawThree = true; - } else if (strcmp(req->path, "recursive") == 0) { - assert(!sawRecursive); - assert(req->requested == 55); - assert(req->recursive); - sawRecursive = true; - } else { - assert(false); - } - } - assert(sawOne); - assert(sawTwo); - assert(sawThree); - assert(sawRecursive); - - /* Smoke test the teardown - */ - freePermissionRequestListElements(&list); - - return 0; -} - -static int -test_permission_table() -{ - int ret; - - /* Test the global permissions table. - * Try calling functions without initializing first. - */ - ret = registerPermissionSet(0, NULL); - assert(ret < 0); - - ret = countPermissionConflicts((PermissionRequestList *)16, false); - assert(ret < 0); - - ret = getPermissionCount(); - assert(ret < 0); - - const Permission *p; - p = getPermissionAt(0); - assert(p == NULL); - - /* Initialize. - */ - ret = permissionInit(); - assert(ret == 0); - - /* Make sure we can't initialize twice. - */ - ret = permissionInit(); - assert(ret < 0); - - /* Test the inspection functions. - */ - ret = getPermissionCount(); - assert(ret == 0); - - p = getPermissionAt(-1); - assert(p == NULL); - - p = getPermissionAt(0); - assert(p == NULL); - - p = getPermissionAt(1); - assert(p == NULL); - - /* Test registerPermissionSet(). - * Try some bad parameter values. - */ - ret = registerPermissionSet(-1, NULL); - assert(ret < 0); - - ret = registerPermissionSet(1, NULL); - assert(ret < 0); - - /* Register some permissions. - */ - Permission p1; - p1.path = "one"; - p1.allowed = 1; - ret = registerPermissionSet(1, &p1); - assert(ret == 0); - ret = getPermissionCount(); - assert(ret == 1); - - Permission p2[2]; - p2[0].path = "two"; - p2[0].allowed = 2; - p2[1].path = "three"; - p2[1].allowed = 3; - ret = registerPermissionSet(2, p2); - assert(ret == 0); - ret = getPermissionCount(); - assert(ret == 3); - - ret = registerPermissionSet(0, NULL); - assert(ret == 0); - ret = getPermissionCount(); - assert(ret == 3); - - p1.path = "four"; - p1.allowed = 4; - ret = registerPermissionSet(1, &p1); - assert(ret == 0); - - /* Make sure the table looks correct. - * Order is important; more-recent additions - * should appear at higher indices. - */ - ret = getPermissionCount(); - assert(ret == 4); - - int i; - for (i = 0; i < ret; i++) { - const Permission *p; - p = getPermissionAt(i); - assert(p != NULL); - assert(p->allowed == (unsigned int)(i + 1)); - switch (i) { - case 0: - assert(strcmp(p->path, "one") == 0); - break; - case 1: - assert(strcmp(p->path, "two") == 0); - break; - case 2: - assert(strcmp(p->path, "three") == 0); - break; - case 3: - assert(strcmp(p->path, "four") == 0); - break; - default: - assert(!"internal error"); - break; - } - } - p = getPermissionAt(ret); - assert(p == NULL); - - /* Smoke test the teardown - */ - permissionCleanup(); - - return 0; -} - -static int -test_allowed_permissions() -{ - int ret; - int numPerms; - - /* Make sure these fail before initialization. - */ - ret = countPermissionConflicts((PermissionRequestList *)1, false); - assert(ret < 0); - - ret = getAllowedPermissions((const char *)1, false, (unsigned int *)1); - assert(ret < 0); - - /* Initialize. - */ - ret = permissionInit(); - assert(ret == 0); - - /* Make sure countPermissionConflicts() fails with bad parameters. - */ - ret = countPermissionConflicts(NULL, false); - assert(ret < 0); - - /* Register a set of permissions. - */ - Permission perms[] = { - { "/", PERM_NONE }, - { "/stat", PERM_STAT }, - { "/read", PERMSET_READ }, - { "/write", PERMSET_WRITE }, - { "/.stat", PERM_STAT }, - { "/.stat/.read", PERMSET_READ }, - { "/.stat/.read/.write", PERMSET_WRITE }, - { "/.stat/.write", PERMSET_WRITE }, - }; - numPerms = sizeof(perms) / sizeof(perms[0]); - ret = registerPermissionSet(numPerms, perms); - assert(ret == 0); - - /* Build a permission request list. - */ - PermissionRequestList list; - ret = initPermissionRequestList(&list); - assert(ret == 0); - - ret = addPermissionRequestToList(&list, "/stat", false, PERM_STAT); - assert(ret == 0); - - ret = addPermissionRequestToList(&list, "/read", false, PERM_READ); - assert(ret == 0); - - ret = addPermissionRequestToList(&list, "/write", false, PERM_WRITE); - assert(ret == 0); - - //TODO: cover more cases once the permission stuff has been implemented - - /* All of the requests in the list should be allowed. - */ - ret = countPermissionConflicts(&list, false); - assert(ret == 0); - - /* Add a request that will be denied. - */ - ret = addPermissionRequestToList(&list, "/stat", false, 1<<31 | PERM_STAT); - assert(ret == 0); - - ret = countPermissionConflicts(&list, false); - assert(ret == 1); - - //TODO: more tests - - permissionCleanup(); - - return 0; -} - -int -test_permissions() -{ - int ret; - - ret = test_permission_list(); - if (ret != 0) { - fprintf(stderr, "test_permission_list() failed: %d\n", ret); - return ret; - } - - ret = test_permission_table(); - if (ret != 0) { - fprintf(stderr, "test_permission_table() failed: %d\n", ret); - return ret; - } - - ret = test_allowed_permissions(); - if (ret != 0) { - fprintf(stderr, "test_permission_table() failed: %d\n", ret); - return ret; - } - - return 0; -} diff --git a/commands.c b/commands.c index 23ad91c..b4678ba 100644 --- a/commands.c +++ b/commands.c @@ -57,39 +57,13 @@ static int gDidShowProgress = 0; if (argc < 0) return -1; \ assert(argc == 0 || argv != NULL); \ if (argc != 0 && argv == NULL) return -1; \ - if (permissions != NULL) { \ - int CW_I_; \ - for (CW_I_ = 0; CW_I_ < argc; CW_I_++) { \ - assert(argv[CW_I_] != NULL); \ - if (argv[CW_I_] == NULL) return -1; \ - } \ - } \ } while (false) #define CHECK_FN() \ do { \ CHECK_WORDS(); \ - if (permissions != NULL) { \ - assert(result == NULL); \ - if (result != NULL) return -1; \ - } else { \ - assert(result != NULL); \ - if (result == NULL) return -1; \ - } \ - } while (false) - -#define NO_PERMS(perms) \ - do { \ - PermissionRequestList *NP_PRL_ = (perms); \ - if (NP_PRL_ != NULL) { \ - int NP_RET_ = addPermissionRequestToList(NP_PRL_, \ - "", false, PERM_NONE); \ - if (NP_RET_ < 0) { \ - /* Returns from the calling function. \ - */ \ - return NP_RET_; \ - } \ - } \ + assert(result != NULL); \ + if (result == NULL) return -1; \ } while (false) /* @@ -99,13 +73,11 @@ static int gDidShowProgress = 0; /* assert */ static int -cmd_assert(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_assert(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); CHECK_BOOL(); - NO_PERMS(permissions); /* If our argument is false, return non-zero (failure) * If our argument is true, return zero (success) @@ -120,8 +92,7 @@ cmd_assert(const char *name, void *cookie, int argc, const char *argv[], /* format */ static int -cmd_format(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_format(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -151,8 +122,7 @@ cmd_format(const char *name, void *cookie, int argc, const char *argv[], * give up early. */ static int -cmd_delete(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_delete(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -166,7 +136,6 @@ cmd_delete(const char *name, void *cookie, int argc, const char *argv[], recurse = (strcmp(name, "delete_recursive") == 0); ui_print("Deleting files...\n"); -//xxx permissions int i; for (i = 0; i < argc; i++) { @@ -233,13 +202,11 @@ static void extract_cb(const char *fn, void *cookie) * or a fixed default timestamp will be supplied otherwise. */ static int -cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); CHECK_WORDS(); -//xxx permissions // To create a consistent system image, never use the clock for timestamps. struct utimbuf timestamp = { 1217592000, 1217592000 }; // 8/1/2008 default @@ -331,8 +298,7 @@ cmd_copy_dir(const char *name, void *cookie, int argc, const char *argv[], * Run an external program included in the update package. */ static int -cmd_run_program(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_run_program(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -407,8 +373,7 @@ cmd_run_program(const char *name, void *cookie, int argc, const char *argv[], * User, group, and modes must all be integer values (hex or octal OK). */ static int -cmd_set_perm(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_set_perm(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -461,8 +426,7 @@ cmd_set_perm(const char *name, void *cookie, int argc, const char *argv[], * if the actual rate of progress can be determined). */ static int -cmd_show_progress(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_show_progress(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -499,8 +463,7 @@ cmd_show_progress(const char *name, void *cookie, int argc, const char *argv[], * for the target filesystem (and may be relative). */ static int -cmd_symlink(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_symlink(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -554,7 +517,7 @@ static bool firmware_fn(const unsigned char *data, int data_len, void *cookie) */ static int cmd_write_firmware_image(const char *name, void *cookie, - int argc, const char *argv[], PermissionRequestList *permissions) + int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); @@ -634,11 +597,10 @@ static bool write_raw_image_process_fn( */ static int cmd_write_raw_image(const char *name, void *cookie, - int argc, const char *argv[], PermissionRequestList *permissions) + int argc, const char *argv[]) { UNUSED(cookie); CHECK_WORDS(); -//xxx permissions if (argc != 2) { LOGE("Command %s requires exactly two arguments\n", name); @@ -726,8 +688,7 @@ cmd_write_raw_image(const char *name, void *cookie, /* mark dirty|clean */ static int -cmd_mark(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_mark(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -742,8 +703,7 @@ cmd_mark(const char *name, void *cookie, int argc, const char *argv[], /* done */ static int -cmd_done(const char *name, void *cookie, int argc, const char *argv[], - PermissionRequestList *permissions) +cmd_done(const char *name, void *cookie, int argc, const char *argv[]) { UNUSED(name); UNUSED(cookie); @@ -764,13 +724,11 @@ cmd_done(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_compatible_with(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 1) { fprintf(stderr, "%s: wrong number of arguments (%d)\n", @@ -796,13 +754,11 @@ fn_compatible_with(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_update_forced(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 0) { fprintf(stderr, "%s: wrong number of arguments (%d)\n", @@ -830,13 +786,11 @@ fn_update_forced(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_get_mark(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 1) { fprintf(stderr, "%s: wrong number of arguments (%d)\n", @@ -857,8 +811,7 @@ fn_get_mark(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { int ret = -1; @@ -875,24 +828,6 @@ fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], dir = argv[0]; } - if (permissions != NULL) { - if (dir == NULL) { - /* The argument is the result of another function. - * Assume the worst case, where the function returns - * the root. - */ - dir = "/"; - } - ret = addPermissionRequestToList(permissions, dir, true, PERM_READ); - } else { -//xxx build and return the string - *result = strdup("hashvalue"); - if (resultLen != NULL) { - *resultLen = strlen(*result); - } - ret = 0; - } - return ret; } @@ -904,13 +839,11 @@ fn_hash_dir(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_matches(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc < 2) { fprintf(stderr, "%s: not enough arguments (%d < 2)\n", @@ -941,13 +874,11 @@ fn_matches(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_concat(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(name); UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); size_t totalLen = 0; int i; @@ -977,12 +908,10 @@ fn_concat(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_getprop(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 1) { LOGE("Command %s requires exactly one argument\n", name); @@ -1005,12 +934,10 @@ fn_getprop(const char *name, void *cookie, int argc, const char *argv[], */ static int fn_file_contains(const char *name, void *cookie, int argc, const char *argv[], - char **result, size_t *resultLen, - PermissionRequestList *permissions) + char **result, size_t *resultLen) { UNUSED(cookie); CHECK_FN(); - NO_PERMS(permissions); if (argc != 2) { LOGE("Command %s requires exactly two arguments\n", name); diff --git a/recovery.c b/recovery.c index e329db9..188d4de 100644 --- a/recovery.c +++ b/recovery.c @@ -265,7 +265,6 @@ test_amend() { extern int test_symtab(void); extern int test_cmd_fn(void); - extern int test_permissions(void); int ret; LOGD("Testing symtab...\n"); ret = test_symtab(); @@ -273,9 +272,6 @@ test_amend() LOGD("Testing cmd_fn...\n"); ret = test_cmd_fn(); LOGD(" returned %d\n", ret); - LOGD("Testing permissions...\n"); - ret = test_permissions(); - LOGD(" returned %d\n", ret); } #endif // TEST_AMEND From b2ee9201be583b17ddbf0eaa69a37545f992b565 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 4 Jun 2009 10:24:53 -0700 Subject: [PATCH 12/24] allow OTA package to provide binary instead of script Allow installation of OTA packages which do not contain an update-script, but instead contain an update-binary. --- install.c | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/install.c b/install.c index e7db2a8..eff9312 100644 --- a/install.c +++ b/install.c @@ -14,10 +14,13 @@ * limitations under the License. */ +#include #include #include #include #include +#include +#include #include "amend/amend.h" #include "common.h" @@ -30,8 +33,10 @@ #include "mtdutils/mtdutils.h" #include "roots.h" #include "verifier.h" +#include "firmware.h" #define ASSUMED_UPDATE_SCRIPT_NAME "META-INF/com/google/android/update-script" +#define ASSUMED_UPDATE_BINARY_NAME "META-INF/com/google/android/update-binary" #define PUBLIC_KEYS_FILE "/res/keys" static const ZipEntry * @@ -95,7 +100,7 @@ handle_update_script(ZipArchive *zip, const ZipEntry *update_script_entry) int ret = execCommandList((ExecContext *)1, commands); if (ret != 0) { int num = ret; - char *line, *next = script_data; + char *line = NULL, *next = script_data; while (next != NULL && ret-- > 0) { line = next; next = memchr(line, '\n', script_data + script_len - line); @@ -109,6 +114,159 @@ handle_update_script(ZipArchive *zip, const ZipEntry *update_script_entry) return INSTALL_SUCCESS; } +// The update binary ask us to install a firmware file on reboot. Set +// that up. Takes ownership of type and filename. +static int +handle_firmware_update(char* type, char* filename) { + struct stat st_data; + if (stat(filename, &st_data) < 0) { + LOGE("Error stat'ing %s: %s\n", filename, strerror(errno)); + return INSTALL_ERROR; + } + + LOGI("type is [%s]\n", type); + + char* data = malloc(st_data.st_size); + if (data == NULL) { + LOGE("Can't allocate %d bytes for firmware data\n", st_data.st_size); + return INSTALL_ERROR; + } + + FILE* f = fopen(filename, "rb"); + if (f == NULL) { + LOGE("Failed to open %s: %s\n", filename, strerror(errno)); + return INSTALL_ERROR; + } + if (fread(data, 1, st_data.st_size, f) != st_data.st_size) { + LOGE("Failed to read firmware data: %s\n", strerror(errno)); + return INSTALL_ERROR; + } + fclose(f); + + if (remember_firmware_update(type, data, st_data.st_size)) { + LOGE("Can't store %s image\n", type); + free(data); + return INSTALL_ERROR; + } + free(filename); + + return INSTALL_SUCCESS; +} + +// If the package contains an update binary, extract it and run it. +static int +try_update_binary(const char *path, ZipArchive *zip) { + const ZipEntry* binary_entry = + mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME); + if (binary_entry == NULL) { + return INSTALL_CORRUPT; + } + + char* binary = "/tmp/update_binary"; + unlink(binary); + int fd = creat(binary, 0755); + if (fd < 0) { + LOGE("Can't make %s\n", binary); + return 1; + } + bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd); + close(fd); + + if (!ok) { + LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME); + return 1; + } + + int pipefd[2]; + pipe(pipefd); + + // When executing the update binary contained in the package, the + // arguments passed are: + // + // - the version number for this interface (currently 1) + // + // - an fd to which the program can write in order to update the + // progress bar. The program can write single-line commands: + // + // progress + // fill up of the progress bar over seconds. + // + // firmware <"hboot"|"radio"> + // arrange to install the contents of in the + // given partition on reboot. + // + // - the name of the package zip file. + // + + char** args = malloc(sizeof(char*) * 5); + args[0] = binary; + args[1] = "1"; + args[2] = malloc(10); + sprintf(args[2], "%d", pipefd[1]); + args[3] = (char*)path; + args[4] = NULL; + + pid_t pid = fork(); + if (pid == 0) { + close(pipefd[0]); + execv(binary, args); + fprintf(stderr, "E:Can't run %s (%s)\n", binary, strerror(errno)); + _exit(-1); + } + close(pipefd[1]); + + char* firmware_type = NULL; + char* firmware_filename = NULL; + + char buffer[81]; + FILE* from_child = fdopen(pipefd[0], "r"); + while (fgets(buffer, sizeof(buffer), from_child) != NULL) { + LOGI("read: %s", buffer); + + char* command = strtok(buffer, " \n"); + if (command == NULL) { + continue; + } else if (strcmp(command, "progress") == 0) { + char* fraction_s = strtok(NULL, " \n"); + char* seconds_s = strtok(NULL, " \n"); + + float fraction = strtof(fraction_s, NULL); + int seconds = strtol(seconds_s, NULL, 10); + + ui_show_progress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), + seconds); + } else if (strcmp(command, "firmware") == 0) { + char* type = strtok(NULL, " \n"); + char* filename = strtok(NULL, " \n"); + + if (type != NULL && filename != NULL) { + if (firmware_type != NULL) { + LOGE("ignoring attempt to do multiple firmware updates"); + } else { + firmware_type = strdup(type); + firmware_filename = strdup(filename); + } + } + } else { + LOGE("unknown command [%s]\n", command); + } + } + fclose(from_child); + + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + LOGE("Error in %s\n(Status %d)\n", path, status); + return INSTALL_ERROR; + } + + if (firmware_type != NULL) { + return handle_firmware_update(firmware_type, firmware_filename); + } else { + return INSTALL_SUCCESS; + } +} + static int handle_update_package(const char *path, ZipArchive *zip, const RSAPublicKey *keys, int numKeys) @@ -127,6 +285,16 @@ handle_update_package(const char *path, ZipArchive *zip, // Update should take the rest of the progress bar. ui_print("Installing update...\n"); + int result = try_update_binary(path, zip); + if (result == INSTALL_SUCCESS || result == INSTALL_ERROR) { + register_package_root(NULL, NULL); // Unregister package root + return result; + } + + // if INSTALL_CORRUPT is returned, this package doesn't have an + // update binary. Fall back to the older mechanism of looking for + // an update script. + const ZipEntry *script_entry; script_entry = find_update_script(zip); if (script_entry == NULL) { From 37bee62aefb5ac10716054edf482fd37078f1427 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Mon, 8 Jun 2009 17:35:39 -0700 Subject: [PATCH 13/24] core of edify, an eventual replacement for amend Edify is a simple scripting language for OTA installation, to be used when we move to OTAs being installed via binaries shipped with the package. --- edify/Android.mk | 40 +++++++ edify/README | 108 ++++++++++++++++++ edify/expr.c | 279 +++++++++++++++++++++++++++++++++++++++++++++++ edify/expr.h | 80 ++++++++++++++ edify/lexer.l | 97 ++++++++++++++++ edify/main.c | 164 ++++++++++++++++++++++++++++ edify/parser.y | 121 ++++++++++++++++++++ 7 files changed, 889 insertions(+) create mode 100644 edify/Android.mk create mode 100644 edify/README create mode 100644 edify/expr.c create mode 100644 edify/expr.h create mode 100644 edify/lexer.l create mode 100644 edify/main.c create mode 100644 edify/parser.y diff --git a/edify/Android.mk b/edify/Android.mk new file mode 100644 index 0000000..803fba2 --- /dev/null +++ b/edify/Android.mk @@ -0,0 +1,40 @@ +# Copyright 2009 The Android Open Source Project + +LOCAL_PATH := $(call my-dir) + +edify_src_files := \ + lexer.l \ + parser.y \ + expr.c + +# "-x c" forces the lex/yacc files to be compiled as c; +# the build system otherwise forces them to be c++. +edify_cflags := -x c + +# +# Build the host-side command line tool +# +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + $(edify_src_files) \ + main.c + +LOCAL_CFLAGS := $(edify_cflags) -g -O0 +LOCAL_MODULE := edify +LOCAL_YACCFLAGS := -v + +include $(BUILD_HOST_EXECUTABLE) + +# # +# # Build the device-side library +# # +# include $(CLEAR_VARS) + +# LOCAL_SRC_FILES := $(edify_src_files) +# LOCAL_SRC_FILES += $(edify_test_files) + +# LOCAL_CFLAGS := $(edify_cflags) +# LOCAL_MODULE := libedify + +# include $(BUILD_STATIC_LIBRARY) diff --git a/edify/README b/edify/README new file mode 100644 index 0000000..5ccb582 --- /dev/null +++ b/edify/README @@ -0,0 +1,108 @@ +Update scripts (from donut onwards) are written in a new little +scripting language ("edify") that is superficially somewhat similar to +the old one ("amend"). This is a brief overview of the new language. + +- The entire script is a single expression. + +- All expressions are string-valued. + +- String literals appear in double quotes. \n, \t, \", and \\ are + understood, as are hexadecimal escapes like \x4a. + +- String literals consisting of only letters, numbers, colons, + underscores, and slashes don't need to be in double quotes. + +- The following words are reserved: + + if then else endif + + They have special meaning when unquoted. (In quotes, they are just + string literals.) + +- When used as a boolean, the empty string is "false" and all other + strings are "true". + +- All functions are actually macros (in the Lisp sense); the body of + the function can control which (if any) of the arguments are + evaluated. This means that functions can act as control + structures. + +- Operators (like "&&" and "||") are just syntactic sugar for builtin + functions, so they can act as control structures as well. + +- ";" is a binary operator; evaluating it just means to first evaluate + the left side, then the right. It can also appear after any + expression. + +- Comments start with "#" and run to the end of the line. + + + +Some examples: + +- There's no distinction between quoted and unquoted strings; the + quotes are only needed if you want characters like whitespace to + appear in the string. The following expressions all evaluate to the + same string. + + "a b" + a + " " + b + "a" + " " + "b" + "a\x20b" + a + "\x20b" + concat(a, " ", "b") + "concat"(a, " ", "b") + + As shown in the last example, function names are just strings, + too. They must be string *literals*, however. This is not legal: + + ("con" + "cat")(a, " ", b) # syntax error! + + +- The ifelse() builtin takes three arguments: it evaluates exactly + one of the second and third, depending on whether the first one is + true. There is also some syntactic sugar to make expressions that + look like if/else statements: + + # these are all equivalent + ifelse(something(), "yes", "no") + if something() then yes else no endif + if something() then "yes" else "no" endif + + The else part is optional. + + if something() then "yes" endif # if something() is false, + # evaluates to false + + ifelse(condition(), "", abort()) # abort() only called if + # condition() is false + + The last example is equivalent to: + + assert(condition()) + + +- The && and || operators can be used similarly; they evaluate their + second argument only if it's needed to determine the truth of the + expression. Their value is the value of the last-evaluated + argument: + + file_exists("/data/system/bad") && delete("/data/system/bad") + + file_exists("/data/system/missing") || create("/data/system/missing") + + get_it() || "xxx" # returns value of get_it() if that value is + # true, otherwise returns "xxx" + + +- The purpose of ";" is to simulate imperative statements, of course, + but the operator can be used anywhere. Its value is the value of + its right side: + + concat(a;b;c, d, e;f) # evaluates to "cdf" + + A more useful example might be something like: + + ifelse(condition(), + (first_step(); second_step();), # second ; is optional + alternative_procedure()) diff --git a/edify/expr.c b/edify/expr.c new file mode 100644 index 0000000..b3b8927 --- /dev/null +++ b/edify/expr.c @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2009 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 +#include +#include +#include +#include + +#include "expr.h" + +// Functions should: +// +// - return a malloc()'d string +// - if Evaluate() on any argument returns NULL, return NULL. + +int BooleanString(const char* s) { + return s[0] != '\0'; +} + +char* Evaluate(void* cookie, Expr* expr) { + return expr->fn(expr->name, cookie, expr->argc, expr->argv); +} + +char* ConcatFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc == 0) { + return strdup(""); + } + char** strings = malloc(argc * sizeof(char*)); + int i; + for (i = 0; i < argc; ++i) { + strings[i] = NULL; + } + char* result = NULL; + int length = 0; + for (i = 0; i < argc; ++i) { + strings[i] = Evaluate(cookie, argv[i]); + if (strings[i] == NULL) { + goto done; + } + length += strlen(strings[i]); + } + + result = malloc(length+1); + int p = 0; + for (i = 0; i < argc; ++i) { + strcpy(result+p, strings[i]); + p += strlen(strings[i]); + } + result[p] = '\0'; + +done: + for (i = 0; i < argc; ++i) { + free(strings[i]); + } + return result; +} + +char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc != 2 && argc != 3) { + return NULL; + } + char* cond = Evaluate(cookie, argv[0]); + if (cond == NULL) { + return NULL; + } + + if (BooleanString(cond) == true) { + free(cond); + return Evaluate(cookie, argv[1]); + } else { + if (argc == 3) { + free(cond); + return Evaluate(cookie, argv[2]); + } else { + return cond; + } + } +} + +char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]) { + return NULL; +} + +char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]) { + int i; + for (i = 0; i < argc; ++i) { + char* v = Evaluate(cookie, argv[i]); + if (v == NULL) { + return NULL; + } + int b = BooleanString(v); + free(v); + if (!b) { + return NULL; + } + } + return strdup(""); +} + +char* PrintFn(const char* name, void* cookie, int argc, Expr* argv[]) { + int i; + for (i = 0; i < argc; ++i) { + char* v = Evaluate(cookie, argv[i]); + if (v == NULL) { + return NULL; + } + fputs(v, stdout); + free(v); + } + return strdup(""); +} + +char* LogicalAndFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + char* left = Evaluate(cookie, argv[0]); + if (left == NULL) return NULL; + if (BooleanString(left) == true) { + free(left); + return Evaluate(cookie, argv[1]); + } else { + return left; + } +} + +char* LogicalOrFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + char* left = Evaluate(cookie, argv[0]); + if (left == NULL) return NULL; + if (BooleanString(left) == false) { + free(left); + return Evaluate(cookie, argv[1]); + } else { + return left; + } +} + +char* LogicalNotFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + char* val = Evaluate(cookie, argv[0]); + if (val == NULL) return NULL; + bool bv = BooleanString(val); + free(val); + if (bv) { + return strdup(""); + } else { + return strdup("t"); + } +} + +char* SubstringFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + char* needle = Evaluate(cookie, argv[0]); + if (needle == NULL) return NULL; + char* haystack = Evaluate(cookie, argv[1]); + if (haystack == NULL) { + free(needle); + return NULL; + } + + char* result = strdup(strstr(haystack, needle) ? "t" : ""); + free(needle); + free(haystack); + return result; +} + +char* EqualityFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* left = Evaluate(cookie, argv[0]); + if (left == NULL) return NULL; + char* right = Evaluate(cookie, argv[1]); + if (right == NULL) { + free(left); + return NULL; + } + + char* result = strdup(strcmp(left, right) == 0 ? "t" : ""); + free(left); + free(right); + return result; +} + +char* InequalityFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* left = Evaluate(cookie, argv[0]); + if (left == NULL) return NULL; + char* right = Evaluate(cookie, argv[1]); + if (right == NULL) { + free(left); + return NULL; + } + + char* result = strdup(strcmp(left, right) != 0 ? "t" : ""); + free(left); + free(right); + return result; +} + +char* SequenceFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* left = Evaluate(cookie, argv[0]); + if (left == NULL) return NULL; + free(left); + return Evaluate(cookie, argv[1]); +} + +char* Literal(const char* name, void* cookie, int argc, Expr* argv[]) { + return strdup(name); +} + +Expr* Build(Function fn, int count, ...) { + va_list v; + va_start(v, count); + Expr* e = malloc(sizeof(Expr)); + e->fn = fn; + e->name = "(operator)"; + e->argc = count; + e->argv = malloc(count * sizeof(Expr*)); + int i; + for (i = 0; i < count; ++i) { + e->argv[i] = va_arg(v, Expr*); + } + va_end(v); + return e; +} + +static int fn_entries = 0; +static int fn_size = 0; +NamedFunction* fn_table = NULL; + +void RegisterFunction(const char* name, Function fn) { + if (fn_entries <= fn_size) { + fn_size = fn_size*2 + 1; + fn_table = realloc(fn_table, fn_size * sizeof(NamedFunction)); + } + fn_table[fn_entries].name = name; + fn_table[fn_entries].fn = fn; + ++fn_entries; +} + +static int fn_entry_compare(const void* a, const void* b) { + const char* na = ((const NamedFunction*)a)->name; + const char* nb = ((const NamedFunction*)b)->name; + return strcmp(na, nb); +} + +void FinishRegistration() { + qsort(fn_table, fn_entries, sizeof(NamedFunction), fn_entry_compare); +} + +Function FindFunction(const char* name) { + NamedFunction key; + key.name = name; + NamedFunction* nf = bsearch(&key, fn_table, fn_entries, sizeof(NamedFunction), + fn_entry_compare); + if (nf == NULL) { + return NULL; + } + return nf->fn; +} + +void RegisterBuiltins() { + RegisterFunction("ifelse", IfElseFn); + RegisterFunction("abort", AbortFn); + RegisterFunction("assert", AssertFn); + RegisterFunction("concat", ConcatFn); + RegisterFunction("is_substring", SubstringFn); + RegisterFunction("print", PrintFn); +} diff --git a/edify/expr.h b/edify/expr.h new file mode 100644 index 0000000..ac5df18 --- /dev/null +++ b/edify/expr.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef _EXPRESSION_H +#define _EXPRESSION_H + +#define MAX_STRING_LEN 1024 + +typedef struct Expr Expr; + +typedef char* (*Function)(const char* name, void* cookie, + int argc, Expr* argv[]); + +struct Expr { + Function fn; + char* name; + int argc; + Expr** argv; +}; + +char* Evaluate(void* cookie, Expr* expr); + +// Glue to make an Expr out of a literal. +char* Literal(const char* name, void* cookie, int argc, Expr* argv[]); + +// Functions corresponding to various syntactic sugar operators. +// ("concat" is also available as a builtin function, to concatenate +// more than two strings.) +char* ConcatFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* LogicalAndFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* LogicalOrFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* LogicalNotFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* SubstringFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* EqualityFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* InequalityFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* SequenceFn(const char* name, void* cookie, int argc, Expr* argv[]); + +// Convenience function for building expressions with a fixed number +// of arguments. +Expr* Build(Function fn, int count, ...); + +// Global builtins, registered by RegisterBuiltins(). +char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]); + +typedef struct { + const char* name; + Function fn; +} NamedFunction; + +// Register a new function. The same Function may be registered under +// multiple names, but a given name should only be used once. +void RegisterFunction(const char* name, Function fn); + +// Register all the builtins. +void RegisterBuiltins(); + +// Call this after all calls to RegisterFunction() but before parsing +// any scripts to finish building the function table. +void FinishRegistration(); + +// Find the Function for a given name; return NULL if no such function +// exists. +Function FindFunction(const char* name); + +#endif // _EXPRESSION_H diff --git a/edify/lexer.l b/edify/lexer.l new file mode 100644 index 0000000..4faef5d --- /dev/null +++ b/edify/lexer.l @@ -0,0 +1,97 @@ +%{ +/* + * Copyright (C) 2009 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 "expr.h" +#include "parser.h" + +int gLine = 1; +int gColumn = 1; + +// TODO: enforce MAX_STRING_LEN during lexing +char string_buffer[MAX_STRING_LEN]; +char* string_pos; +%} + +%x STR + +%option noyywrap + +%% + + +\" { + ++gColumn; + BEGIN(STR); + string_pos = string_buffer; +} + +{ + \" { + ++gColumn; + BEGIN(INITIAL); + *string_pos = '\0'; + yylval.str = strdup(string_buffer); + return STRING; + } + + \\n { gColumn += yyleng; *string_pos++ = '\n'; } + \\t { gColumn += yyleng; *string_pos++ = '\t'; } + \\\" { gColumn += yyleng; *string_pos++ = '\"'; } + \\\\ { gColumn += yyleng; *string_pos++ = '\\'; } + + \\x[0-9a-fA-F]{2} { + gColumn += yyleng; + int val; + sscanf(yytext+2, "%x", &val); + *string_pos++ = val; + } + + \n { + ++gLine; + gColumn = 1; + *string_pos++ = yytext[0]; + } + + . { + ++gColumn; + *string_pos++ = yytext[0]; + } +} + +if { gColumn += yyleng; return IF; } +then { gColumn += yyleng; return THEN; } +else { gColumn += yyleng; return ELSE; } +endif { gColumn += yyleng; return ENDIF; } + +[a-zA-Z0-9_:/]+ { + gColumn += yyleng; + yylval.str = strdup(yytext); + return STRING; +} + +\&\& { gColumn += yyleng; return AND; } +\|\| { gColumn += yyleng; return OR; } +== { gColumn += yyleng; return EQ; } +!= { gColumn += yyleng; return NE; } + +[+(),!;] { gColumn += yyleng; return yytext[0]; } + +[ \t]+ gColumn += yyleng; + +(#.*)?\n { ++gLine; gColumn = 1; } + +. return BAD; diff --git a/edify/main.c b/edify/main.c new file mode 100644 index 0000000..4d65da2 --- /dev/null +++ b/edify/main.c @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2009 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 +#include +#include + +#include "expr.h" +#include "parser.h" + +int expect(const char* expr_str, const char* expected, int* errors) { + Expr* e; + int error; + char* result; + + printf("."); + + yy_scan_string(expr_str); + error = yyparse(&e); + if (error > 0) { + fprintf(stderr, "error parsing \"%s\"\n", expr_str); + ++*errors; + return 0; + } + + result = Evaluate(NULL, e); + if (result == NULL && expected != NULL) { + fprintf(stderr, "error evaluating \"%s\"\n", expr_str); + ++*errors; + return 0; + } + + if (result == NULL && expected == NULL) { + return 1; + } + + if (strcmp(result, expected) != 0) { + fprintf(stderr, "evaluating \"%s\": expected \"%s\", got \"%s\"\n", + expr_str, expected, result); + ++*errors; + free(result); + return 0; + } + + free(result); + return 1; +} + +int test() { + int errors = 0; + + expect("a", "a", &errors); + expect("\"a\"", "a", &errors); + expect("\"\\x61\"", "a", &errors); + expect("# this is a comment\n" + " a\n" + " \n", + "a", &errors); + + + // sequence operator + expect("a; b; c", "c", &errors); + + // string concat operator + expect("a + b", "ab", &errors); + expect("a + \n \"b\"", "ab", &errors); + expect("a + b +\nc\n", "abc", &errors); + + // string concat function + expect("concat(a, b)", "ab", &errors); + expect("concat(a,\n \"b\")", "ab", &errors); + expect("concat(a + b,\nc,\"d\")", "abcd", &errors); + expect("\"concat\"(a + b,\nc,\"d\")", "abcd", &errors); + + // logical and + expect("a && b", "b", &errors); + expect("a && \"\"", "", &errors); + expect("\"\" && b", "", &errors); + expect("\"\" && \"\"", "", &errors); + expect("\"\" && abort()", "", &errors); // test short-circuiting + expect("t && abort()", NULL, &errors); + + // logical or + expect("a || b", "a", &errors); + expect("a || \"\"", "a", &errors); + expect("\"\" || b", "b", &errors); + expect("\"\" || \"\"", "", &errors); + expect("a || abort()", "a", &errors); // test short-circuiting + expect("\"\" || abort()", NULL, &errors); + + // logical not + expect("!a", "", &errors); + expect("! \"\"", "t", &errors); + expect("!!a", "t", &errors); + + // precedence + expect("\"\" == \"\" && b", "b", &errors); + expect("a + b == ab", "t", &errors); + expect("ab == a + b", "t", &errors); + expect("a + (b == ab)", "a", &errors); + expect("(ab == a) + b", "b", &errors); + + // substring function + expect("is_substring(cad, abracadabra)", "t", &errors); + expect("is_substring(abrac, abracadabra)", "t", &errors); + expect("is_substring(dabra, abracadabra)", "t", &errors); + expect("is_substring(cad, abracxadabra)", "", &errors); + expect("is_substring(abrac, axbracadabra)", "", &errors); + expect("is_substring(dabra, abracadabrxa)", "", &errors); + + // ifelse function + expect("ifelse(t, yes, no)", "yes", &errors); + expect("ifelse(!t, yes, no)", "no", &errors); + expect("ifelse(t, yes, abort())", "yes", &errors); + expect("ifelse(!t, abort(), no)", "no", &errors); + + // if "statements" + expect("if t then yes else no endif", "yes", &errors); + expect("if \"\" then yes else no endif", "no", &errors); + expect("if \"\" then yes endif", "", &errors); + expect("if \"\"; t then yes endif", "yes", &errors); + + printf("\n"); + + return errors; +} + +int main(int argc, char** argv) { + RegisterBuiltins(); + FinishRegistration(); + + if (argc == 1) { + return test() != 0; + } + + FILE* f = fopen(argv[1], "r"); + char buffer[8192]; + int size = fread(buffer, 1, 8191, f); + fclose(f); + buffer[size] = '\0'; + + Expr* root; + yy_scan_bytes(buffer, size); + int error = yyparse(&root); + printf("parse returned %d\n", error); + if (error == 0) { + char* result = Evaluate(NULL, root); + printf("result is [%s]\n", result == NULL ? "(NULL)" : result); + } + return 0; +} diff --git a/edify/parser.y b/edify/parser.y new file mode 100644 index 0000000..67a210f --- /dev/null +++ b/edify/parser.y @@ -0,0 +1,121 @@ +%{ +/* + * Copyright (C) 2009 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 +#include +#include + +#include "expr.h" +#include "parser.h" + +extern int gLine; +extern int gColumn; + +void yyerror(Expr** root, const char* s); +int yyparse(Expr** root); + +%} + +%union { + char* str; + Expr* expr; + struct { + int argc; + Expr** argv; + } args; +} + +%token AND OR SUBSTR SUPERSTR EQ NE IF THEN ELSE ENDIF +%token STRING BAD +%type expr +%type arglist + +%parse-param {Expr** root} +%error-verbose + +/* declarations in increasing order of precedence */ +%left ';' +%left ',' +%left OR +%left AND +%left EQ NE +%left '+' +%right '!' + +%% + +input: expr { *root = $1; } +; + +expr: STRING { + $$ = malloc(sizeof(Expr)); + $$->fn = Literal; + $$->name = $1; + $$->argc = 0; + $$->argv = NULL; +} +| '(' expr ')' { $$ = $2; } +| expr ';' { $$ = $1; } +| expr ';' expr { $$ = Build(SequenceFn, 2, $1, $3); } +| error ';' expr { $$ = $3; } +| expr '+' expr { $$ = Build(ConcatFn, 2, $1, $3); } +| expr EQ expr { $$ = Build(EqualityFn, 2, $1, $3); } +| expr NE expr { $$ = Build(InequalityFn, 2, $1, $3); } +| expr AND expr { $$ = Build(LogicalAndFn, 2, $1, $3); } +| expr OR expr { $$ = Build(LogicalOrFn, 2, $1, $3); } +| '!' expr { $$ = Build(LogicalNotFn, 1, $2); } +| IF expr THEN expr ENDIF { $$ = Build(IfElseFn, 2, $2, $4); } +| IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, 3, $2, $4, $6); } +| STRING '(' arglist ')' { + $$ = malloc(sizeof(Expr)); + $$->fn = FindFunction($1); + if ($$->fn == NULL) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "unknown function \"%s\"", $1); + yyerror(root, buffer); + YYERROR; + } + $$->name = $1; + $$->argc = $3.argc; + $$->argv = $3.argv; +} +; + +arglist: /* empty */ { + $$.argc = 0; + $$.argv = NULL; +} +| expr { + $$.argc = 1; + $$.argv = malloc(sizeof(Expr*)); + $$.argv[0] = $1; +} +| arglist ',' expr { + $$.argc = $1.argc + 1; + $$.argv = realloc($$.argv, $$.argc * sizeof(Expr*)); + $$.argv[$$.argc-1] = $3; +} +; + +%% + +void yyerror(Expr** root, const char* s) { + if (strlen(s) == 0) { + s = "syntax error"; + } + printf("line %d col %d: %s\n", gLine, gColumn, s); +} From 9931f7f3c1288171319e9ff7d053ebaad07db720 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Wed, 10 Jun 2009 14:11:53 -0700 Subject: [PATCH 14/24] edify extensions for OTA package installation, part 1 Adds the following edify functions: mount unmount format show_progress delete delete_recursive package_extract symlink set_perm set_perm_recursive This set is enough to extract and install the system part of a (full) OTA package. Adds the updater binary that extracts an edify script from the OTA package and then executes it. Minor changes to the edify core (adds a sleep() builtin for debugging, adds "." to the set of characters that can appear in an unquoted string). --- Android.mk | 2 + edify/Android.mk | 17 +-- edify/README | 2 +- edify/expr.c | 95 ++++++++++++ edify/expr.h | 23 +++ edify/lexer.l | 2 +- edify/main.c | 9 +- install.c | 2 +- updater/Android.mk | 24 +++ updater/install.c | 370 +++++++++++++++++++++++++++++++++++++++++++++ updater/install.h | 22 +++ updater/updater.c | 111 ++++++++++++++ updater/updater.h | 28 ++++ 13 files changed, 694 insertions(+), 13 deletions(-) create mode 100644 updater/Android.mk create mode 100644 updater/install.c create mode 100644 updater/install.h create mode 100644 updater/updater.c create mode 100644 updater/updater.h diff --git a/Android.mk b/Android.mk index 8c1de73..bfb1bed 100644 --- a/Android.mk +++ b/Android.mk @@ -44,4 +44,6 @@ include $(commands_recovery_local_path)/amend/Android.mk include $(commands_recovery_local_path)/minzip/Android.mk include $(commands_recovery_local_path)/mtdutils/Android.mk include $(commands_recovery_local_path)/tools/Android.mk +include $(commands_recovery_local_path)/edify/Android.mk +include $(commands_recovery_local_path)/updater/Android.mk commands_recovery_local_path := diff --git a/edify/Android.mk b/edify/Android.mk index 803fba2..fac0ba7 100644 --- a/edify/Android.mk +++ b/edify/Android.mk @@ -26,15 +26,14 @@ LOCAL_YACCFLAGS := -v include $(BUILD_HOST_EXECUTABLE) -# # -# # Build the device-side library -# # -# include $(CLEAR_VARS) +# +# Build the device-side library +# +include $(CLEAR_VARS) -# LOCAL_SRC_FILES := $(edify_src_files) -# LOCAL_SRC_FILES += $(edify_test_files) +LOCAL_SRC_FILES := $(edify_src_files) -# LOCAL_CFLAGS := $(edify_cflags) -# LOCAL_MODULE := libedify +LOCAL_CFLAGS := $(edify_cflags) +LOCAL_MODULE := libedify -# include $(BUILD_STATIC_LIBRARY) +include $(BUILD_STATIC_LIBRARY) diff --git a/edify/README b/edify/README index 5ccb582..810455c 100644 --- a/edify/README +++ b/edify/README @@ -10,7 +10,7 @@ the old one ("amend"). This is a brief overview of the new language. understood, as are hexadecimal escapes like \x4a. - String literals consisting of only letters, numbers, colons, - underscores, and slashes don't need to be in double quotes. + underscores, slashes, and periods don't need to be in double quotes. - The following words are reserved: diff --git a/edify/expr.c b/edify/expr.c index b3b8927..129fbd9 100644 --- a/edify/expr.c +++ b/edify/expr.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "expr.h" @@ -92,6 +93,12 @@ char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]) { } char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* msg = NULL; + if (argc > 0) { + msg = Evaluate(cookie, argv[0]); + } + SetError(msg == NULL ? "called abort()" : msg); + free(msg); return NULL; } @@ -105,12 +112,23 @@ char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]) { int b = BooleanString(v); free(v); if (!b) { + SetError("assert() failed"); return NULL; } } return strdup(""); } +char* SleepFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* val = Evaluate(cookie, argv[0]); + if (val == NULL) { + return NULL; + } + int v = strtol(val, NULL, 10); + sleep(v); + return val; +} + char* PrintFn(const char* name, void* cookie, int argc, Expr* argv[]) { int i; for (i = 0; i < argc; ++i) { @@ -234,6 +252,32 @@ Expr* Build(Function fn, int count, ...) { return e; } +// ----------------------------------------------------------------- +// error reporting +// ----------------------------------------------------------------- + +static char* error_message = NULL; + +void SetError(const char* message) { + if (error_message) { + free(error_message); + } + error_message = strdup(message); +} + +const char* GetError() { + return error_message; +} + +void ClearError() { + free(error_message); + error_message = NULL; +} + +// ----------------------------------------------------------------- +// the function table +// ----------------------------------------------------------------- + static int fn_entries = 0; static int fn_size = 0; NamedFunction* fn_table = NULL; @@ -276,4 +320,55 @@ void RegisterBuiltins() { RegisterFunction("concat", ConcatFn); RegisterFunction("is_substring", SubstringFn); RegisterFunction("print", PrintFn); + RegisterFunction("sleep", SleepFn); +} + + +// ----------------------------------------------------------------- +// convenience methods for functions +// ----------------------------------------------------------------- + +// Evaluate the expressions in argv, giving 'count' char* (the ... is +// zero or more char** to put them in). If any expression evaluates +// to NULL, free the rest and return -1. Return 0 on success. +int ReadArgs(void* cookie, Expr* argv[], int count, ...) { + char** args = malloc(count * sizeof(char*)); + va_list v; + va_start(v, count); + int i; + for (i = 0; i < count; ++i) { + args[i] = Evaluate(cookie, argv[i]); + if (args[i] == NULL) { + va_end(v); + int j; + for (j = 0; j < i; ++j) { + free(args[j]); + } + return -1; + } + *(va_arg(v, char**)) = args[i]; + } + va_end(v); + return 0; +} + +// Evaluate the expressions in argv, returning an array of char* +// results. If any evaluate to NULL, free the rest and return NULL. +// The caller is responsible for freeing the returned array and the +// strings it contains. +char** ReadVarArgs(void* cookie, int argc, Expr* argv[]) { + char** args = (char**)malloc(argc * sizeof(char*)); + int i = 0; + for (i = 0; i < argc; ++i) { + args[i] = Evaluate(cookie, argv[i]); + if (args[i] == NULL) { + int j; + for (j = 0; j < i; ++j) { + free(args[j]); + } + free(args); + return NULL; + } + } + return args; } diff --git a/edify/expr.h b/edify/expr.h index ac5df18..cfbef90 100644 --- a/edify/expr.h +++ b/edify/expr.h @@ -57,6 +57,14 @@ char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]); char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]); char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]); + +// For setting and getting the global error string (when returning +// NULL from a function). +void SetError(const char* message); // makes a copy +const char* GetError(); // retains ownership +void ClearError(); + + typedef struct { const char* name; Function fn; @@ -77,4 +85,19 @@ void FinishRegistration(); // exists. Function FindFunction(const char* name); + +// --- convenience functions for use in functions --- + +// Evaluate the expressions in argv, giving 'count' char* (the ... is +// zero or more char** to put them in). If any expression evaluates +// to NULL, free the rest and return -1. Return 0 on success. +int ReadArgs(void* cookie, Expr* argv[], int count, ...); + +// Evaluate the expressions in argv, returning an array of char* +// results. If any evaluate to NULL, free the rest and return NULL. +// The caller is responsible for freeing the returned array and the +// strings it contains. +char** ReadVarArgs(void* cookie, int argc, Expr* argv[]); + + #endif // _EXPRESSION_H diff --git a/edify/lexer.l b/edify/lexer.l index 4faef5d..cb5eb31 100644 --- a/edify/lexer.l +++ b/edify/lexer.l @@ -77,7 +77,7 @@ then { gColumn += yyleng; return THEN; } else { gColumn += yyleng; return ELSE; } endif { gColumn += yyleng; return ENDIF; } -[a-zA-Z0-9_:/]+ { +[a-zA-Z0-9_:/.]+ { gColumn += yyleng; yylval.str = strdup(yytext); return STRING; diff --git a/edify/main.c b/edify/main.c index 4d65da2..c959683 100644 --- a/edify/main.c +++ b/edify/main.c @@ -158,7 +158,14 @@ int main(int argc, char** argv) { printf("parse returned %d\n", error); if (error == 0) { char* result = Evaluate(NULL, root); - printf("result is [%s]\n", result == NULL ? "(NULL)" : result); + if (result == NULL) { + char* errmsg = GetError(); + printf("result was NULL, message is: %s\n", + (errmsg == NULL ? "(NULL)" : errmsg)); + ClearError(); + } else { + printf("result is [%s]\n", result); + } } return 0; } diff --git a/install.c b/install.c index eff9312..0b5c04d 100644 --- a/install.c +++ b/install.c @@ -256,7 +256,7 @@ try_update_binary(const char *path, ZipArchive *zip) { int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - LOGE("Error in %s\n(Status %d)\n", path, status); + LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status)); return INSTALL_ERROR; } diff --git a/updater/Android.mk b/updater/Android.mk new file mode 100644 index 0000000..2716d48 --- /dev/null +++ b/updater/Android.mk @@ -0,0 +1,24 @@ +# Copyright 2009 The Android Open Source Project + +LOCAL_PATH := $(call my-dir) + +updater_src_files := \ + install.c \ + updater.c + +# +# Build the device-side library +# +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(updater_src_files) + +LOCAL_STATIC_LIBRARIES := libedify libmtdutils libminzip libz +LOCAL_STATIC_LIBRARIES += libcutils libstdc++ libc +LOCAL_C_INCLUDES += $(LOCAL_PATH)/.. + +LOCAL_MODULE := updater + +LOCAL_FORCE_STATIC_EXECUTABLE := true + +include $(BUILD_EXECUTABLE) diff --git a/updater/install.c b/updater/install.c new file mode 100644 index 0000000..2336f61 --- /dev/null +++ b/updater/install.c @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2009 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "minzip/DirUtil.h" +#include "mtdutils/mounts.h" +#include "mtdutils/mtdutils.h" +#include "updater.h" + +char* ErrorAbort(void* cookie, char* format, ...) { + char* buffer = malloc(4096); + va_list v; + va_start(v, format); + vsnprintf(buffer, 4096, format, v); + va_end(v); + SetError(buffer); + return NULL; +} + +// mount(type, location, mount_point) +// +// what: type="MTD" location="" to mount a yaffs2 filesystem +// type="vfat" location="/dev/block/" to mount a device +char* MountFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + if (argc != 3) { + return ErrorAbort(cookie, "%s() expects 3 args, got %d", name, argc); + } + char* type; + char* location; + char* mount_point; + if (ReadArgs(cookie, argv, 3, &type, &location, &mount_point) < 0) { + return NULL; + } + + if (strlen(type) == 0) { + ErrorAbort(cookie, "type argument to %s() can't be empty", name); + goto done; + } + if (strlen(location) == 0) { + ErrorAbort(cookie, "location argument to %s() can't be empty", name); + goto done; + } + if (strlen(mount_point) == 0) { + ErrorAbort(cookie, "mount_point argument to %s() can't be empty", name); + goto done; + } + + mkdir(mount_point, 0755); + + if (strcmp(type, "MTD") == 0) { + mtd_scan_partitions(); + const MtdPartition* mtd; + mtd = mtd_find_partition_by_name(location); + if (mtd == NULL) { + fprintf(stderr, "%s: no mtd partition named \"%s\"", + name, location); + result = strdup(""); + goto done; + } + if (mtd_mount_partition(mtd, mount_point, "yaffs2", 0 /* rw */) != 0) { + fprintf(stderr, "mtd mount of %s failed: %s\n", + location, strerror(errno)); + result = strdup(""); + goto done; + } + result = mount_point; + } else { + if (mount(location, mount_point, type, + MS_NOATIME | MS_NODEV | MS_NODIRATIME, "") < 0) { + result = strdup(""); + } else { + result = mount_point; + } + } + +done: + free(type); + free(location); + if (result != mount_point) free(mount_point); + return result; +} + +char* UnmountFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + if (argc != 1) { + return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + } + char* mount_point; + if (ReadArgs(cookie, argv, 1, &mount_point) < 0) { + return NULL; + } + if (strlen(mount_point) == 0) { + ErrorAbort(cookie, "mount_point argument to unmount() can't be empty"); + goto done; + } + + scan_mounted_volumes(); + const MountedVolume* vol = find_mounted_volume_by_mount_point(mount_point); + if (vol == NULL) { + fprintf(stderr, "unmount of %s failed; no such volume\n", mount_point); + result = strdup(""); + } else { + unmount_mounted_volume(vol); + result = mount_point; + } + +done: + if (result != mount_point) free(mount_point); + return result; +} +// format(type, location) +// +// type="MTD" location=partition +char* FormatFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + if (argc != 2) { + return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + } + char* type; + char* location; + if (ReadArgs(cookie, argv, 2, &type, &location) < 0) { + return NULL; + } + + if (strlen(type) == 0) { + ErrorAbort(cookie, "type argument to %s() can't be empty", name); + goto done; + } + if (strlen(location) == 0) { + ErrorAbort(cookie, "location argument to %s() can't be empty", name); + goto done; + } + + if (strcmp(type, "MTD") == 0) { + mtd_scan_partitions(); + const MtdPartition* mtd = mtd_find_partition_by_name(location); + if (mtd == NULL) { + fprintf(stderr, "%s: no mtd partition named \"%s\"", + name, location); + result = strdup(""); + goto done; + } + MtdWriteContext* ctx = mtd_write_partition(mtd); + if (ctx == NULL) { + fprintf(stderr, "%s: can't write \"%s\"", name, location); + result = strdup(""); + goto done; + } + if (mtd_erase_blocks(ctx, -1) == -1) { + mtd_write_close(ctx); + fprintf(stderr, "%s: failed to erase \"%s\"", name, location); + result = strdup(""); + goto done; + } + if (mtd_write_close(ctx) != 0) { + fprintf(stderr, "%s: failed to close \"%s\"", name, location); + result = strdup(""); + goto done; + } + result = location; + } else { + fprintf(stderr, "%s: unsupported type \"%s\"", name, type); + } + +done: + free(type); + if (result != location) free(location); + return result; +} + +char* DeleteFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char** paths = malloc(argc * sizeof(char*)); + int i; + for (i = 0; i < argc; ++i) { + paths[i] = Evaluate(cookie, argv[i]); + if (paths[i] == NULL) { + int j; + for (j = 0; j < i; ++i) { + free(paths[j]); + } + free(paths); + return NULL; + } + } + + bool recursive = (strcmp(name, "delete_recursive") == 0); + + int success = 0; + for (i = 0; i < argc; ++i) { + if ((recursive ? dirUnlinkHierarchy(paths[i]) : unlink(paths[i])) == 0) + ++success; + free(paths[i]); + } + free(paths); + + char buffer[10]; + sprintf(buffer, "%d", success); + return strdup(buffer); +} + +char* ShowProgressFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc != 2) { + return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + } + char* frac_str; + char* sec_str; + if (ReadArgs(cookie, argv, 2, &frac_str, &sec_str) < 0) { + return NULL; + } + + double frac = strtod(frac_str, NULL); + int sec = strtol(sec_str, NULL, 10); + + UpdaterInfo* ui = (UpdaterInfo*)cookie; + fprintf(ui->cmd_pipe, "progress %f %d\n", frac, sec); + + free(frac_str); + free(sec_str); + return strdup(""); +} + +// package_extract package_path destination_path +char* PackageExtractFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc != 2) { + return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + } + char* zip_path; + char* dest_path; + if (ReadArgs(cookie, argv, 2, &zip_path, &dest_path) < 0) return NULL; + + ZipArchive* za = ((UpdaterInfo*)cookie)->package_zip; + + // To create a consistent system image, never use the clock for timestamps. + struct utimbuf timestamp = { 1217592000, 1217592000 }; // 8/1/2008 default + + bool success = mzExtractRecursive(za, zip_path, dest_path, + MZ_EXTRACT_FILES_ONLY, ×tamp, + NULL, NULL); + free(zip_path); + free(dest_path); + return strdup(success ? "t" : ""); +} + +// symlink target src1 src2 ... +char* SymlinkFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc == 0) { + return ErrorAbort(cookie, "%s() expects 1+ args, got %d", name, argc); + } + char* target; + target = Evaluate(cookie, argv[0]); + if (target == NULL) return NULL; + + char** srcs = ReadVarArgs(cookie, argc-1, argv+1); + if (srcs == NULL) { + free(target); + return NULL; + } + + int i; + for (i = 0; i < argc-1; ++i) { + symlink(target, srcs[i]); + free(srcs[i]); + } + free(srcs); + return strdup(""); +} + +char* SetPermFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + bool recursive = (strcmp(name, "set_perm_recursive") == 0); + + int min_args = 4 + (recursive ? 1 : 0); + if (argc < min_args) { + return ErrorAbort(cookie, "%s() expects %d+ args, got %d", name, argc); + } + + char** args = ReadVarArgs(cookie, argc, argv); + if (args == NULL) return NULL; + + char* end; + int i; + + int uid = strtoul(args[0], &end, 0); + if (*end != '\0' || args[0][0] == 0) { + ErrorAbort(cookie, "%s: \"%s\" not a valid uid", name, args[0]); + goto done; + } + + int gid = strtoul(args[1], &end, 0); + if (*end != '\0' || args[1][0] == 0) { + ErrorAbort(cookie, "%s: \"%s\" not a valid gid", name, args[1]); + goto done; + } + + if (recursive) { + int dir_mode = strtoul(args[2], &end, 0); + if (*end != '\0' || args[2][0] == 0) { + ErrorAbort(cookie, "%s: \"%s\" not a valid dirmode", name, args[2]); + goto done; + } + + int file_mode = strtoul(args[3], &end, 0); + if (*end != '\0' || args[3][0] == 0) { + ErrorAbort(cookie, "%s: \"%s\" not a valid filemode", + name, args[3]); + goto done; + } + + for (i = 4; i < argc; ++i) { + dirSetHierarchyPermissions(args[i], uid, gid, dir_mode, file_mode); + } + } else { + int mode = strtoul(args[2], &end, 0); + if (*end != '\0' || args[2][0] == 0) { + ErrorAbort(cookie, "%s: \"%s\" not a valid mode", name, args[2]); + goto done; + } + + for (i = 4; i < argc; ++i) { + chown(args[i], uid, gid); + chmod(args[i], mode); + } + } + result = strdup(""); + +done: + for (i = 0; i < argc; ++i) { + free(args[i]); + } + free(args); + + return result; +} + +void RegisterInstallFunctions() { + RegisterFunction("mount", MountFn); + RegisterFunction("unmount", UnmountFn); + RegisterFunction("format", FormatFn); + RegisterFunction("show_progress", ShowProgressFn); + RegisterFunction("delete", DeleteFn); + RegisterFunction("delete_recursive", DeleteFn); + RegisterFunction("package_extract", PackageExtractFn); + RegisterFunction("symlink", SymlinkFn); + RegisterFunction("set_perm", SetPermFn); + RegisterFunction("set_perm_recursive", SetPermFn); +} diff --git a/updater/install.h b/updater/install.h new file mode 100644 index 0000000..94f344f --- /dev/null +++ b/updater/install.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef _UPDATER_INSTALL_H_ +#define _UPDATER_INSTALL_H_ + +void RegisterInstallFunctions(); + +#endif diff --git a/updater/updater.c b/updater/updater.c new file mode 100644 index 0000000..aa03803 --- /dev/null +++ b/updater/updater.c @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2009 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 +#include +#include + +#include "edify/expr.h" +#include "updater.h" +#include "install.h" +#include "minzip/Zip.h" + +// Where in the package we expect to find the edify script to execute. +// (Note it's "updateR-script", not the older "update-script".) +#define SCRIPT_NAME "META-INF/com/google/android/updater-script" + +int main(int argc, char** argv) { + if (argc != 4) { + fprintf(stderr, "unexpected number of arguments (%d)\n", argc); + return 1; + } + + char* version = argv[1]; + if (version[0] != '1' || version[1] != '\0') { + fprintf(stderr, "wrong updater binary API; expected 1, got %s\n", + version); + return 2; + } + + // Set up the pipe for sending commands back to the parent process. + + int fd = atoi(argv[2]); + FILE* cmd_pipe = fdopen(fd, "wb"); + setlinebuf(cmd_pipe); + + // Extract the script from the package. + + char* package_data = argv[3]; + ZipArchive za; + int err; + err = mzOpenZipArchive(package_data, &za); + if (err != 0) { + fprintf(stderr, "failed to open package %s: %s\n", + package_data, strerror(err)); + return 3; + } + + const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME); + if (script_entry == NULL) { + fprintf(stderr, "failed to find %s in %s\n", SCRIPT_NAME, package_data); + return 4; + } + + char* script = malloc(script_entry->uncompLen+1); + if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) { + fprintf(stderr, "failed to read script from package\n"); + return 5; + } + script[script_entry->uncompLen] = '\0'; + + // Configure edify's functions. + + RegisterBuiltins(); + RegisterInstallFunctions(); + FinishRegistration(); + + // Parse the script. + + Expr* root; + yy_scan_string(script); + int error = yyparse(&root); + if (error != 0) { + fprintf(stderr, "%d parse errors\n", error); + return 6; + } + + // Evaluate the parsed script. + + UpdaterInfo updater_info; + updater_info.cmd_pipe = cmd_pipe; + updater_info.package_zip = &za; + + char* result = Evaluate(&updater_info, root); + if (result == NULL) { + const char* errmsg = GetError(); + fprintf(stderr, "script aborted with error: %s\n", + errmsg == NULL ? "(none)" : errmsg); + ClearError(); + return 7; + } else { + fprintf(stderr, "script result was [%s]\n", result); + free(result); + } + + mzCloseZipArchive(&za); + + return 0; +} diff --git a/updater/updater.h b/updater/updater.h new file mode 100644 index 0000000..22fbfd2 --- /dev/null +++ b/updater/updater.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef _UPDATER_UPDATER_H_ +#define _UPDATER_UPDATER_H_ + +#include +#include "minzip/Zip.h" + +typedef struct { + FILE* cmd_pipe; + ZipArchive* package_zip; +} UpdaterInfo; + +#endif From 9dbc027b5f540bcf23c968398f8a70e92abd56cd Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 11 Jun 2009 17:32:55 -0700 Subject: [PATCH 15/24] fix sim build in donut, too --- Android.mk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Android.mk b/Android.mk index bfb1bed..6ab498e 100644 --- a/Android.mk +++ b/Android.mk @@ -36,10 +36,6 @@ LOCAL_STATIC_LIBRARIES += libstdc++ libc include $(BUILD_EXECUTABLE) include $(commands_recovery_local_path)/minui/Android.mk - -endif # TARGET_ARCH == arm -endif # !TARGET_SIMULATOR - include $(commands_recovery_local_path)/amend/Android.mk include $(commands_recovery_local_path)/minzip/Android.mk include $(commands_recovery_local_path)/mtdutils/Android.mk @@ -47,3 +43,7 @@ include $(commands_recovery_local_path)/tools/Android.mk include $(commands_recovery_local_path)/edify/Android.mk include $(commands_recovery_local_path)/updater/Android.mk commands_recovery_local_path := + +endif # TARGET_ARCH == arm +endif # !TARGET_SIMULATOR + From 8edb00c990e563e6f91b278a212f2edf877cf763 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 11 Jun 2009 17:21:44 -0700 Subject: [PATCH 16/24] edify extensions for OTA package installation, part 2 Adds more edify functions for OTAs: is_mounted getprop apply_patch apply_patch_check apply_patch_space write_raw_image write_firmware_image package_extract_file This allows us to install radios, hboots, boot images, and install incremental OTA packages. Fixes a couple of dumb bugs in edify itself: - we were doubling the size of the function table each time it was *not* full, rather than each time it was full - "no such function" errors weren't visible to the parser, so they didn't prevent execution of the script. --- edify/expr.c | 2 +- edify/main.c | 7 +- edify/parser.y | 10 +- install.c | 3 +- updater/Android.mk | 3 +- updater/install.c | 275 ++++++++++++++++++++++++++++++++++++++++++++- updater/updater.c | 7 +- 7 files changed, 291 insertions(+), 16 deletions(-) diff --git a/edify/expr.c b/edify/expr.c index 129fbd9..5470a2b 100644 --- a/edify/expr.c +++ b/edify/expr.c @@ -283,7 +283,7 @@ static int fn_size = 0; NamedFunction* fn_table = NULL; void RegisterFunction(const char* name, Function fn) { - if (fn_entries <= fn_size) { + if (fn_entries >= fn_size) { fn_size = fn_size*2 + 1; fn_table = realloc(fn_table, fn_size * sizeof(NamedFunction)); } diff --git a/edify/main.c b/edify/main.c index c959683..7da89e2 100644 --- a/edify/main.c +++ b/edify/main.c @@ -153,10 +153,11 @@ int main(int argc, char** argv) { buffer[size] = '\0'; Expr* root; + int error_count = 0; yy_scan_bytes(buffer, size); - int error = yyparse(&root); - printf("parse returned %d\n", error); - if (error == 0) { + int error = yyparse(&root, &error_count); + printf("parse returned %d; %d errors encountered\n", error, error_count); + if (error == 0 || error_count > 0) { char* result = Evaluate(NULL, root); if (result == NULL) { char* errmsg = GetError(); diff --git a/edify/parser.y b/edify/parser.y index 67a210f..cf163c0 100644 --- a/edify/parser.y +++ b/edify/parser.y @@ -25,8 +25,8 @@ extern int gLine; extern int gColumn; -void yyerror(Expr** root, const char* s); -int yyparse(Expr** root); +void yyerror(Expr** root, int* error_count, const char* s); +int yyparse(Expr** root, int* error_count); %} @@ -45,6 +45,7 @@ int yyparse(Expr** root); %type arglist %parse-param {Expr** root} +%parse-param {int* error_count} %error-verbose /* declarations in increasing order of precedence */ @@ -86,7 +87,7 @@ expr: STRING { if ($$->fn == NULL) { char buffer[256]; snprintf(buffer, sizeof(buffer), "unknown function \"%s\"", $1); - yyerror(root, buffer); + yyerror(root, error_count, buffer); YYERROR; } $$->name = $1; @@ -113,9 +114,10 @@ arglist: /* empty */ { %% -void yyerror(Expr** root, const char* s) { +void yyerror(Expr** root, int* error_count, const char* s) { if (strlen(s) == 0) { s = "syntax error"; } printf("line %d col %d: %s\n", gLine, gColumn, s); + ++*error_count; } diff --git a/install.c b/install.c index 0b5c04d..cca9400 100644 --- a/install.c +++ b/install.c @@ -124,7 +124,8 @@ handle_firmware_update(char* type, char* filename) { return INSTALL_ERROR; } - LOGI("type is [%s]\n", type); + LOGI("type is %s; size is %d; file is %s\n", + type, (int)st_data.st_size, filename); char* data = malloc(st_data.st_size); if (data == NULL) { diff --git a/updater/Android.mk b/updater/Android.mk index 2716d48..1159d7b 100644 --- a/updater/Android.mk +++ b/updater/Android.mk @@ -13,7 +13,8 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES := $(updater_src_files) -LOCAL_STATIC_LIBRARIES := libedify libmtdutils libminzip libz +LOCAL_STATIC_LIBRARIES := libapplypatch libedify libmtdutils libminzip libz +LOCAL_STATIC_LIBRARIES += libmincrypt libbz LOCAL_STATIC_LIBRARIES += libcutils libstdc++ libc LOCAL_C_INCLUDES += $(LOCAL_PATH)/.. diff --git a/updater/install.c b/updater/install.c index 2336f61..2e965ce 100644 --- a/updater/install.c +++ b/updater/install.c @@ -24,6 +24,8 @@ #include #include +#include "cutils/misc.h" +#include "cutils/properties.h" #include "edify/expr.h" #include "minzip/DirUtil.h" #include "mtdutils/mounts.h" @@ -40,6 +42,7 @@ char* ErrorAbort(void* cookie, char* format, ...) { return NULL; } + // mount(type, location, mount_point) // // what: type="MTD" location="" to mount a yaffs2 filesystem @@ -104,6 +107,36 @@ done: return result; } + +// is_mounted(mount_point) +char* IsMountedFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + if (argc != 1) { + return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + } + char* mount_point; + if (ReadArgs(cookie, argv, 1, &mount_point) < 0) { + return NULL; + } + if (strlen(mount_point) == 0) { + ErrorAbort(cookie, "mount_point argument to unmount() can't be empty"); + goto done; + } + + scan_mounted_volumes(); + const MountedVolume* vol = find_mounted_volume_by_mount_point(mount_point); + if (vol == NULL) { + result = strdup(""); + } else { + result = mount_point; + } + +done: + if (result != mount_point) free(mount_point); + return result; +} + + char* UnmountFn(const char* name, void* cookie, int argc, Expr* argv[]) { char* result = NULL; if (argc != 1) { @@ -132,6 +165,8 @@ done: if (result != mount_point) free(mount_point); return result; } + + // format(type, location) // // type="MTD" location=partition @@ -192,6 +227,7 @@ done: return result; } + char* DeleteFn(const char* name, void* cookie, int argc, Expr* argv[]) { char** paths = malloc(argc * sizeof(char*)); int i; @@ -222,6 +258,7 @@ char* DeleteFn(const char* name, void* cookie, int argc, Expr* argv[]) { return strdup(buffer); } + char* ShowProgressFn(const char* name, void* cookie, int argc, Expr* argv[]) { if (argc != 2) { return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); @@ -243,8 +280,9 @@ char* ShowProgressFn(const char* name, void* cookie, int argc, Expr* argv[]) { return strdup(""); } -// package_extract package_path destination_path -char* PackageExtractFn(const char* name, void* cookie, int argc, Expr* argv[]) { +// package_extract_dir(package_path, destination_path) +char* PackageExtractDirFn(const char* name, void* cookie, + int argc, Expr* argv[]) { if (argc != 2) { return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); } @@ -265,6 +303,42 @@ char* PackageExtractFn(const char* name, void* cookie, int argc, Expr* argv[]) { return strdup(success ? "t" : ""); } + +// package_extract_file(package_path, destination_path) +char* PackageExtractFileFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + if (argc != 2) { + return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + } + char* zip_path; + char* dest_path; + if (ReadArgs(cookie, argv, 2, &zip_path, &dest_path) < 0) return NULL; + + bool success = false; + + ZipArchive* za = ((UpdaterInfo*)cookie)->package_zip; + const ZipEntry* entry = mzFindZipEntry(za, zip_path); + if (entry == NULL) { + fprintf(stderr, "%s: no %s in package\n", name, zip_path); + goto done; + } + + FILE* f = fopen(dest_path, "wb"); + if (f == NULL) { + fprintf(stderr, "%s: can't open %s for write: %s\n", + name, dest_path, strerror(errno)); + goto done; + } + success = mzExtractZipEntryToFile(za, entry, fileno(f)); + fclose(f); + + done: + free(zip_path); + free(dest_path); + return strdup(success ? "t" : ""); +} + + // symlink target src1 src2 ... char* SymlinkFn(const char* name, void* cookie, int argc, Expr* argv[]) { if (argc == 0) { @@ -289,6 +363,7 @@ char* SymlinkFn(const char* name, void* cookie, int argc, Expr* argv[]) { return strdup(""); } + char* SetPermFn(const char* name, void* cookie, int argc, Expr* argv[]) { char* result = NULL; bool recursive = (strcmp(name, "set_perm_recursive") == 0); @@ -356,15 +431,209 @@ done: return result; } + +char* GetPropFn(const char* name, void* cookie, int argc, Expr* argv[]) { + if (argc != 1) { + return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + } + char* key; + key = Evaluate(cookie, argv[0]); + if (key == NULL) return NULL; + + char value[PROPERTY_VALUE_MAX]; + property_get(key, value, ""); + free(key); + + return strdup(value); +} + + +static bool write_raw_image_cb(const unsigned char* data, + int data_len, void* ctx) { + int r = mtd_write_data((MtdWriteContext*)ctx, (const char *)data, data_len); + if (r == data_len) return true; + fprintf(stderr, "%s\n", strerror(errno)); + return false; +} + +// write_raw_image(file, partition) +char* WriteRawImageFn(const char* name, void* cookie, int argc, Expr* argv[]) { + char* result = NULL; + + char* partition; + char* filename; + if (ReadArgs(cookie, argv, 2, &filename, &partition) < 0) { + return NULL; + } + + if (strlen(partition) == 0) { + ErrorAbort(cookie, "partition argument to %s can't be empty", name); + goto done; + } + if (strlen(filename) == 0) { + ErrorAbort(cookie, "file argument to %s can't be empty", name); + goto done; + } + + mtd_scan_partitions(); + const MtdPartition* mtd = mtd_find_partition_by_name(partition); + if (mtd == NULL) { + fprintf(stderr, "%s: no mtd partition named \"%s\"\n", name, partition); + result = strdup(""); + goto done; + } + + MtdWriteContext* ctx = mtd_write_partition(mtd); + if (ctx == NULL) { + fprintf(stderr, "%s: can't write mtd partition \"%s\"\n", + name, partition); + result = strdup(""); + goto done; + } + + bool success; + + FILE* f = fopen(filename, "rb"); + if (f == NULL) { + fprintf(stderr, "%s: can't open %s: %s\n", + name, filename, strerror(errno)); + result = strdup(""); + goto done; + } + + success = true; + char* buffer = malloc(BUFSIZ); + int read; + while (success && (read = fread(buffer, 1, BUFSIZ, f)) > 0) { + int wrote = mtd_write_data(ctx, buffer, read); + success = success && (wrote == read); + if (!success) { + fprintf(stderr, "mtd_write_data to %s failed: %s\n", + partition, strerror(errno)); + } + } + free(buffer); + fclose(f); + + printf("%s %s partition from %s\n", + success ? "wrote" : "failed to write", partition, filename); + + result = success ? partition : strdup(""); + +done: + if (result != partition) free(partition); + free(filename); + return result; +} + +// write_firmware_image(file, partition) +// +// partition is "radio" or "hboot" +// file is not used until after updater exits +// +// TODO: this should live in some HTC-specific library +char* WriteFirmwareImageFn(const char* name, void* cookie, + int argc, Expr* argv[]) { + char* result = NULL; + + char* partition; + char* filename; + if (ReadArgs(cookie, argv, 2, &filename, &partition) < 0) { + return NULL; + } + + if (strlen(partition) == 0) { + ErrorAbort(cookie, "partition argument to %s can't be empty", name); + goto done; + } + if (strlen(filename) == 0) { + ErrorAbort(cookie, "file argument to %s can't be empty", name); + goto done; + } + + FILE* cmd = ((UpdaterInfo*)cookie)->cmd_pipe; + fprintf(cmd, "firmware %s %s\n", partition, filename); + + printf("will write %s firmware from %s\n", partition, filename); + result = partition; + +done: + if (result != partition) free(partition); + free(filename); + return result; +} + + +extern int applypatch(int argc, char** argv); + +// apply_patch(srcfile, tgtfile, tgtsha1, tgtsize, sha1:patch, ...) +// apply_patch_check(file, sha1, ...) +// apply_patch_space(bytes) +char* ApplyPatchFn(const char* name, void* cookie, int argc, Expr* argv[]) { + printf("in applypatchfn (%s)\n", name); + + char* prepend = NULL; + if (strstr(name, "check") != NULL) { + prepend = "-c"; + } else if (strstr(name, "space") != NULL) { + prepend = "-s"; + } + + char** args = ReadVarArgs(cookie, argc, argv); + if (args == NULL) return NULL; + + // insert the "program name" argv[0] and a copy of the "prepend" + // string (if any) at the start of the args. + + int extra = 1 + (prepend != NULL ? 1 : 0); + char** temp = malloc((argc+extra) * sizeof(char*)); + memcpy(temp+extra, args, argc * sizeof(char*)); + temp[0] = strdup("updater"); + if (prepend) { + temp[1] = strdup(prepend); + } + free(args); + args = temp; + argc += extra; + + printf("calling applypatch\n"); + fflush(stdout); + int result = applypatch(argc, args); + printf("applypatch returned %d\n", result); + + int i; + for (i = 0; i < argc; ++i) { + free(args[i]); + } + free(args); + + switch (result) { + case 0: return strdup("t"); + case 1: return strdup(""); + default: return ErrorAbort(cookie, "applypatch couldn't parse args"); + } +} + + void RegisterInstallFunctions() { RegisterFunction("mount", MountFn); + RegisterFunction("is_mounted", IsMountedFn); RegisterFunction("unmount", UnmountFn); RegisterFunction("format", FormatFn); RegisterFunction("show_progress", ShowProgressFn); RegisterFunction("delete", DeleteFn); RegisterFunction("delete_recursive", DeleteFn); - RegisterFunction("package_extract", PackageExtractFn); + RegisterFunction("package_extract_dir", PackageExtractDirFn); + RegisterFunction("package_extract_file", PackageExtractFileFn); RegisterFunction("symlink", SymlinkFn); RegisterFunction("set_perm", SetPermFn); RegisterFunction("set_perm_recursive", SetPermFn); + + RegisterFunction("getprop", GetPropFn); + RegisterFunction("write_raw_image", WriteRawImageFn); + RegisterFunction("write_firmware_image", WriteFirmwareImageFn); + + RegisterFunction("apply_patch", ApplyPatchFn); + RegisterFunction("apply_patch_check", ApplyPatchFn); + RegisterFunction("apply_patch_space", ApplyPatchFn); } diff --git a/updater/updater.c b/updater/updater.c index aa03803..0977625 100644 --- a/updater/updater.c +++ b/updater/updater.c @@ -80,10 +80,11 @@ int main(int argc, char** argv) { // Parse the script. Expr* root; + int error_count = 0; yy_scan_string(script); - int error = yyparse(&root); - if (error != 0) { - fprintf(stderr, "%d parse errors\n", error); + int error = yyparse(&root, &error_count); + if (error != 0 || error_count > 0) { + fprintf(stderr, "%d parse errors\n", error_count); return 6; } From d9c9d10d9da76f067d3955bea71f7bb39e859fa5 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Fri, 12 Jun 2009 12:24:39 -0700 Subject: [PATCH 17/24] fixes to edify and updater script A few more changes to edify: - fix write_raw_image(); my last change neglected to close the write context, so the written image was corrupt. - each expression tracks the span of the source code from which it was compiled, so that assert()'s error message can include the source of the expression that failed. - the 'cookie' argument to each Function is replaced with a State object, which contains the cookie, the source script (for use with the above spans), and the current error message (replacing the global variables that were used for this purpose). - in the recovery image, a new command "ui_print" can be sent back through the command pipe to cause text to appear on the screen. Add a new ui_print() function to print things from scripts. Rename existing "print" function to "stdout". --- edify/expr.c | 520 +++++++++++++++++++++++----------------------- edify/expr.h | 61 ++++-- edify/lexer.l | 47 +++-- edify/main.c | 269 +++++++++++++----------- edify/parser.y | 31 +-- edify/yydefs.h | 36 ++++ install.c | 10 + updater/install.c | 166 +++++++++------ updater/updater.c | 25 ++- 9 files changed, 665 insertions(+), 500 deletions(-) create mode 100644 edify/yydefs.h diff --git a/edify/expr.c b/edify/expr.c index 5470a2b..406c67e 100644 --- a/edify/expr.c +++ b/edify/expr.c @@ -29,249 +29,241 @@ // - if Evaluate() on any argument returns NULL, return NULL. int BooleanString(const char* s) { - return s[0] != '\0'; + return s[0] != '\0'; } -char* Evaluate(void* cookie, Expr* expr) { - return expr->fn(expr->name, cookie, expr->argc, expr->argv); +char* Evaluate(State* state, Expr* expr) { + return expr->fn(expr->name, state, expr->argc, expr->argv); } -char* ConcatFn(const char* name, void* cookie, int argc, Expr* argv[]) { - if (argc == 0) { - return strdup(""); - } - char** strings = malloc(argc * sizeof(char*)); - int i; - for (i = 0; i < argc; ++i) { - strings[i] = NULL; - } - char* result = NULL; - int length = 0; - for (i = 0; i < argc; ++i) { - strings[i] = Evaluate(cookie, argv[i]); - if (strings[i] == NULL) { - goto done; +char* ConcatFn(const char* name, State* state, int argc, Expr* argv[]) { + if (argc == 0) { + return strdup(""); + } + char** strings = malloc(argc * sizeof(char*)); + int i; + for (i = 0; i < argc; ++i) { + strings[i] = NULL; + } + char* result = NULL; + int length = 0; + for (i = 0; i < argc; ++i) { + strings[i] = Evaluate(state, argv[i]); + if (strings[i] == NULL) { + goto done; + } + length += strlen(strings[i]); } - length += strlen(strings[i]); - } - result = malloc(length+1); - int p = 0; - for (i = 0; i < argc; ++i) { - strcpy(result+p, strings[i]); - p += strlen(strings[i]); - } - result[p] = '\0'; + result = malloc(length+1); + int p = 0; + for (i = 0; i < argc; ++i) { + strcpy(result+p, strings[i]); + p += strlen(strings[i]); + } + result[p] = '\0'; -done: - for (i = 0; i < argc; ++i) { - free(strings[i]); - } - return result; + done: + for (i = 0; i < argc; ++i) { + free(strings[i]); + } + return result; } -char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]) { - if (argc != 2 && argc != 3) { - return NULL; - } - char* cond = Evaluate(cookie, argv[0]); - if (cond == NULL) { - return NULL; - } +char* IfElseFn(const char* name, State* state, int argc, Expr* argv[]) { + if (argc != 2 && argc != 3) { + return NULL; + } + char* cond = Evaluate(state, argv[0]); + if (cond == NULL) { + return NULL; + } - if (BooleanString(cond) == true) { - free(cond); - return Evaluate(cookie, argv[1]); - } else { - if (argc == 3) { - free(cond); - return Evaluate(cookie, argv[2]); + if (BooleanString(cond) == true) { + free(cond); + return Evaluate(state, argv[1]); } else { - return cond; + if (argc == 3) { + free(cond); + return Evaluate(state, argv[2]); + } else { + return cond; + } } - } } -char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]) { - char* msg = NULL; - if (argc > 0) { - msg = Evaluate(cookie, argv[0]); - } - SetError(msg == NULL ? "called abort()" : msg); - free(msg); - return NULL; -} - -char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]) { - int i; - for (i = 0; i < argc; ++i) { - char* v = Evaluate(cookie, argv[i]); - if (v == NULL) { - return NULL; +char* AbortFn(const char* name, State* state, int argc, Expr* argv[]) { + char* msg = NULL; + if (argc > 0) { + msg = Evaluate(state, argv[0]); } - int b = BooleanString(v); - free(v); - if (!b) { - SetError("assert() failed"); - return NULL; + free(state->errmsg); + if (msg) { + state->errmsg = msg; + } else { + state->errmsg = strdup("called abort()"); } - } - return strdup(""); -} - -char* SleepFn(const char* name, void* cookie, int argc, Expr* argv[]) { - char* val = Evaluate(cookie, argv[0]); - if (val == NULL) { return NULL; - } - int v = strtol(val, NULL, 10); - sleep(v); - return val; } -char* PrintFn(const char* name, void* cookie, int argc, Expr* argv[]) { - int i; - for (i = 0; i < argc; ++i) { - char* v = Evaluate(cookie, argv[i]); - if (v == NULL) { - return NULL; +char* AssertFn(const char* name, State* state, int argc, Expr* argv[]) { + int i; + for (i = 0; i < argc; ++i) { + char* v = Evaluate(state, argv[i]); + if (v == NULL) { + return NULL; + } + int b = BooleanString(v); + free(v); + if (!b) { + int prefix_len; + int len = argv[i]->end - argv[i]->start; + char* err_src = malloc(len + 20); + strcpy(err_src, "assert failed: "); + prefix_len = strlen(err_src); + memcpy(err_src + prefix_len, state->script + argv[i]->start, len); + err_src[prefix_len + len] = '\0'; + free(state->errmsg); + state->errmsg = err_src; + return NULL; + } } - fputs(v, stdout); - free(v); - } - return strdup(""); -} - -char* LogicalAndFn(const char* name, void* cookie, - int argc, Expr* argv[]) { - char* left = Evaluate(cookie, argv[0]); - if (left == NULL) return NULL; - if (BooleanString(left) == true) { - free(left); - return Evaluate(cookie, argv[1]); - } else { - return left; - } -} - -char* LogicalOrFn(const char* name, void* cookie, - int argc, Expr* argv[]) { - char* left = Evaluate(cookie, argv[0]); - if (left == NULL) return NULL; - if (BooleanString(left) == false) { - free(left); - return Evaluate(cookie, argv[1]); - } else { - return left; - } -} - -char* LogicalNotFn(const char* name, void* cookie, - int argc, Expr* argv[]) { - char* val = Evaluate(cookie, argv[0]); - if (val == NULL) return NULL; - bool bv = BooleanString(val); - free(val); - if (bv) { return strdup(""); - } else { - return strdup("t"); - } } -char* SubstringFn(const char* name, void* cookie, +char* SleepFn(const char* name, State* state, int argc, Expr* argv[]) { + char* val = Evaluate(state, argv[0]); + if (val == NULL) { + return NULL; + } + int v = strtol(val, NULL, 10); + sleep(v); + return val; +} + +char* StdoutFn(const char* name, State* state, int argc, Expr* argv[]) { + int i; + for (i = 0; i < argc; ++i) { + char* v = Evaluate(state, argv[i]); + if (v == NULL) { + return NULL; + } + fputs(v, stdout); + free(v); + } + return strdup(""); +} + +char* LogicalAndFn(const char* name, State* state, + int argc, Expr* argv[]) { + char* left = Evaluate(state, argv[0]); + if (left == NULL) return NULL; + if (BooleanString(left) == true) { + free(left); + return Evaluate(state, argv[1]); + } else { + return left; + } +} + +char* LogicalOrFn(const char* name, State* state, int argc, Expr* argv[]) { - char* needle = Evaluate(cookie, argv[0]); - if (needle == NULL) return NULL; - char* haystack = Evaluate(cookie, argv[1]); - if (haystack == NULL) { + char* left = Evaluate(state, argv[0]); + if (left == NULL) return NULL; + if (BooleanString(left) == false) { + free(left); + return Evaluate(state, argv[1]); + } else { + return left; + } +} + +char* LogicalNotFn(const char* name, State* state, + int argc, Expr* argv[]) { + char* val = Evaluate(state, argv[0]); + if (val == NULL) return NULL; + bool bv = BooleanString(val); + free(val); + if (bv) { + return strdup(""); + } else { + return strdup("t"); + } +} + +char* SubstringFn(const char* name, State* state, + int argc, Expr* argv[]) { + char* needle = Evaluate(state, argv[0]); + if (needle == NULL) return NULL; + char* haystack = Evaluate(state, argv[1]); + if (haystack == NULL) { + free(needle); + return NULL; + } + + char* result = strdup(strstr(haystack, needle) ? "t" : ""); free(needle); - return NULL; - } - - char* result = strdup(strstr(haystack, needle) ? "t" : ""); - free(needle); - free(haystack); - return result; + free(haystack); + return result; } -char* EqualityFn(const char* name, void* cookie, int argc, Expr* argv[]) { - char* left = Evaluate(cookie, argv[0]); - if (left == NULL) return NULL; - char* right = Evaluate(cookie, argv[1]); - if (right == NULL) { +char* EqualityFn(const char* name, State* state, int argc, Expr* argv[]) { + char* left = Evaluate(state, argv[0]); + if (left == NULL) return NULL; + char* right = Evaluate(state, argv[1]); + if (right == NULL) { + free(left); + return NULL; + } + + char* result = strdup(strcmp(left, right) == 0 ? "t" : ""); free(left); - return NULL; - } - - char* result = strdup(strcmp(left, right) == 0 ? "t" : ""); - free(left); - free(right); - return result; + free(right); + return result; } -char* InequalityFn(const char* name, void* cookie, int argc, Expr* argv[]) { - char* left = Evaluate(cookie, argv[0]); - if (left == NULL) return NULL; - char* right = Evaluate(cookie, argv[1]); - if (right == NULL) { +char* InequalityFn(const char* name, State* state, int argc, Expr* argv[]) { + char* left = Evaluate(state, argv[0]); + if (left == NULL) return NULL; + char* right = Evaluate(state, argv[1]); + if (right == NULL) { + free(left); + return NULL; + } + + char* result = strdup(strcmp(left, right) != 0 ? "t" : ""); free(left); - return NULL; - } - - char* result = strdup(strcmp(left, right) != 0 ? "t" : ""); - free(left); - free(right); - return result; + free(right); + return result; } -char* SequenceFn(const char* name, void* cookie, int argc, Expr* argv[]) { - char* left = Evaluate(cookie, argv[0]); - if (left == NULL) return NULL; - free(left); - return Evaluate(cookie, argv[1]); +char* SequenceFn(const char* name, State* state, int argc, Expr* argv[]) { + char* left = Evaluate(state, argv[0]); + if (left == NULL) return NULL; + free(left); + return Evaluate(state, argv[1]); } -char* Literal(const char* name, void* cookie, int argc, Expr* argv[]) { - return strdup(name); +char* Literal(const char* name, State* state, int argc, Expr* argv[]) { + return strdup(name); } -Expr* Build(Function fn, int count, ...) { - va_list v; - va_start(v, count); - Expr* e = malloc(sizeof(Expr)); - e->fn = fn; - e->name = "(operator)"; - e->argc = count; - e->argv = malloc(count * sizeof(Expr*)); - int i; - for (i = 0; i < count; ++i) { - e->argv[i] = va_arg(v, Expr*); - } - va_end(v); - return e; -} - -// ----------------------------------------------------------------- -// error reporting -// ----------------------------------------------------------------- - -static char* error_message = NULL; - -void SetError(const char* message) { - if (error_message) { - free(error_message); - } - error_message = strdup(message); -} - -const char* GetError() { - return error_message; -} - -void ClearError() { - free(error_message); - error_message = NULL; +Expr* Build(Function fn, YYLTYPE loc, int count, ...) { + va_list v; + va_start(v, count); + Expr* e = malloc(sizeof(Expr)); + e->fn = fn; + e->name = "(operator)"; + e->argc = count; + e->argv = malloc(count * sizeof(Expr*)); + int i; + for (i = 0; i < count; ++i) { + e->argv[i] = va_arg(v, Expr*); + } + va_end(v); + e->start = loc.start; + e->end = loc.end; + return e; } // ----------------------------------------------------------------- @@ -283,44 +275,44 @@ static int fn_size = 0; NamedFunction* fn_table = NULL; void RegisterFunction(const char* name, Function fn) { - if (fn_entries >= fn_size) { - fn_size = fn_size*2 + 1; - fn_table = realloc(fn_table, fn_size * sizeof(NamedFunction)); - } - fn_table[fn_entries].name = name; - fn_table[fn_entries].fn = fn; - ++fn_entries; + if (fn_entries >= fn_size) { + fn_size = fn_size*2 + 1; + fn_table = realloc(fn_table, fn_size * sizeof(NamedFunction)); + } + fn_table[fn_entries].name = name; + fn_table[fn_entries].fn = fn; + ++fn_entries; } static int fn_entry_compare(const void* a, const void* b) { - const char* na = ((const NamedFunction*)a)->name; - const char* nb = ((const NamedFunction*)b)->name; - return strcmp(na, nb); + const char* na = ((const NamedFunction*)a)->name; + const char* nb = ((const NamedFunction*)b)->name; + return strcmp(na, nb); } void FinishRegistration() { - qsort(fn_table, fn_entries, sizeof(NamedFunction), fn_entry_compare); + qsort(fn_table, fn_entries, sizeof(NamedFunction), fn_entry_compare); } Function FindFunction(const char* name) { - NamedFunction key; - key.name = name; - NamedFunction* nf = bsearch(&key, fn_table, fn_entries, sizeof(NamedFunction), - fn_entry_compare); - if (nf == NULL) { - return NULL; - } - return nf->fn; + NamedFunction key; + key.name = name; + NamedFunction* nf = bsearch(&key, fn_table, fn_entries, + sizeof(NamedFunction), fn_entry_compare); + if (nf == NULL) { + return NULL; + } + return nf->fn; } void RegisterBuiltins() { - RegisterFunction("ifelse", IfElseFn); - RegisterFunction("abort", AbortFn); - RegisterFunction("assert", AssertFn); - RegisterFunction("concat", ConcatFn); - RegisterFunction("is_substring", SubstringFn); - RegisterFunction("print", PrintFn); - RegisterFunction("sleep", SleepFn); + RegisterFunction("ifelse", IfElseFn); + RegisterFunction("abort", AbortFn); + RegisterFunction("assert", AssertFn); + RegisterFunction("concat", ConcatFn); + RegisterFunction("is_substring", SubstringFn); + RegisterFunction("stdout", StdoutFn); + RegisterFunction("sleep", SleepFn); } @@ -331,44 +323,44 @@ void RegisterBuiltins() { // Evaluate the expressions in argv, giving 'count' char* (the ... is // zero or more char** to put them in). If any expression evaluates // to NULL, free the rest and return -1. Return 0 on success. -int ReadArgs(void* cookie, Expr* argv[], int count, ...) { - char** args = malloc(count * sizeof(char*)); - va_list v; - va_start(v, count); - int i; - for (i = 0; i < count; ++i) { - args[i] = Evaluate(cookie, argv[i]); - if (args[i] == NULL) { - va_end(v); - int j; - for (j = 0; j < i; ++j) { - free(args[j]); - } - return -1; +int ReadArgs(State* state, Expr* argv[], int count, ...) { + char** args = malloc(count * sizeof(char*)); + va_list v; + va_start(v, count); + int i; + for (i = 0; i < count; ++i) { + args[i] = Evaluate(state, argv[i]); + if (args[i] == NULL) { + va_end(v); + int j; + for (j = 0; j < i; ++j) { + free(args[j]); + } + return -1; + } + *(va_arg(v, char**)) = args[i]; } - *(va_arg(v, char**)) = args[i]; - } - va_end(v); - return 0; + va_end(v); + return 0; } // Evaluate the expressions in argv, returning an array of char* // results. If any evaluate to NULL, free the rest and return NULL. // The caller is responsible for freeing the returned array and the // strings it contains. -char** ReadVarArgs(void* cookie, int argc, Expr* argv[]) { - char** args = (char**)malloc(argc * sizeof(char*)); - int i = 0; - for (i = 0; i < argc; ++i) { - args[i] = Evaluate(cookie, argv[i]); - if (args[i] == NULL) { - int j; - for (j = 0; j < i; ++j) { - free(args[j]); - } - free(args); - return NULL; +char** ReadVarArgs(State* state, int argc, Expr* argv[]) { + char** args = (char**)malloc(argc * sizeof(char*)); + int i = 0; + for (i = 0; i < argc; ++i) { + args[i] = Evaluate(state, argv[i]); + if (args[i] == NULL) { + int j; + for (j = 0; j < i; ++j) { + free(args[j]); + } + free(args); + return NULL; + } } - } - return args; + return args; } diff --git a/edify/expr.h b/edify/expr.h index cfbef90..671b499 100644 --- a/edify/expr.h +++ b/edify/expr.h @@ -17,45 +17,64 @@ #ifndef _EXPRESSION_H #define _EXPRESSION_H +#include "yydefs.h" + #define MAX_STRING_LEN 1024 typedef struct Expr Expr; -typedef char* (*Function)(const char* name, void* cookie, +typedef struct { + // Optional pointer to app-specific data; the core of edify never + // uses this value. + void* cookie; + + // The source of the original script. Must be NULL-terminated, + // and in writable memory (Evaluate may make temporary changes to + // it but will restore it when done). + char* script; + + // The error message (if any) returned if the evaluation aborts. + // Should be NULL initially, will be either NULL or a malloc'd + // pointer after Evaluate() returns. + char* errmsg; +} State; + +typedef char* (*Function)(const char* name, State* state, int argc, Expr* argv[]); struct Expr { - Function fn; - char* name; - int argc; - Expr** argv; + Function fn; + char* name; + int argc; + Expr** argv; + int start, end; }; -char* Evaluate(void* cookie, Expr* expr); +char* Evaluate(State* state, Expr* expr); // Glue to make an Expr out of a literal. -char* Literal(const char* name, void* cookie, int argc, Expr* argv[]); +char* Literal(const char* name, State* state, int argc, Expr* argv[]); // Functions corresponding to various syntactic sugar operators. // ("concat" is also available as a builtin function, to concatenate // more than two strings.) -char* ConcatFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* LogicalAndFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* LogicalOrFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* LogicalNotFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* SubstringFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* EqualityFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* InequalityFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* SequenceFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* ConcatFn(const char* name, State* state, int argc, Expr* argv[]); +char* LogicalAndFn(const char* name, State* state, int argc, Expr* argv[]); +char* LogicalOrFn(const char* name, State* state, int argc, Expr* argv[]); +char* LogicalNotFn(const char* name, State* state, int argc, Expr* argv[]); +char* SubstringFn(const char* name, State* state, int argc, Expr* argv[]); +char* EqualityFn(const char* name, State* state, int argc, Expr* argv[]); +char* InequalityFn(const char* name, State* state, int argc, Expr* argv[]); +char* SequenceFn(const char* name, State* state, int argc, Expr* argv[]); // Convenience function for building expressions with a fixed number // of arguments. -Expr* Build(Function fn, int count, ...); +Expr* Build(Function fn, YYLTYPE loc, int count, ...); // Global builtins, registered by RegisterBuiltins(). -char* IfElseFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* AssertFn(const char* name, void* cookie, int argc, Expr* argv[]); -char* AbortFn(const char* name, void* cookie, int argc, Expr* argv[]); +char* IfElseFn(const char* name, State* state, int argc, Expr* argv[]); +char* AssertFn(const char* name, State* state, int argc, Expr* argv[]); +char* AbortFn(const char* name, State* state, int argc, Expr* argv[]); // For setting and getting the global error string (when returning @@ -91,13 +110,13 @@ Function FindFunction(const char* name); // Evaluate the expressions in argv, giving 'count' char* (the ... is // zero or more char** to put them in). If any expression evaluates // to NULL, free the rest and return -1. Return 0 on success. -int ReadArgs(void* cookie, Expr* argv[], int count, ...); +int ReadArgs(State* state, Expr* argv[], int count, ...); // Evaluate the expressions in argv, returning an array of char* // results. If any evaluate to NULL, free the rest and return NULL. // The caller is responsible for freeing the returned array and the // strings it contains. -char** ReadVarArgs(void* cookie, int argc, Expr* argv[]); +char** ReadVarArgs(State* state, int argc, Expr* argv[]); #endif // _EXPRESSION_H diff --git a/edify/lexer.l b/edify/lexer.l index cb5eb31..2c4489c 100644 --- a/edify/lexer.l +++ b/edify/lexer.l @@ -16,14 +16,20 @@ */ #include "expr.h" +#include "yydefs.h" #include "parser.h" int gLine = 1; int gColumn = 1; +int gPos = 0; // TODO: enforce MAX_STRING_LEN during lexing char string_buffer[MAX_STRING_LEN]; char* string_pos; + +#define ADVANCE do {yylloc.start=gPos; yylloc.end=gPos+yyleng; \ + gColumn+=yyleng; gPos+=yyleng;} while(0) + %} %x STR @@ -34,27 +40,32 @@ char* string_pos; \" { - ++gColumn; BEGIN(STR); string_pos = string_buffer; + yylloc.start = gPos; + ++gColumn; + ++gPos; } { \" { ++gColumn; + ++gPos; BEGIN(INITIAL); *string_pos = '\0'; yylval.str = strdup(string_buffer); + yylloc.end = gPos; return STRING; } - \\n { gColumn += yyleng; *string_pos++ = '\n'; } - \\t { gColumn += yyleng; *string_pos++ = '\t'; } - \\\" { gColumn += yyleng; *string_pos++ = '\"'; } - \\\\ { gColumn += yyleng; *string_pos++ = '\\'; } + \\n { gColumn += yyleng; gPos += yyleng; *string_pos++ = '\n'; } + \\t { gColumn += yyleng; gPos += yyleng; *string_pos++ = '\t'; } + \\\" { gColumn += yyleng; gPos += yyleng; *string_pos++ = '\"'; } + \\\\ { gColumn += yyleng; gPos += yyleng; *string_pos++ = '\\'; } \\x[0-9a-fA-F]{2} { gColumn += yyleng; + gPos += yyleng; int val; sscanf(yytext+2, "%x", &val); *string_pos++ = val; @@ -62,36 +73,38 @@ char* string_pos; \n { ++gLine; + ++gPos; gColumn = 1; *string_pos++ = yytext[0]; } . { ++gColumn; + ++gPos; *string_pos++ = yytext[0]; } } -if { gColumn += yyleng; return IF; } -then { gColumn += yyleng; return THEN; } -else { gColumn += yyleng; return ELSE; } -endif { gColumn += yyleng; return ENDIF; } +if ADVANCE; return IF; +then ADVANCE; return THEN; +else ADVANCE; return ELSE; +endif ADVANCE; return ENDIF; [a-zA-Z0-9_:/.]+ { - gColumn += yyleng; + ADVANCE; yylval.str = strdup(yytext); return STRING; } -\&\& { gColumn += yyleng; return AND; } -\|\| { gColumn += yyleng; return OR; } -== { gColumn += yyleng; return EQ; } -!= { gColumn += yyleng; return NE; } +\&\& ADVANCE; return AND; +\|\| ADVANCE; return OR; +== ADVANCE; return EQ; +!= ADVANCE; return NE; -[+(),!;] { gColumn += yyleng; return yytext[0]; } +[+(),!;] ADVANCE; return yytext[0]; -[ \t]+ gColumn += yyleng; +[ \t]+ ADVANCE; -(#.*)?\n { ++gLine; gColumn = 1; } +(#.*)?\n gPos += yyleng; ++gLine; gColumn = 1; . return BAD; diff --git a/edify/main.c b/edify/main.c index 7da89e2..03eefc6 100644 --- a/edify/main.c +++ b/edify/main.c @@ -21,152 +21,183 @@ #include "expr.h" #include "parser.h" +extern int yyparse(Expr** root, int* error_count); + int expect(const char* expr_str, const char* expected, int* errors) { - Expr* e; - int error; - char* result; + Expr* e; + int error; + char* result; - printf("."); + printf("."); - yy_scan_string(expr_str); - error = yyparse(&e); - if (error > 0) { - fprintf(stderr, "error parsing \"%s\"\n", expr_str); - ++*errors; - return 0; - } + yy_scan_string(expr_str); + int error_count = 0; + error = yyparse(&e, &error_count); + if (error > 0 || error_count > 0) { + fprintf(stderr, "error parsing \"%s\" (%d errors)\n", + expr_str, error_count); + ++*errors; + return 0; + } - result = Evaluate(NULL, e); - if (result == NULL && expected != NULL) { - fprintf(stderr, "error evaluating \"%s\"\n", expr_str); - ++*errors; - return 0; - } + State state; + state.cookie = NULL; + state.script = expr_str; + state.errmsg = NULL; - if (result == NULL && expected == NULL) { - return 1; - } + result = Evaluate(&state, e); + free(state.errmsg); + if (result == NULL && expected != NULL) { + fprintf(stderr, "error evaluating \"%s\"\n", expr_str); + ++*errors; + return 0; + } + + if (result == NULL && expected == NULL) { + return 1; + } + + if (strcmp(result, expected) != 0) { + fprintf(stderr, "evaluating \"%s\": expected \"%s\", got \"%s\"\n", + expr_str, expected, result); + ++*errors; + free(result); + return 0; + } - if (strcmp(result, expected) != 0) { - fprintf(stderr, "evaluating \"%s\": expected \"%s\", got \"%s\"\n", - expr_str, expected, result); - ++*errors; free(result); - return 0; - } - - free(result); - return 1; + return 1; } int test() { - int errors = 0; + int errors = 0; - expect("a", "a", &errors); - expect("\"a\"", "a", &errors); - expect("\"\\x61\"", "a", &errors); - expect("# this is a comment\n" - " a\n" - " \n", - "a", &errors); + expect("a", "a", &errors); + expect("\"a\"", "a", &errors); + expect("\"\\x61\"", "a", &errors); + expect("# this is a comment\n" + " a\n" + " \n", + "a", &errors); - // sequence operator - expect("a; b; c", "c", &errors); + // sequence operator + expect("a; b; c", "c", &errors); - // string concat operator - expect("a + b", "ab", &errors); - expect("a + \n \"b\"", "ab", &errors); - expect("a + b +\nc\n", "abc", &errors); + // string concat operator + expect("a + b", "ab", &errors); + expect("a + \n \"b\"", "ab", &errors); + expect("a + b +\nc\n", "abc", &errors); - // string concat function - expect("concat(a, b)", "ab", &errors); - expect("concat(a,\n \"b\")", "ab", &errors); - expect("concat(a + b,\nc,\"d\")", "abcd", &errors); - expect("\"concat\"(a + b,\nc,\"d\")", "abcd", &errors); + // string concat function + expect("concat(a, b)", "ab", &errors); + expect("concat(a,\n \"b\")", "ab", &errors); + expect("concat(a + b,\nc,\"d\")", "abcd", &errors); + expect("\"concat\"(a + b,\nc,\"d\")", "abcd", &errors); - // logical and - expect("a && b", "b", &errors); - expect("a && \"\"", "", &errors); - expect("\"\" && b", "", &errors); - expect("\"\" && \"\"", "", &errors); - expect("\"\" && abort()", "", &errors); // test short-circuiting - expect("t && abort()", NULL, &errors); + // logical and + expect("a && b", "b", &errors); + expect("a && \"\"", "", &errors); + expect("\"\" && b", "", &errors); + expect("\"\" && \"\"", "", &errors); + expect("\"\" && abort()", "", &errors); // test short-circuiting + expect("t && abort()", NULL, &errors); - // logical or - expect("a || b", "a", &errors); - expect("a || \"\"", "a", &errors); - expect("\"\" || b", "b", &errors); - expect("\"\" || \"\"", "", &errors); - expect("a || abort()", "a", &errors); // test short-circuiting - expect("\"\" || abort()", NULL, &errors); + // logical or + expect("a || b", "a", &errors); + expect("a || \"\"", "a", &errors); + expect("\"\" || b", "b", &errors); + expect("\"\" || \"\"", "", &errors); + expect("a || abort()", "a", &errors); // test short-circuiting + expect("\"\" || abort()", NULL, &errors); - // logical not - expect("!a", "", &errors); - expect("! \"\"", "t", &errors); - expect("!!a", "t", &errors); + // logical not + expect("!a", "", &errors); + expect("! \"\"", "t", &errors); + expect("!!a", "t", &errors); - // precedence - expect("\"\" == \"\" && b", "b", &errors); - expect("a + b == ab", "t", &errors); - expect("ab == a + b", "t", &errors); - expect("a + (b == ab)", "a", &errors); - expect("(ab == a) + b", "b", &errors); + // precedence + expect("\"\" == \"\" && b", "b", &errors); + expect("a + b == ab", "t", &errors); + expect("ab == a + b", "t", &errors); + expect("a + (b == ab)", "a", &errors); + expect("(ab == a) + b", "b", &errors); - // substring function - expect("is_substring(cad, abracadabra)", "t", &errors); - expect("is_substring(abrac, abracadabra)", "t", &errors); - expect("is_substring(dabra, abracadabra)", "t", &errors); - expect("is_substring(cad, abracxadabra)", "", &errors); - expect("is_substring(abrac, axbracadabra)", "", &errors); - expect("is_substring(dabra, abracadabrxa)", "", &errors); + // substring function + expect("is_substring(cad, abracadabra)", "t", &errors); + expect("is_substring(abrac, abracadabra)", "t", &errors); + expect("is_substring(dabra, abracadabra)", "t", &errors); + expect("is_substring(cad, abracxadabra)", "", &errors); + expect("is_substring(abrac, axbracadabra)", "", &errors); + expect("is_substring(dabra, abracadabrxa)", "", &errors); - // ifelse function - expect("ifelse(t, yes, no)", "yes", &errors); - expect("ifelse(!t, yes, no)", "no", &errors); - expect("ifelse(t, yes, abort())", "yes", &errors); - expect("ifelse(!t, abort(), no)", "no", &errors); + // ifelse function + expect("ifelse(t, yes, no)", "yes", &errors); + expect("ifelse(!t, yes, no)", "no", &errors); + expect("ifelse(t, yes, abort())", "yes", &errors); + expect("ifelse(!t, abort(), no)", "no", &errors); - // if "statements" - expect("if t then yes else no endif", "yes", &errors); - expect("if \"\" then yes else no endif", "no", &errors); - expect("if \"\" then yes endif", "", &errors); - expect("if \"\"; t then yes endif", "yes", &errors); + // if "statements" + expect("if t then yes else no endif", "yes", &errors); + expect("if \"\" then yes else no endif", "no", &errors); + expect("if \"\" then yes endif", "", &errors); + expect("if \"\"; t then yes endif", "yes", &errors); - printf("\n"); + printf("\n"); - return errors; + return errors; +} + +void ExprDump(int depth, Expr* n, char* script) { + printf("%*s", depth*2, ""); + char temp = script[n->end]; + script[n->end] = '\0'; + printf("%s %p (%d-%d) \"%s\"\n", + n->name == NULL ? "(NULL)" : n->name, n->fn, n->start, n->end, + script+n->start); + script[n->end] = temp; + int i; + for (i = 0; i < n->argc; ++i) { + ExprDump(depth+1, n->argv[i], script); + } } int main(int argc, char** argv) { - RegisterBuiltins(); - FinishRegistration(); + RegisterBuiltins(); + FinishRegistration(); - if (argc == 1) { - return test() != 0; - } - - FILE* f = fopen(argv[1], "r"); - char buffer[8192]; - int size = fread(buffer, 1, 8191, f); - fclose(f); - buffer[size] = '\0'; - - Expr* root; - int error_count = 0; - yy_scan_bytes(buffer, size); - int error = yyparse(&root, &error_count); - printf("parse returned %d; %d errors encountered\n", error, error_count); - if (error == 0 || error_count > 0) { - char* result = Evaluate(NULL, root); - if (result == NULL) { - char* errmsg = GetError(); - printf("result was NULL, message is: %s\n", - (errmsg == NULL ? "(NULL)" : errmsg)); - ClearError(); - } else { - printf("result is [%s]\n", result); + if (argc == 1) { + return test() != 0; } - } - return 0; + + FILE* f = fopen(argv[1], "r"); + char buffer[8192]; + int size = fread(buffer, 1, 8191, f); + fclose(f); + buffer[size] = '\0'; + + Expr* root; + int error_count = 0; + yy_scan_bytes(buffer, size); + int error = yyparse(&root, &error_count); + printf("parse returned %d; %d errors encountered\n", error, error_count); + if (error == 0 || error_count > 0) { + + ExprDump(0, root, buffer); + + State state; + state.cookie = NULL; + state.script = buffer; + state.errmsg = NULL; + + char* result = Evaluate(&state, root); + if (result == NULL) { + printf("result was NULL, message is: %s\n", + (state.errmsg == NULL ? "(NULL)" : state.errmsg)); + free(state.errmsg); + } else { + printf("result is [%s]\n", result); + } + } + return 0; } diff --git a/edify/parser.y b/edify/parser.y index cf163c0..3f9ade1 100644 --- a/edify/parser.y +++ b/edify/parser.y @@ -20,6 +20,7 @@ #include #include "expr.h" +#include "yydefs.h" #include "parser.h" extern int gLine; @@ -30,6 +31,8 @@ int yyparse(Expr** root, int* error_count); %} +%locations + %union { char* str; Expr* expr; @@ -68,19 +71,21 @@ expr: STRING { $$->name = $1; $$->argc = 0; $$->argv = NULL; + $$->start = @$.start; + $$->end = @$.end; } -| '(' expr ')' { $$ = $2; } -| expr ';' { $$ = $1; } -| expr ';' expr { $$ = Build(SequenceFn, 2, $1, $3); } -| error ';' expr { $$ = $3; } -| expr '+' expr { $$ = Build(ConcatFn, 2, $1, $3); } -| expr EQ expr { $$ = Build(EqualityFn, 2, $1, $3); } -| expr NE expr { $$ = Build(InequalityFn, 2, $1, $3); } -| expr AND expr { $$ = Build(LogicalAndFn, 2, $1, $3); } -| expr OR expr { $$ = Build(LogicalOrFn, 2, $1, $3); } -| '!' expr { $$ = Build(LogicalNotFn, 1, $2); } -| IF expr THEN expr ENDIF { $$ = Build(IfElseFn, 2, $2, $4); } -| IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, 3, $2, $4, $6); } +| '(' expr ')' { $$ = $2; $$->start=@$.start; $$->end=@$.end; } +| expr ';' { $$ = $1; $$->start=@1.start; $$->end=@1.end; } +| expr ';' expr { $$ = Build(SequenceFn, @$, 2, $1, $3); } +| error ';' expr { $$ = $3; $$->start=@$.start; $$->end=@$.end; } +| expr '+' expr { $$ = Build(ConcatFn, @$, 2, $1, $3); } +| expr EQ expr { $$ = Build(EqualityFn, @$, 2, $1, $3); } +| expr NE expr { $$ = Build(InequalityFn, @$, 2, $1, $3); } +| expr AND expr { $$ = Build(LogicalAndFn, @$, 2, $1, $3); } +| expr OR expr { $$ = Build(LogicalOrFn, @$, 2, $1, $3); } +| '!' expr { $$ = Build(LogicalNotFn, @$, 1, $2); } +| IF expr THEN expr ENDIF { $$ = Build(IfElseFn, @$, 2, $2, $4); } +| IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, @$, 3, $2, $4, $6); } | STRING '(' arglist ')' { $$ = malloc(sizeof(Expr)); $$->fn = FindFunction($1); @@ -93,6 +98,8 @@ expr: STRING { $$->name = $1; $$->argc = $3.argc; $$->argv = $3.argv; + $$->start = @$.start; + $$->end = @$.end; } ; diff --git a/edify/yydefs.h b/edify/yydefs.h new file mode 100644 index 0000000..6257862 --- /dev/null +++ b/edify/yydefs.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef _YYDEFS_H_ +#define _YYDEFS_H_ + +#define YYLTYPE YYLTYPE +typedef struct { + int start, end; +} YYLTYPE; + +#define YYLLOC_DEFAULT(Current, Rhs, N) \ + do { \ + if (N) { \ + (Current).start = YYRHSLOC(Rhs, 1).start; \ + (Current).end = YYRHSLOC(Rhs, N).end; \ + } else { \ + (Current).start = YYRHSLOC(Rhs, 0).start; \ + (Current).end = YYRHSLOC(Rhs, 0).end; \ + } \ + } while (0) + +#endif diff --git a/install.c b/install.c index cca9400..c2e1385 100644 --- a/install.c +++ b/install.c @@ -196,6 +196,9 @@ try_update_binary(const char *path, ZipArchive *zip) { // arrange to install the contents of in the // given partition on reboot. // + // ui_print + // display on the screen. + // // - the name of the package zip file. // @@ -248,6 +251,13 @@ try_update_binary(const char *path, ZipArchive *zip) { firmware_filename = strdup(filename); } } + } else if (strcmp(command, "ui_print") == 0) { + char* str = strtok(NULL, "\n"); + if (str) { + ui_print(str); + } else { + ui_print("\n"); + } } else { LOGE("unknown command [%s]\n", command); } diff --git a/updater/install.c b/updater/install.c index 2e965ce..616cb2c 100644 --- a/updater/install.c +++ b/updater/install.c @@ -32,13 +32,14 @@ #include "mtdutils/mtdutils.h" #include "updater.h" -char* ErrorAbort(void* cookie, char* format, ...) { +char* ErrorAbort(State* state, char* format, ...) { char* buffer = malloc(4096); va_list v; va_start(v, format); vsnprintf(buffer, 4096, format, v); va_end(v); - SetError(buffer); + free(state->errmsg); + state->errmsg = buffer; return NULL; } @@ -47,28 +48,28 @@ char* ErrorAbort(void* cookie, char* format, ...) { // // what: type="MTD" location="" to mount a yaffs2 filesystem // type="vfat" location="/dev/block/" to mount a device -char* MountFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* MountFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; if (argc != 3) { - return ErrorAbort(cookie, "%s() expects 3 args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 3 args, got %d", name, argc); } char* type; char* location; char* mount_point; - if (ReadArgs(cookie, argv, 3, &type, &location, &mount_point) < 0) { + if (ReadArgs(state, argv, 3, &type, &location, &mount_point) < 0) { return NULL; } if (strlen(type) == 0) { - ErrorAbort(cookie, "type argument to %s() can't be empty", name); + ErrorAbort(state, "type argument to %s() can't be empty", name); goto done; } if (strlen(location) == 0) { - ErrorAbort(cookie, "location argument to %s() can't be empty", name); + ErrorAbort(state, "location argument to %s() can't be empty", name); goto done; } if (strlen(mount_point) == 0) { - ErrorAbort(cookie, "mount_point argument to %s() can't be empty", name); + ErrorAbort(state, "mount_point argument to %s() can't be empty", name); goto done; } @@ -109,17 +110,17 @@ done: // is_mounted(mount_point) -char* IsMountedFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* IsMountedFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; if (argc != 1) { - return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc); } char* mount_point; - if (ReadArgs(cookie, argv, 1, &mount_point) < 0) { + if (ReadArgs(state, argv, 1, &mount_point) < 0) { return NULL; } if (strlen(mount_point) == 0) { - ErrorAbort(cookie, "mount_point argument to unmount() can't be empty"); + ErrorAbort(state, "mount_point argument to unmount() can't be empty"); goto done; } @@ -137,17 +138,17 @@ done: } -char* UnmountFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* UnmountFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; if (argc != 1) { - return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc); } char* mount_point; - if (ReadArgs(cookie, argv, 1, &mount_point) < 0) { + if (ReadArgs(state, argv, 1, &mount_point) < 0) { return NULL; } if (strlen(mount_point) == 0) { - ErrorAbort(cookie, "mount_point argument to unmount() can't be empty"); + ErrorAbort(state, "mount_point argument to unmount() can't be empty"); goto done; } @@ -170,23 +171,23 @@ done: // format(type, location) // // type="MTD" location=partition -char* FormatFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* FormatFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; if (argc != 2) { - return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc); } char* type; char* location; - if (ReadArgs(cookie, argv, 2, &type, &location) < 0) { + if (ReadArgs(state, argv, 2, &type, &location) < 0) { return NULL; } if (strlen(type) == 0) { - ErrorAbort(cookie, "type argument to %s() can't be empty", name); + ErrorAbort(state, "type argument to %s() can't be empty", name); goto done; } if (strlen(location) == 0) { - ErrorAbort(cookie, "location argument to %s() can't be empty", name); + ErrorAbort(state, "location argument to %s() can't be empty", name); goto done; } @@ -228,11 +229,11 @@ done: } -char* DeleteFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* DeleteFn(const char* name, State* state, int argc, Expr* argv[]) { char** paths = malloc(argc * sizeof(char*)); int i; for (i = 0; i < argc; ++i) { - paths[i] = Evaluate(cookie, argv[i]); + paths[i] = Evaluate(state, argv[i]); if (paths[i] == NULL) { int j; for (j = 0; j < i; ++i) { @@ -259,20 +260,20 @@ char* DeleteFn(const char* name, void* cookie, int argc, Expr* argv[]) { } -char* ShowProgressFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* ShowProgressFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc != 2) { - return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc); } char* frac_str; char* sec_str; - if (ReadArgs(cookie, argv, 2, &frac_str, &sec_str) < 0) { + if (ReadArgs(state, argv, 2, &frac_str, &sec_str) < 0) { return NULL; } double frac = strtod(frac_str, NULL); int sec = strtol(sec_str, NULL, 10); - UpdaterInfo* ui = (UpdaterInfo*)cookie; + UpdaterInfo* ui = (UpdaterInfo*)(state->cookie); fprintf(ui->cmd_pipe, "progress %f %d\n", frac, sec); free(frac_str); @@ -281,16 +282,16 @@ char* ShowProgressFn(const char* name, void* cookie, int argc, Expr* argv[]) { } // package_extract_dir(package_path, destination_path) -char* PackageExtractDirFn(const char* name, void* cookie, +char* PackageExtractDirFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc != 2) { - return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc); } char* zip_path; char* dest_path; - if (ReadArgs(cookie, argv, 2, &zip_path, &dest_path) < 0) return NULL; + if (ReadArgs(state, argv, 2, &zip_path, &dest_path) < 0) return NULL; - ZipArchive* za = ((UpdaterInfo*)cookie)->package_zip; + ZipArchive* za = ((UpdaterInfo*)(state->cookie))->package_zip; // To create a consistent system image, never use the clock for timestamps. struct utimbuf timestamp = { 1217592000, 1217592000 }; // 8/1/2008 default @@ -305,18 +306,18 @@ char* PackageExtractDirFn(const char* name, void* cookie, // package_extract_file(package_path, destination_path) -char* PackageExtractFileFn(const char* name, void* cookie, +char* PackageExtractFileFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc != 2) { - return ErrorAbort(cookie, "%s() expects 2 args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc); } char* zip_path; char* dest_path; - if (ReadArgs(cookie, argv, 2, &zip_path, &dest_path) < 0) return NULL; + if (ReadArgs(state, argv, 2, &zip_path, &dest_path) < 0) return NULL; bool success = false; - ZipArchive* za = ((UpdaterInfo*)cookie)->package_zip; + ZipArchive* za = ((UpdaterInfo*)(state->cookie))->package_zip; const ZipEntry* entry = mzFindZipEntry(za, zip_path); if (entry == NULL) { fprintf(stderr, "%s: no %s in package\n", name, zip_path); @@ -340,15 +341,15 @@ char* PackageExtractFileFn(const char* name, void* cookie, // symlink target src1 src2 ... -char* SymlinkFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* SymlinkFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc == 0) { - return ErrorAbort(cookie, "%s() expects 1+ args, got %d", name, argc); + return ErrorAbort(state, "%s() expects 1+ args, got %d", name, argc); } char* target; - target = Evaluate(cookie, argv[0]); + target = Evaluate(state, argv[0]); if (target == NULL) return NULL; - char** srcs = ReadVarArgs(cookie, argc-1, argv+1); + char** srcs = ReadVarArgs(state, argc-1, argv+1); if (srcs == NULL) { free(target); return NULL; @@ -364,16 +365,16 @@ char* SymlinkFn(const char* name, void* cookie, int argc, Expr* argv[]) { } -char* SetPermFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* SetPermFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; bool recursive = (strcmp(name, "set_perm_recursive") == 0); int min_args = 4 + (recursive ? 1 : 0); if (argc < min_args) { - return ErrorAbort(cookie, "%s() expects %d+ args, got %d", name, argc); + return ErrorAbort(state, "%s() expects %d+ args, got %d", name, argc); } - char** args = ReadVarArgs(cookie, argc, argv); + char** args = ReadVarArgs(state, argc, argv); if (args == NULL) return NULL; char* end; @@ -381,26 +382,26 @@ char* SetPermFn(const char* name, void* cookie, int argc, Expr* argv[]) { int uid = strtoul(args[0], &end, 0); if (*end != '\0' || args[0][0] == 0) { - ErrorAbort(cookie, "%s: \"%s\" not a valid uid", name, args[0]); + ErrorAbort(state, "%s: \"%s\" not a valid uid", name, args[0]); goto done; } int gid = strtoul(args[1], &end, 0); if (*end != '\0' || args[1][0] == 0) { - ErrorAbort(cookie, "%s: \"%s\" not a valid gid", name, args[1]); + ErrorAbort(state, "%s: \"%s\" not a valid gid", name, args[1]); goto done; } if (recursive) { int dir_mode = strtoul(args[2], &end, 0); if (*end != '\0' || args[2][0] == 0) { - ErrorAbort(cookie, "%s: \"%s\" not a valid dirmode", name, args[2]); + ErrorAbort(state, "%s: \"%s\" not a valid dirmode", name, args[2]); goto done; } int file_mode = strtoul(args[3], &end, 0); if (*end != '\0' || args[3][0] == 0) { - ErrorAbort(cookie, "%s: \"%s\" not a valid filemode", + ErrorAbort(state, "%s: \"%s\" not a valid filemode", name, args[3]); goto done; } @@ -411,7 +412,7 @@ char* SetPermFn(const char* name, void* cookie, int argc, Expr* argv[]) { } else { int mode = strtoul(args[2], &end, 0); if (*end != '\0' || args[2][0] == 0) { - ErrorAbort(cookie, "%s: \"%s\" not a valid mode", name, args[2]); + ErrorAbort(state, "%s: \"%s\" not a valid mode", name, args[2]); goto done; } @@ -432,12 +433,12 @@ done: } -char* GetPropFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* GetPropFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc != 1) { - return ErrorAbort(cookie, "%s() expects 1 arg, got %d", name, argc); + return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc); } char* key; - key = Evaluate(cookie, argv[0]); + key = Evaluate(state, argv[0]); if (key == NULL) return NULL; char value[PROPERTY_VALUE_MAX]; @@ -457,21 +458,21 @@ static bool write_raw_image_cb(const unsigned char* data, } // write_raw_image(file, partition) -char* WriteRawImageFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* WriteRawImageFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; char* partition; char* filename; - if (ReadArgs(cookie, argv, 2, &filename, &partition) < 0) { + if (ReadArgs(state, argv, 2, &filename, &partition) < 0) { return NULL; } if (strlen(partition) == 0) { - ErrorAbort(cookie, "partition argument to %s can't be empty", name); + ErrorAbort(state, "partition argument to %s can't be empty", name); goto done; } if (strlen(filename) == 0) { - ErrorAbort(cookie, "file argument to %s can't be empty", name); + ErrorAbort(state, "file argument to %s can't be empty", name); goto done; } @@ -515,6 +516,13 @@ char* WriteRawImageFn(const char* name, void* cookie, int argc, Expr* argv[]) { free(buffer); fclose(f); + if (mtd_erase_blocks(ctx, -1) == -1) { + fprintf(stderr, "%s: error erasing blocks of %s\n", name, partition); + } + if (mtd_write_close(ctx) != 0) { + fprintf(stderr, "%s: error closing write of %s\n", name, partition); + } + printf("%s %s partition from %s\n", success ? "wrote" : "failed to write", partition, filename); @@ -532,26 +540,26 @@ done: // file is not used until after updater exits // // TODO: this should live in some HTC-specific library -char* WriteFirmwareImageFn(const char* name, void* cookie, +char* WriteFirmwareImageFn(const char* name, State* state, int argc, Expr* argv[]) { char* result = NULL; char* partition; char* filename; - if (ReadArgs(cookie, argv, 2, &filename, &partition) < 0) { + if (ReadArgs(state, argv, 2, &filename, &partition) < 0) { return NULL; } if (strlen(partition) == 0) { - ErrorAbort(cookie, "partition argument to %s can't be empty", name); + ErrorAbort(state, "partition argument to %s can't be empty", name); goto done; } if (strlen(filename) == 0) { - ErrorAbort(cookie, "file argument to %s can't be empty", name); + ErrorAbort(state, "file argument to %s can't be empty", name); goto done; } - FILE* cmd = ((UpdaterInfo*)cookie)->cmd_pipe; + FILE* cmd = ((UpdaterInfo*)(state->cookie))->cmd_pipe; fprintf(cmd, "firmware %s %s\n", partition, filename); printf("will write %s firmware from %s\n", partition, filename); @@ -569,7 +577,7 @@ extern int applypatch(int argc, char** argv); // apply_patch(srcfile, tgtfile, tgtsha1, tgtsize, sha1:patch, ...) // apply_patch_check(file, sha1, ...) // apply_patch_space(bytes) -char* ApplyPatchFn(const char* name, void* cookie, int argc, Expr* argv[]) { +char* ApplyPatchFn(const char* name, State* state, int argc, Expr* argv[]) { printf("in applypatchfn (%s)\n", name); char* prepend = NULL; @@ -579,7 +587,7 @@ char* ApplyPatchFn(const char* name, void* cookie, int argc, Expr* argv[]) { prepend = "-s"; } - char** args = ReadVarArgs(cookie, argc, argv); + char** args = ReadVarArgs(state, argc, argv); if (args == NULL) return NULL; // insert the "program name" argv[0] and a copy of the "prepend" @@ -610,10 +618,42 @@ char* ApplyPatchFn(const char* name, void* cookie, int argc, Expr* argv[]) { switch (result) { case 0: return strdup("t"); case 1: return strdup(""); - default: return ErrorAbort(cookie, "applypatch couldn't parse args"); + default: return ErrorAbort(state, "applypatch couldn't parse args"); } } +char* UIPrintFn(const char* name, State* state, int argc, Expr* argv[]) { + char** args = ReadVarArgs(state, argc, argv); + if (args == NULL) { + return NULL; + } + + int size = 0; + int i; + for (i = 0; i < argc; ++i) { + size += strlen(args[i]); + } + char* buffer = malloc(size+1); + size = 0; + for (i = 0; i < argc; ++i) { + strcpy(buffer+size, args[i]); + size += strlen(args[i]); + free(args[i]); + } + free(args); + buffer[size] = '\0'; + + char* line = strtok(buffer, "\n"); + while (line) { + fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe, + "ui_print %s\n", line); + line = strtok(NULL, "\n"); + } + fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe, "ui_print\n"); + + return buffer; +} + void RegisterInstallFunctions() { RegisterFunction("mount", MountFn); @@ -636,4 +676,6 @@ void RegisterInstallFunctions() { RegisterFunction("apply_patch", ApplyPatchFn); RegisterFunction("apply_patch_check", ApplyPatchFn); RegisterFunction("apply_patch_space", ApplyPatchFn); + + RegisterFunction("ui_print", UIPrintFn); } diff --git a/updater/updater.c b/updater/updater.c index 0977625..5a2ed2c 100644 --- a/updater/updater.c +++ b/updater/updater.c @@ -94,12 +94,26 @@ int main(int argc, char** argv) { updater_info.cmd_pipe = cmd_pipe; updater_info.package_zip = &za; - char* result = Evaluate(&updater_info, root); + State state; + state.cookie = &updater_info; + state.script = script; + state.errmsg = NULL; + + char* result = Evaluate(&state, root); if (result == NULL) { - const char* errmsg = GetError(); - fprintf(stderr, "script aborted with error: %s\n", - errmsg == NULL ? "(none)" : errmsg); - ClearError(); + if (state.errmsg == NULL) { + fprintf(stderr, "script aborted (no error message)\n"); + fprintf(cmd_pipe, "ui_print script aborted (no error message)\n"); + } else { + fprintf(stderr, "script aborted: %s\n", state.errmsg); + char* line = strtok(state.errmsg, "\n"); + while (line) { + fprintf(cmd_pipe, "ui_print %s\n", line); + line = strtok(NULL, "\n"); + } + fprintf(cmd_pipe, "ui_print\n"); + } + free(state.errmsg); return 7; } else { fprintf(stderr, "script result was [%s]\n", result); @@ -107,6 +121,7 @@ int main(int argc, char** argv) { } mzCloseZipArchive(&za); + free(script); return 0; } From e3da02e7bcfd85c543419e7590a3c86f64d8cc8a Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Fri, 12 Jun 2009 16:13:52 -0700 Subject: [PATCH 18/24] add less_than_int, greater_than_int to edify Add functions less_than_int() and greater_than_int() that interpret their args as ints and do the comparison. ("<" and ">" operators, if implemented, should do string comparison.) This lets us do the build time check currently done by the check_prereq binary. --- edify/expr.c | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ edify/main.c | 10 ++++++++++ 2 files changed, 63 insertions(+) diff --git a/edify/expr.c b/edify/expr.c index 406c67e..f1c5555 100644 --- a/edify/expr.c +++ b/edify/expr.c @@ -72,6 +72,8 @@ char* ConcatFn(const char* name, State* state, int argc, Expr* argv[]) { char* IfElseFn(const char* name, State* state, int argc, Expr* argv[]) { if (argc != 2 && argc != 3) { + free(state->errmsg); + state->errmsg = strdup("ifelse expects 2 or 3 arguments"); return NULL; } char* cond = Evaluate(state, argv[0]); @@ -244,6 +246,54 @@ char* SequenceFn(const char* name, State* state, int argc, Expr* argv[]) { return Evaluate(state, argv[1]); } +char* LessThanIntFn(const char* name, State* state, int argc, Expr* argv[]) { + if (argc != 2) { + free(state->errmsg); + state->errmsg = strdup("less_than_int expects 2 arguments"); + return NULL; + } + + char* left; + char* right; + if (ReadArgs(state, argv, 2, &left, &right) < 0) return NULL; + + bool result = false; + char* end; + + long l_int = strtol(left, &end, 10); + if (left[0] == '\0' || *end != '\0') { + fprintf(stderr, "[%s] is not an int\n", left); + goto done; + } + + long r_int = strtol(right, &end, 10); + if (right[0] == '\0' || *end != '\0') { + fprintf(stderr, "[%s] is not an int\n", right); + goto done; + } + + result = l_int < r_int; + + done: + free(left); + free(right); + return strdup(result ? "t" : ""); +} + +char* GreaterThanIntFn(const char* name, State* state, int argc, Expr* argv[]) { + if (argc != 2) { + free(state->errmsg); + state->errmsg = strdup("greater_than_int expects 2 arguments"); + return NULL; + } + + Expr* temp[2]; + temp[0] = argv[1]; + temp[1] = argv[0]; + + return LessThanIntFn(name, state, 2, temp); +} + char* Literal(const char* name, State* state, int argc, Expr* argv[]) { return strdup(name); } @@ -313,6 +363,9 @@ void RegisterBuiltins() { RegisterFunction("is_substring", SubstringFn); RegisterFunction("stdout", StdoutFn); RegisterFunction("sleep", SleepFn); + + RegisterFunction("less_than_int", LessThanIntFn); + RegisterFunction("greater_than_int", GreaterThanIntFn); } diff --git a/edify/main.c b/edify/main.c index 03eefc6..0e36108 100644 --- a/edify/main.c +++ b/edify/main.c @@ -143,6 +143,16 @@ int test() { expect("if \"\" then yes endif", "", &errors); expect("if \"\"; t then yes endif", "yes", &errors); + // numeric comparisons + expect("less_than_int(3, 14)", "t", &errors); + expect("less_than_int(14, 3)", "", &errors); + expect("less_than_int(x, 3)", "", &errors); + expect("less_than_int(3, x)", "", &errors); + expect("greater_than_int(3, 14)", "", &errors); + expect("greater_than_int(14, 3)", "t", &errors); + expect("greater_than_int(x, 3)", "", &errors); + expect("greater_than_int(3, x)", "", &errors); + printf("\n"); return errors; From fb2e3af3f915c0e3f2b4b027ef26777267ad46dc Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Wed, 17 Jun 2009 17:29:40 -0700 Subject: [PATCH 19/24] let the "firmware" command take the file straight from the package To do a firmware-install-on-reboot, the update binary tells recovery what file to install before rebooting. Let this file be specified as "PACKAGE:" to indicate taking the file out of the OTA package, avoiding an extra copy to /tmp. Bump the API version number to reflect this change. --- Android.mk | 3 +++ common.h | 3 +++ install.c | 66 +++++++++++++++++++++++++++++++---------------- recovery.c | 3 ++- updater/updater.c | 7 ++--- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/Android.mk b/Android.mk index 6ab498e..0367fef 100644 --- a/Android.mk +++ b/Android.mk @@ -22,6 +22,9 @@ LOCAL_MODULE := recovery LOCAL_FORCE_STATIC_EXECUTABLE := true +RECOVERY_API_VERSION := 2 +LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION) + # This binary is in the recovery ramdisk, which is otherwise a copy of root. # It gets copied there in config/Makefile. LOCAL_MODULE_TAGS suppresses # a (redundant) copy of the binary in /system/bin for user builds. diff --git a/common.h b/common.h index 98ce868..6761159 100644 --- a/common.h +++ b/common.h @@ -90,4 +90,7 @@ void ui_reset_progress(); #define LOGD(...) do {} while (0) #endif +#define STRINGIFY(x) #x +#define EXPAND(x) STRINGIFY(x) + #endif // RECOVERY_COMMON_H diff --git a/install.c b/install.c index c2e1385..cbb3580 100644 --- a/install.c +++ b/install.c @@ -117,34 +117,54 @@ handle_update_script(ZipArchive *zip, const ZipEntry *update_script_entry) // The update binary ask us to install a firmware file on reboot. Set // that up. Takes ownership of type and filename. static int -handle_firmware_update(char* type, char* filename) { - struct stat st_data; - if (stat(filename, &st_data) < 0) { - LOGE("Error stat'ing %s: %s\n", filename, strerror(errno)); - return INSTALL_ERROR; +handle_firmware_update(char* type, char* filename, ZipArchive* zip) { + unsigned int data_size; + const ZipEntry* entry = NULL; + + if (strncmp(filename, "PACKAGE:", 8) == 0) { + entry = mzFindZipEntry(zip, filename+8); + if (entry == NULL) { + LOGE("Failed to find \"%s\" in package", filename+8); + return INSTALL_ERROR; + } + data_size = entry->uncompLen; + } else { + struct stat st_data; + if (stat(filename, &st_data) < 0) { + LOGE("Error stat'ing %s: %s\n", filename, strerror(errno)); + return INSTALL_ERROR; + } + data_size = st_data.st_size; } LOGI("type is %s; size is %d; file is %s\n", - type, (int)st_data.st_size, filename); + type, data_size, filename); - char* data = malloc(st_data.st_size); + char* data = malloc(data_size); if (data == NULL) { - LOGE("Can't allocate %d bytes for firmware data\n", st_data.st_size); + LOGI("Can't allocate %d bytes for firmware data\n", data_size); return INSTALL_ERROR; } - FILE* f = fopen(filename, "rb"); - if (f == NULL) { - LOGE("Failed to open %s: %s\n", filename, strerror(errno)); - return INSTALL_ERROR; + if (entry) { + if (mzReadZipEntry(zip, entry, data, data_size) == false) { + LOGE("Failed to read \"%s\" from package", filename+8); + return INSTALL_ERROR; + } + } else { + FILE* f = fopen(filename, "rb"); + if (f == NULL) { + LOGE("Failed to open %s: %s\n", filename, strerror(errno)); + return INSTALL_ERROR; + } + if (fread(data, 1, data_size, f) != data_size) { + LOGE("Failed to read firmware data: %s\n", strerror(errno)); + return INSTALL_ERROR; + } + fclose(f); } - if (fread(data, 1, st_data.st_size, f) != st_data.st_size) { - LOGE("Failed to read firmware data: %s\n", strerror(errno)); - return INSTALL_ERROR; - } - fclose(f); - if (remember_firmware_update(type, data, st_data.st_size)) { + if (remember_firmware_update(type, data, data_size)) { LOGE("Can't store %s image\n", type); free(data); return INSTALL_ERROR; @@ -184,7 +204,7 @@ try_update_binary(const char *path, ZipArchive *zip) { // When executing the update binary contained in the package, the // arguments passed are: // - // - the version number for this interface (currently 1) + // - the version number for this interface // // - an fd to which the program can write in order to update the // progress bar. The program can write single-line commands: @@ -194,7 +214,9 @@ try_update_binary(const char *path, ZipArchive *zip) { // // firmware <"hboot"|"radio"> // arrange to install the contents of in the - // given partition on reboot. + // given partition on reboot. (API v2: may + // start with "PACKAGE:" to indicate taking a file from + // the OTA package.) // // ui_print // display on the screen. @@ -204,7 +226,7 @@ try_update_binary(const char *path, ZipArchive *zip) { char** args = malloc(sizeof(char*) * 5); args[0] = binary; - args[1] = "1"; + args[1] = EXPAND(RECOVERY_API_VERSION); // defined in Android.mk args[2] = malloc(10); sprintf(args[2], "%d", pipefd[1]); args[3] = (char*)path; @@ -272,7 +294,7 @@ try_update_binary(const char *path, ZipArchive *zip) { } if (firmware_type != NULL) { - return handle_firmware_update(firmware_type, firmware_filename); + return handle_firmware_update(firmware_type, firmware_filename, zip); } else { return INSTALL_SUCCESS; } diff --git a/recovery.c b/recovery.c index 188d4de..5ccd38f 100644 --- a/recovery.c +++ b/recovery.c @@ -287,7 +287,8 @@ erase_root(const char *root) static void prompt_and_wait() { - char* headers[] = { "Android system recovery utility", + char* headers[] = { "Android system recovery <" + EXPAND(RECOVERY_API_VERSION) ">", "", "Use trackball to highlight;", "click to select.", diff --git a/updater/updater.c b/updater/updater.c index 5a2ed2c..31d93ae 100644 --- a/updater/updater.c +++ b/updater/updater.c @@ -34,9 +34,10 @@ int main(int argc, char** argv) { } char* version = argv[1]; - if (version[0] != '1' || version[1] != '\0') { - fprintf(stderr, "wrong updater binary API; expected 1, got %s\n", - version); + if ((version[0] != '1' && version[0] != '2') || version[1] != '\0') { + // We support version "1" or "2". + fprintf(stderr, "wrong updater binary API; expected 1 or 2, got %s\n", + argv[1]); return 2; } From 47cace98369f60df2351a65801c8065bb7f9dbf0 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 18 Jun 2009 10:11:50 -0700 Subject: [PATCH 20/24] add file_getprop() to updater Add a function to read a property from a ".prop"-formatted file (key=value pairs, one per line, ignore # comment lines and blank lines). Move ErrorAbort to the core of edify; it's not specific to updater now that errors aren't stored in the app cookie. --- edify/expr.c | 13 ++++++ edify/expr.h | 4 ++ updater/install.c | 111 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 117 insertions(+), 11 deletions(-) diff --git a/edify/expr.c b/edify/expr.c index f1c5555..72e5100 100644 --- a/edify/expr.c +++ b/edify/expr.c @@ -417,3 +417,16 @@ char** ReadVarArgs(State* state, int argc, Expr* argv[]) { } return args; } + +// Use printf-style arguments to compose an error message to put into +// *state. Returns NULL. +char* ErrorAbort(State* state, char* format, ...) { + char* buffer = malloc(4096); + va_list v; + va_start(v, format); + vsnprintf(buffer, 4096, format, v); + va_end(v); + free(state->errmsg); + state->errmsg = buffer; + return NULL; +} diff --git a/edify/expr.h b/edify/expr.h index 671b499..d2e7392 100644 --- a/edify/expr.h +++ b/edify/expr.h @@ -118,5 +118,9 @@ int ReadArgs(State* state, Expr* argv[], int count, ...); // strings it contains. char** ReadVarArgs(State* state, int argc, Expr* argv[]); +// Use printf-style arguments to compose an error message to put into +// *state. Returns NULL. +char* ErrorAbort(State* state, char* format, ...); + #endif // _EXPRESSION_H diff --git a/updater/install.c b/updater/install.c index 616cb2c..0bd0939 100644 --- a/updater/install.c +++ b/updater/install.c @@ -32,17 +32,6 @@ #include "mtdutils/mtdutils.h" #include "updater.h" -char* ErrorAbort(State* state, char* format, ...) { - char* buffer = malloc(4096); - va_list v; - va_start(v, format); - vsnprintf(buffer, 4096, format, v); - va_end(v); - free(state->errmsg); - state->errmsg = buffer; - return NULL; -} - // mount(type, location, mount_point) // @@ -449,6 +438,105 @@ char* GetPropFn(const char* name, State* state, int argc, Expr* argv[]) { } +// file_getprop(file, key) +// +// interprets 'file' as a getprop-style file (key=value pairs, one +// per line, # comment lines and blank lines okay), and returns the value +// for 'key' (or "" if it isn't defined). +char* FileGetPropFn(const char* name, State* state, int argc, Expr* argv[]) { + char* result = NULL; + char* buffer = NULL; + char* filename; + char* key; + if (ReadArgs(state, argv, 2, &filename, &key) < 0) { + return NULL; + } + + struct stat st; + if (stat(filename, &st) < 0) { + ErrorAbort(state, "%s: failed to stat \"%s\": %s", + name, filename, strerror(errno)); + goto done; + } + +#define MAX_FILE_GETPROP_SIZE 65536 + + if (st.st_size > MAX_FILE_GETPROP_SIZE) { + ErrorAbort(state, "%s too large for %s (max %d)", + filename, name, MAX_FILE_GETPROP_SIZE); + goto done; + } + + buffer = malloc(st.st_size+1); + if (buffer == NULL) { + ErrorAbort(state, "%s: failed to alloc %d bytes", name, st.st_size+1); + goto done; + } + + FILE* f = fopen(filename, "rb"); + if (f == NULL) { + ErrorAbort(state, "%s: failed to open %s: %s", + name, filename, strerror(errno)); + goto done; + } + + if (fread(buffer, 1, st.st_size, f) != st.st_size) { + ErrorAbort(state, "%s: failed to read %d bytes from %s", + name, st.st_size+1, filename); + fclose(f); + goto done; + } + buffer[st.st_size] = '\0'; + + fclose(f); + + char* line = strtok(buffer, "\n"); + do { + // skip whitespace at start of line + while (*line && isspace(*line)) ++line; + + // comment or blank line: skip to next line + if (*line == '\0' || *line == '#') continue; + + char* equal = strchr(line, '='); + if (equal == NULL) { + ErrorAbort(state, "%s: malformed line \"%s\": %s not a prop file?", + name, line, filename); + goto done; + } + + // trim whitespace between key and '=' + char* key_end = equal-1; + while (key_end > line && isspace(*key_end)) --key_end; + key_end[1] = '\0'; + + // not the key we're looking for + if (strcmp(key, line) != 0) continue; + + // skip whitespace after the '=' to the start of the value + char* val_start = equal+1; + while(*val_start && isspace(*val_start)) ++val_start; + + // trim trailing whitespace + char* val_end = val_start + strlen(val_start)-1; + while (val_end > val_start && isspace(*val_end)) --val_end; + val_end[1] = '\0'; + + result = strdup(val_start); + break; + + } while ((line = strtok(NULL, "\n"))); + + if (result == NULL) result = strdup(""); + + done: + free(filename); + free(key); + free(buffer); + return result; +} + + static bool write_raw_image_cb(const unsigned char* data, int data_len, void* ctx) { int r = mtd_write_data((MtdWriteContext*)ctx, (const char *)data, data_len); @@ -670,6 +758,7 @@ void RegisterInstallFunctions() { RegisterFunction("set_perm_recursive", SetPermFn); RegisterFunction("getprop", GetPropFn); + RegisterFunction("file_getprop", FileGetPropFn); RegisterFunction("write_raw_image", WriteRawImageFn); RegisterFunction("write_firmware_image", WriteFirmwareImageFn); From fbf3c10e45c20f8fe6bd1ac49ffe220035b9c454 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Wed, 24 Jun 2009 09:36:20 -0700 Subject: [PATCH 21/24] improve updater progress bar Let recovery accept set_progress commands to control progress over the 'current segment' of the bar. Add a set_progress() builtin to the updater binary. --- install.c | 14 +++++++++++++- updater/install.c | 24 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/install.c b/install.c index cbb3580..ab19478 100644 --- a/install.c +++ b/install.c @@ -210,7 +210,15 @@ try_update_binary(const char *path, ZipArchive *zip) { // progress bar. The program can write single-line commands: // // progress - // fill up of the progress bar over seconds. + // fill up the next part of of the progress bar + // over seconds. If is zero, use + // set_progress commands to manually control the + // progress of this segment of the bar + // + // set_progress + // should be between 0.0 and 1.0; sets the + // progress bar within the segment defined by the most + // recent progress command. // // firmware <"hboot"|"radio"> // arrange to install the contents of in the @@ -261,6 +269,10 @@ try_update_binary(const char *path, ZipArchive *zip) { ui_show_progress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), seconds); + } else if (strcmp(command, "set_progress") == 0) { + char* fraction_s = strtok(NULL, " \n"); + float fraction = strtof(fraction_s, NULL); + ui_set_progress(fraction); } else if (strcmp(command, "firmware") == 0) { char* type = strtok(NULL, " \n"); char* filename = strtok(NULL, " \n"); diff --git a/updater/install.c b/updater/install.c index 0bd0939..e1f3c9a 100644 --- a/updater/install.c +++ b/updater/install.c @@ -14,9 +14,10 @@ * limitations under the License. */ -#include +#include #include #include +#include #include #include #include @@ -265,9 +266,25 @@ char* ShowProgressFn(const char* name, State* state, int argc, Expr* argv[]) { UpdaterInfo* ui = (UpdaterInfo*)(state->cookie); fprintf(ui->cmd_pipe, "progress %f %d\n", frac, sec); - free(frac_str); free(sec_str); - return strdup(""); + return frac_str; +} + +char* SetProgressFn(const char* name, State* state, int argc, Expr* argv[]) { + if (argc != 1) { + return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc); + } + char* frac_str; + if (ReadArgs(state, argv, 1, &frac_str) < 0) { + return NULL; + } + + double frac = strtod(frac_str, NULL); + + UpdaterInfo* ui = (UpdaterInfo*)(state->cookie); + fprintf(ui->cmd_pipe, "set_progress %f\n", frac); + + return frac_str; } // package_extract_dir(package_path, destination_path) @@ -749,6 +766,7 @@ void RegisterInstallFunctions() { RegisterFunction("unmount", UnmountFn); RegisterFunction("format", FormatFn); RegisterFunction("show_progress", ShowProgressFn); + RegisterFunction("set_progress", SetProgressFn); RegisterFunction("delete", DeleteFn); RegisterFunction("delete_recursive", DeleteFn); RegisterFunction("package_extract_dir", PackageExtractDirFn); From 0bbfe3d901885c1f0ab006e8d4cc1029c44a7376 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 25 Jun 2009 13:37:31 -0700 Subject: [PATCH 22/24] fix off-by-one error in set_perm() We were inadvertently skipping over the first filename in the list of arguments. --- updater/install.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updater/install.c b/updater/install.c index e1f3c9a..c4f5e03 100644 --- a/updater/install.c +++ b/updater/install.c @@ -422,7 +422,7 @@ char* SetPermFn(const char* name, State* state, int argc, Expr* argv[]) { goto done; } - for (i = 4; i < argc; ++i) { + for (i = 3; i < argc; ++i) { chown(args[i], uid, gid); chmod(args[i], mode); } From ad3db099d5ee17b1b46fee3131b9561b73b36703 Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Fri, 26 Jun 2009 13:38:55 -0700 Subject: [PATCH 23/24] remove updater from the user system image updater (which is only needed in OTA packages) is getting included in /system/bin, where it just takes up (quite a bit of) space. Use the hack of including it only in eng builds so it's not there for user builds. --- updater/Android.mk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/updater/Android.mk b/updater/Android.mk index 1159d7b..897b9d7 100644 --- a/updater/Android.mk +++ b/updater/Android.mk @@ -7,10 +7,15 @@ updater_src_files := \ updater.c # -# Build the device-side library +# Build a statically-linked binary to include in OTA packages # include $(CLEAR_VARS) +# Build only in eng, so we don't end up with a copy of this in /system +# on user builds. (TODO: find a better way to build device binaries +# needed only for OTA packages.) +LOCAL_MODULE_TAGS := eng + LOCAL_SRC_FILES := $(updater_src_files) LOCAL_STATIC_LIBRARIES := libapplypatch libedify libmtdutils libminzip libz From bec02d57fb85cc7dd0196a54b0e9530e306623ac Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Wed, 1 Jul 2009 12:09:29 -0700 Subject: [PATCH 24/24] skip over all-zero blocks when reading MTD partition We fail to detect certain bad blocks (marked in the factory as bad, I think?) when reading mtd partitions. These come back as a block of all zeros. Since it's fairly unlikely a legitimate boot or recovery block will contain 128k of zeros, change mtdutils to skip over such blocks. Arve says https://review.source.android.com/10535 may be a long-term fix for this, but he isn't yet sure. --- mtdutils/mtdutils.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mtdutils/mtdutils.c b/mtdutils/mtdutils.c index 2b0106f..fc06766 100644 --- a/mtdutils/mtdutils.c +++ b/mtdutils/mtdutils.c @@ -297,7 +297,14 @@ static int read_block(const MtdPartition *partition, int fd, char *data) after.corrected - before.corrected, after.failed - before.failed, pos); } else { - return 0; // Success! + int i; + for (i = 0; i < size; ++i) { + if (data[i] != 0) { + return 0; // Success! + } + } + fprintf(stderr, "mtd: read all-zero block at 0x%08lx; skipping\n", + pos); } pos += partition->erase_size; @@ -326,6 +333,10 @@ ssize_t mtd_read_data(MtdReadContext *ctx, char *data, size_t len) read += ctx->partition->erase_size; } + if (read >= len) { + return read; + } + // Read the next block into the buffer if (ctx->consumed == ctx->partition->erase_size && read < (int) len) { if (read_block(ctx->partition, ctx->fd, ctx->buffer)) return -1;