am 58c60900: Merge "verifier: update to support certificates using SHA-256"

* commit '58c60900ac3682fab08f64373bdd1020713b48f7':
  verifier: update to support certificates using SHA-256
diff --git a/edify/expr.c b/edify/expr.c
index 07a8ceb..a2f1f99 100644
--- a/edify/expr.c
+++ b/edify/expr.c
@@ -287,13 +287,13 @@
 
     long l_int = strtol(left, &end, 10);
     if (left[0] == '\0' || *end != '\0') {
-        fprintf(stderr, "[%s] is not an int\n", left);
+        printf("[%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);
+        printf("[%s] is not an int\n", right);
         goto done;
     }
 
diff --git a/edify/main.c b/edify/main.c
index 8557043..9e6bab7 100644
--- a/edify/main.c
+++ b/edify/main.c
@@ -34,8 +34,8 @@
     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);
+        printf("error parsing \"%s\" (%d errors)\n",
+               expr_str, error_count);
         ++*errors;
         return 0;
     }
@@ -49,7 +49,7 @@
     free(state.errmsg);
     free(state.script);
     if (result == NULL && expected != NULL) {
-        fprintf(stderr, "error evaluating \"%s\"\n", expr_str);
+        printf("error evaluating \"%s\"\n", expr_str);
         ++*errors;
         return 0;
     }
@@ -59,8 +59,8 @@
     }
 
     if (strcmp(result, expected) != 0) {
-        fprintf(stderr, "evaluating \"%s\": expected \"%s\", got \"%s\"\n",
-                expr_str, expected, result);
+        printf("evaluating \"%s\": expected \"%s\", got \"%s\"\n",
+               expr_str, expected, result);
         ++*errors;
         free(result);
         return 0;
diff --git a/etc/init.rc b/etc/init.rc
index abc7b31..1754890 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -2,6 +2,7 @@
 
 on early-init
     start ueventd
+    start healthd
 
 on init
     export PATH /sbin
@@ -37,13 +38,20 @@
 
     class_start default
 
+on property:sys.powerctl=*
+   powerctl ${sys.powerctl}
+
 service ueventd /sbin/ueventd
     critical
 
+service healthd /sbin/healthd -n
+    critical
+
 service recovery /sbin/recovery
 
 service adbd /sbin/adbd recovery
     disabled
+    socket adbd stream 660 system system
 
 # Always start adbd on userdebug and eng builds
 on property:ro.debuggable=1
diff --git a/install.cpp b/install.cpp
index 0cb5cc7..797a525 100644
--- a/install.cpp
+++ b/install.cpp
@@ -154,6 +154,7 @@
             } else {
                 ui->Print("\n");
             }
+            fflush(stdout);
         } else if (strcmp(command, "wipe_cache") == 0) {
             *wipe_cache = 1;
         } else if (strcmp(command, "clear_display") == 0) {
@@ -179,7 +180,9 @@
 {
     ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
     ui->Print("Finding update package...\n");
-    ui->SetProgressType(RecoveryUI::INDETERMINATE);
+    // Give verification half the progress bar...
+    ui->SetProgressType(RecoveryUI::DETERMINATE);
+    ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
     LOGI("Update location: %s\n", path);
 
     if (ensure_path_mounted(path) != 0) {
@@ -197,10 +200,7 @@
     }
     LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);
 
-    // Give verification half the progress bar...
     ui->Print("Verifying update package...\n");
-    ui->SetProgressType(RecoveryUI::DETERMINATE);
-    ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
 
     int err;
     err = verify_file(path, loadedKeys, numKeys);
@@ -236,7 +236,13 @@
     } else {
         LOGE("failed to open last_install: %s\n", strerror(errno));
     }
-    int result = really_install_package(path, wipe_cache);
+    int result;
+    if (setup_install_mounts() != 0) {
+        LOGE("failed to set up expected mounts for install; aborting\n");
+        result = INSTALL_ERROR;
+    } else {
+        result = really_install_package(path, wipe_cache);
+    }
     if (install_log) {
         fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
         fputc('\n', install_log);
diff --git a/minui/graphics.c b/minui/graphics.c
index 4968eac..d757165 100644
--- a/minui/graphics.c
+++ b/minui/graphics.c
@@ -385,8 +385,8 @@
 
     get_memory_surface(&gr_mem_surface);
 
-    fprintf(stderr, "framebuffer: fd %d (%d x %d)\n",
-            gr_fb_fd, gr_framebuffer[0].width, gr_framebuffer[0].height);
+    printf("framebuffer: fd %d (%d x %d)\n",
+           gr_fb_fd, gr_framebuffer[0].width, gr_framebuffer[0].height);
 
         /* start with 0 as front (displayed) and 1 as back (drawing) */
     gr_active_fb = 0;
diff --git a/minzip/DirUtil.c b/minzip/DirUtil.c
index 8dd5da1..fe2c880 100644
--- a/minzip/DirUtil.c
+++ b/minzip/DirUtil.c
@@ -234,61 +234,3 @@
     /* delete target directory */
     return rmdir(path);
 }
-
-int
-dirSetHierarchyPermissions(const char *path,
-        int uid, int gid, int dirMode, int fileMode)
-{
-    struct stat st;
-    if (lstat(path, &st)) {
-        return -1;
-    }
-
-    /* ignore symlinks */
-    if (S_ISLNK(st.st_mode)) {
-        return 0;
-    }
-
-    /* directories and files get different permissions */
-    if (chown(path, uid, gid) ||
-        chmod(path, S_ISDIR(st.st_mode) ? dirMode : fileMode)) {
-        return -1;
-    }
-
-    /* recurse over directory components */
-    if (S_ISDIR(st.st_mode)) {
-        DIR *dir = opendir(path);
-        if (dir == NULL) {
-            return -1;
-        }
-
-        errno = 0;
-        const struct dirent *de;
-        while (errno == 0 && (de = readdir(dir)) != NULL) {
-            if (!strcmp(de->d_name, "..") || !strcmp(de->d_name, ".")) {
-                continue;
-            }
-
-            char dn[PATH_MAX];
-            snprintf(dn, sizeof(dn), "%s/%s", path, de->d_name);
-            if (!dirSetHierarchyPermissions(dn, uid, gid, dirMode, fileMode)) {
-                errno = 0;
-            } else if (errno == 0) {
-                errno = -1;
-            }
-        }
-
-        if (errno != 0) {
-            int save = errno;
-            closedir(dir);
-            errno = save;
-            return -1;
-        }
-
-        if (closedir(dir)) {
-            return -1;
-        }
-    }
-
-    return 0;
-}
diff --git a/minzip/DirUtil.h b/minzip/DirUtil.h
index a5cfa76..85a0012 100644
--- a/minzip/DirUtil.h
+++ b/minzip/DirUtil.h
@@ -48,14 +48,6 @@
  */
 int dirUnlinkHierarchy(const char *path);
 
-/* chown -R <uid>:<gid> <path>
- * chmod -R <mode> <path>
- *
- * Sets directories to <dirMode> and files to <fileMode>.  Skips symlinks.
- */
-int dirSetHierarchyPermissions(const char *path,
-         int uid, int gid, int dirMode, int fileMode);
-
 #ifdef __cplusplus
 }
 #endif
diff --git a/mtdutils/mtdutils.c b/mtdutils/mtdutils.c
index 107cbb9..d04b26e 100644
--- a/mtdutils/mtdutils.c
+++ b/mtdutils/mtdutils.c
@@ -289,7 +289,7 @@
 {
     struct mtd_ecc_stats before, after;
     if (ioctl(fd, ECCGETSTATS, &before)) {
-        fprintf(stderr, "mtd: ECCGETSTATS error (%s)\n", strerror(errno));
+        printf("mtd: ECCGETSTATS error (%s)\n", strerror(errno));
         return -1;
     }
 
@@ -300,13 +300,13 @@
 
     while (pos + size <= (int) partition->size) {
         if (lseek64(fd, pos, SEEK_SET) != pos || read(fd, data, size) != size) {
-            fprintf(stderr, "mtd: read error at 0x%08llx (%s)\n",
+            printf("mtd: read error at 0x%08llx (%s)\n",
                     pos, strerror(errno));
         } else if (ioctl(fd, ECCGETSTATS, &after)) {
-            fprintf(stderr, "mtd: ECCGETSTATS error (%s)\n", strerror(errno));
+            printf("mtd: ECCGETSTATS error (%s)\n", strerror(errno));
             return -1;
         } else if (after.failed != before.failed) {
-            fprintf(stderr, "mtd: ECC errors (%d soft, %d hard) at 0x%08llx\n",
+            printf("mtd: ECC errors (%d soft, %d hard) at 0x%08llx\n",
                     after.corrected - before.corrected,
                     after.failed - before.failed, pos);
             // copy the comparison baseline for the next read.
@@ -431,39 +431,39 @@
         int retry;
         for (retry = 0; retry < 2; ++retry) {
             if (ioctl(fd, MEMERASE, &erase_info) < 0) {
-                fprintf(stderr, "mtd: erase failure at 0x%08lx (%s)\n",
+                printf("mtd: erase failure at 0x%08lx (%s)\n",
                         pos, strerror(errno));
                 continue;
             }
             if (lseek(fd, pos, SEEK_SET) != pos ||
                 write(fd, data, size) != size) {
-                fprintf(stderr, "mtd: write error at 0x%08lx (%s)\n",
+                printf("mtd: write error at 0x%08lx (%s)\n",
                         pos, strerror(errno));
             }
 
             char verify[size];
             if (lseek(fd, pos, SEEK_SET) != pos ||
                 read(fd, verify, size) != size) {
-                fprintf(stderr, "mtd: re-read error at 0x%08lx (%s)\n",
+                printf("mtd: re-read error at 0x%08lx (%s)\n",
                         pos, strerror(errno));
                 continue;
             }
             if (memcmp(data, verify, size) != 0) {
-                fprintf(stderr, "mtd: verification error at 0x%08lx (%s)\n",
+                printf("mtd: verification error at 0x%08lx (%s)\n",
                         pos, strerror(errno));
                 continue;
             }
 
             if (retry > 0) {
-                fprintf(stderr, "mtd: wrote block after %d retries\n", retry);
+                printf("mtd: wrote block after %d retries\n", retry);
             }
-            fprintf(stderr, "mtd: successfully wrote block at %lx\n", pos);
+            printf("mtd: successfully wrote block at %lx\n", pos);
             return 0;  // Success!
         }
 
         // Try to erase it once more as we give up on this block
         add_bad_block_offset(ctx, pos);
-        fprintf(stderr, "mtd: skipping write block at 0x%08lx\n", pos);
+        printf("mtd: skipping write block at 0x%08lx\n", pos);
         ioctl(fd, MEMERASE, &erase_info);
         pos += partition->erase_size;
     }
@@ -526,7 +526,7 @@
     while (blocks-- > 0) {
         loff_t bpos = pos;
         if (ioctl(ctx->fd, MEMGETBADBLOCK, &bpos) > 0) {
-            fprintf(stderr, "mtd: not erasing bad block at 0x%08lx\n", pos);
+            printf("mtd: not erasing bad block at 0x%08lx\n", pos);
             pos += ctx->partition->erase_size;
             continue;  // Don't try to erase known factory-bad blocks.
         }
@@ -535,7 +535,7 @@
         erase_info.start = pos;
         erase_info.length = ctx->partition->erase_size;
         if (ioctl(ctx->fd, MEMERASE, &erase_info) < 0) {
-            fprintf(stderr, "mtd: erase failure at 0x%08lx\n", pos);
+            printf("mtd: erase failure at 0x%08lx\n", pos);
         }
         pos += ctx->partition->erase_size;
     }
diff --git a/recovery.cpp b/recovery.cpp
index c82844d..d803cad 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -61,6 +61,7 @@
 
 #define LAST_LOG_FILE "/cache/recovery/last_log"
 
+static const char *CACHE_LOG_DIR = "/cache/recovery";
 static const char *COMMAND_FILE = "/cache/recovery/command";
 static const char *INTENT_FILE = "/cache/recovery/intent";
 static const char *LOG_FILE = "/cache/recovery/log";
@@ -74,6 +75,7 @@
 
 RecoveryUI* ui = NULL;
 char* locale = NULL;
+char recovery_version[PROPERTY_VALUE_MAX+1];
 
 /*
  * The recovery tool communicates with the main system through /cache files.
@@ -283,6 +285,19 @@
     }
 }
 
+static void
+copy_logs() {
+    // Copy logs to cache so the system can find out what happened.
+    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
+    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
+    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
+    chmod(LOG_FILE, 0600);
+    chown(LOG_FILE, 1000, 1000);   // system user
+    chmod(LAST_LOG_FILE, 0640);
+    chmod(LAST_INSTALL_FILE, 0644);
+    sync();
+}
+
 // clear the recovery command and prepare to boot a (hopefully working) system,
 // copy our log file to cache as well (for the system to read), and
 // record any intent we were asked to communicate back to the system.
@@ -312,14 +327,7 @@
         check_and_fclose(fp, LOCALE_FILE);
     }
 
-    // Copy logs to cache so the system can find out what happened.
-    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
-    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
-    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
-    chmod(LOG_FILE, 0600);
-    chown(LOG_FILE, 1000, 1000);   // system user
-    chmod(LAST_LOG_FILE, 0640);
-    chmod(LAST_INSTALL_FILE, 0644);
+    copy_logs();
 
     // Reset to normal system boot so recovery won't cycle indefinitely.
     struct bootloader_message boot;
@@ -336,22 +344,95 @@
     sync();  // For good measure.
 }
 
+typedef struct _saved_log_file {
+    char* name;
+    struct stat st;
+    unsigned char* data;
+    struct _saved_log_file* next;
+} saved_log_file;
+
 static int
 erase_volume(const char *volume) {
+    bool is_cache = (strcmp(volume, CACHE_ROOT) == 0);
+
     ui->SetBackground(RecoveryUI::ERASING);
     ui->SetProgressType(RecoveryUI::INDETERMINATE);
+
+    saved_log_file* head = NULL;
+
+    if (is_cache) {
+        // If we're reformatting /cache, we load any
+        // "/cache/recovery/last*" files into memory, so we can restore
+        // them after the reformat.
+
+        ensure_path_mounted(volume);
+
+        DIR* d;
+        struct dirent* de;
+        d = opendir(CACHE_LOG_DIR);
+        if (d) {
+            char path[PATH_MAX];
+            strcpy(path, CACHE_LOG_DIR);
+            strcat(path, "/");
+            int path_len = strlen(path);
+            while ((de = readdir(d)) != NULL) {
+                if (strncmp(de->d_name, "last", 4) == 0) {
+                    saved_log_file* p = (saved_log_file*) malloc(sizeof(saved_log_file));
+                    strcpy(path+path_len, de->d_name);
+                    p->name = strdup(path);
+                    if (stat(path, &(p->st)) == 0) {
+                        // truncate files to 512kb
+                        if (p->st.st_size > (1 << 19)) {
+                            p->st.st_size = 1 << 19;
+                        }
+                        p->data = (unsigned char*) malloc(p->st.st_size);
+                        FILE* f = fopen(path, "rb");
+                        fread(p->data, 1, p->st.st_size, f);
+                        fclose(f);
+                        p->next = head;
+                        head = p;
+                    } else {
+                        free(p);
+                    }
+                }
+            }
+            closedir(d);
+        } else {
+            if (errno != ENOENT) {
+                printf("opendir failed: %s\n", strerror(errno));
+            }
+        }
+    }
+
     ui->Print("Formatting %s...\n", volume);
 
     ensure_path_unmounted(volume);
+    int result = format_volume(volume);
 
-    if (strcmp(volume, "/cache") == 0) {
+    if (is_cache) {
+        while (head) {
+            FILE* f = fopen_path(head->name, "wb");
+            if (f) {
+                fwrite(head->data, 1, head->st.st_size, f);
+                fclose(f);
+                chmod(head->name, head->st.st_mode);
+                chown(head->name, head->st.st_uid, head->st.st_gid);
+            }
+            free(head->name);
+            free(head->data);
+            saved_log_file* temp = head->next;
+            free(head);
+            head = temp;
+        }
+
         // Any part of the log we'd copied to cache is now gone.
         // Reset the pointer so we copy from the beginning of the temp
         // log.
         tmplog_offset = 0;
+        copy_logs();
     }
 
-    return format_volume(volume);
+    return result;
 }
 
 static char*
@@ -446,21 +527,17 @@
 
 static const char**
 prepend_title(const char* const* headers) {
-    const char* title[] = { "Android system recovery <"
-                            EXPAND(RECOVERY_API_VERSION) "e>",
-                            "",
-                            NULL };
-
     // count the number of lines in our title, plus the
     // caller-provided headers.
-    int count = 0;
+    int count = 3;   // our title has 3 lines
     const char* const* p;
-    for (p = title; *p; ++p, ++count);
     for (p = headers; *p; ++p, ++count);
 
     const char** new_headers = (const char**)malloc((count+1) * sizeof(char*));
     const char** h = new_headers;
-    for (p = title; *p; ++p, ++h) *h = *p;
+    *(h++) = "Android system recovery <" EXPAND(RECOVERY_API_VERSION) "e>";
+    *(h++) = recovery_version;
+    *(h++) = "";
     for (p = headers; *p; ++p, ++h) *h = *p;
     *h = NULL;
 
@@ -734,10 +811,6 @@
                 break;
 
             case Device::APPLY_EXT:
-                // Some packages expect /cache to be mounted (eg,
-                // standard incremental packages expect to use /cache
-                // as scratch space).
-                ensure_path_mounted(CACHE_ROOT);
                 status = update_directory(SDCARD_ROOT, SDCARD_ROOT, &wipe_cache, device);
                 if (status == INSTALL_SUCCESS && wipe_cache) {
                     ui->Print("\n-- Wiping cache (at package request)...\n");
@@ -783,12 +856,12 @@
                 break;
 
             case Device::APPLY_ADB_SIDELOAD:
-                ensure_path_mounted(CACHE_ROOT);
                 status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
                 if (status >= 0) {
                     if (status != INSTALL_SUCCESS) {
                         ui->SetBackground(RecoveryUI::ERROR);
                         ui->Print("Installation aborted.\n");
+                        copy_logs();
                     } else if (!ui->IsTextVisible()) {
                         return;  // reboot if logs aren't visible
                     } else {
@@ -866,7 +939,7 @@
 
     load_volume_table();
     ensure_path_mounted(LAST_LOG_FILE);
-    rotate_last_logs(5);
+    rotate_last_logs(10);
     get_args(&argc, &argv);
 
     int previous_runs = 0;
@@ -913,8 +986,7 @@
     sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);
 
     if (!sehandle) {
-        fprintf(stderr, "Warning: No file_contexts\n");
-        ui->Print("Warning:  No file_contexts\n");
+        ui->Print("Warning: No file_contexts\n");
     }
 
     device->StartRecovery();
@@ -942,6 +1014,7 @@
     printf("\n");
 
     property_list(print_property, NULL);
+    property_get("ro.build.display.id", recovery_version, "");
     printf("\n");
 
     int status = INSTALL_SUCCESS;
@@ -979,6 +1052,7 @@
     }
 
     if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
+        copy_logs();
         ui->SetBackground(RecoveryUI::ERROR);
     }
     if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
@@ -988,6 +1062,6 @@
     // Otherwise, get ready to boot the main system...
     finish_recovery(send_intent);
     ui->Print("Rebooting...\n");
-    android_reboot(ANDROID_RB_RESTART, 0, 0);
+    property_set(ANDROID_RB_PROPERTY, "reboot,");
     return EXIT_SUCCESS;
 }
diff --git a/roots.cpp b/roots.cpp
index 0947122..113dba1 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -202,3 +202,22 @@
     LOGE("format_volume: fs_type \"%s\" unsupported\n", v->fs_type);
     return -1;
 }
+
+int setup_install_mounts() {
+    if (fstab == NULL) {
+        LOGE("can't set up install mounts: no fstab loaded\n");
+        return -1;
+    }
+    for (int i = 0; i < fstab->num_entries; ++i) {
+        Volume* v = fstab->recs + i;
+
+        if (strcmp(v->mount_point, "/tmp") == 0 ||
+            strcmp(v->mount_point, "/cache") == 0) {
+            if (ensure_path_mounted(v->mount_point) != 0) return -1;
+
+        } else {
+            if (ensure_path_unmounted(v->mount_point) != 0) return -1;
+        }
+    }
+    return 0;
+}
diff --git a/roots.h b/roots.h
index 8abe18f..230d9de 100644
--- a/roots.h
+++ b/roots.h
@@ -42,6 +42,10 @@
 // it is mounted.
 int format_volume(const char* volume);
 
+// Ensure that all and only the volumes that packages expect to find
+// mounted (/tmp and /cache) are mounted.  Returns 0 on success.
+int setup_install_mounts();
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/screen_ui.cpp b/screen_ui.cpp
index 93e2609..8376341 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -196,9 +196,29 @@
     }
 }
 
-#define C_HEADER  247,0,6
-#define C_MENU    0,106,157
-#define C_LOG     249,194,0
+void ScreenRecoveryUI::SetColor(UIElement e) {
+    switch (e) {
+        case HEADER:
+            gr_color(247, 0, 6, 255);
+            break;
+        case MENU:
+        case MENU_SEL_BG:
+            gr_color(0, 106, 157, 255);
+            break;
+        case MENU_SEL_FG:
+            gr_color(255, 255, 255, 255);
+            break;
+        case LOG:
+            gr_color(249, 194, 0, 255);
+            break;
+        case TEXT_FILL:
+            gr_color(0, 0, 0, 160);
+            break;
+        default:
+            gr_color(255, 255, 255, 255);
+            break;
+    }
+}
 
 // Redraw everything on the screen.  Does not flip pages.
 // Should only be called with updateMutex locked.
@@ -208,37 +228,38 @@
     draw_progress_locked();
 
     if (show_text) {
-        gr_color(0, 0, 0, 160);
+        SetColor(TEXT_FILL);
         gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
         int y = 0;
         int i = 0;
         if (show_menu) {
-            gr_color(C_HEADER, 255);
+            SetColor(HEADER);
 
             for (; i < menu_top + menu_items; ++i) {
-                if (i == menu_top) gr_color(C_MENU, 255);
+                if (i == menu_top) SetColor(MENU);
 
                 if (i == menu_top + menu_sel) {
                     // draw the highlight bar
+                    SetColor(MENU_SEL_BG);
                     gr_fill(0, y-2, gr_fb_width(), y+char_height+2);
                     // white text of selected item
-                    gr_color(255, 255, 255, 255);
+                    SetColor(MENU_SEL_FG);
                     if (menu[i][0]) gr_text(4, y, menu[i], 1);
-                    gr_color(C_MENU, 255);
+                    SetColor(MENU);
                 } else {
                     if (menu[i][0]) gr_text(4, y, menu[i], i < menu_top);
                 }
                 y += char_height+4;
             }
-            gr_color(C_MENU, 255);
+            SetColor(MENU);
             y += 4;
             gr_fill(0, y, gr_fb_width(), y+2);
             y += 4;
             ++i;
         }
 
-        gr_color(C_LOG, 255);
+        SetColor(LOG);
 
         // display from the bottom up, until we hit the top of the
         // screen, the bottom of the menu, or we've displayed the
@@ -446,10 +467,11 @@
     pthread_mutex_lock(&updateMutex);
     if (progressBarType != type) {
         progressBarType = type;
-        update_progress_locked();
     }
     progressScopeStart = 0;
+    progressScopeSize = 0;
     progress = 0;
+    update_progress_locked();
     pthread_mutex_unlock(&updateMutex);
 }
 
@@ -585,3 +607,10 @@
     update_screen_locked();
     pthread_mutex_unlock(&updateMutex);
 }
+
+void ScreenRecoveryUI::Redraw()
+{
+    pthread_mutex_lock(&updateMutex);
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
diff --git a/screen_ui.h b/screen_ui.h
index fe0de46..0bd220f 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -53,6 +53,11 @@
     int SelectMenu(int sel);
     void EndMenu();
 
+    void Redraw();
+
+    enum UIElement { HEADER, MENU, MENU_SEL_BG, MENU_SEL_FG, LOG, TEXT_FILL };
+    virtual void SetColor(UIElement e);
+
   private:
     Icon currentIcon;
     int installingFrame;
diff --git a/ui.cpp b/ui.cpp
index 65f4028..cece02d 100644
--- a/ui.cpp
+++ b/ui.cpp
@@ -46,7 +46,8 @@
 RecoveryUI::RecoveryUI() :
     key_queue_len(0),
     key_last_down(-1),
-    key_down_time(0) {
+    key_long_press(false),
+    key_down_count(0) {
     pthread_mutex_init(&key_queue_mutex, NULL);
     pthread_cond_init(&key_queue_cond, NULL);
     self = this;
@@ -112,19 +113,22 @@
     bool register_key = false;
     bool long_press = false;
 
-    const long long_threshold = CLOCKS_PER_SEC * 750 / 1000;
-
     pthread_mutex_lock(&key_queue_mutex);
     key_pressed[key_code] = updown;
     if (updown) {
+        ++key_down_count;
         key_last_down = key_code;
-        key_down_time = clock();
+        key_long_press = false;
+        pthread_t th;
+        key_timer_t* info = new key_timer_t;
+        info->ui = this;
+        info->key_code = key_code;
+        info->count = key_down_count;
+        pthread_create(&th, NULL, &RecoveryUI::time_key_helper, info);
+        pthread_detach(th);
     } else {
         if (key_last_down == key_code) {
-            long duration = clock() - key_down_time;
-            if (duration > long_threshold) {
-                long_press = true;
-            }
+            long_press = key_long_press;
             register_key = true;
         }
         key_last_down = -1;
@@ -152,6 +156,24 @@
     }
 }
 
+void* RecoveryUI::time_key_helper(void* cookie) {
+    key_timer_t* info = (key_timer_t*) cookie;
+    info->ui->time_key(info->key_code, info->count);
+    delete info;
+    return NULL;
+}
+
+void RecoveryUI::time_key(int key_code, int count) {
+    usleep(750000);  // 750 ms == "long"
+    bool long_press = false;
+    pthread_mutex_lock(&key_queue_mutex);
+    if (key_last_down == key_code && key_down_count == count) {
+        long_press = key_long_press = true;
+    }
+    pthread_mutex_unlock(&key_queue_mutex);
+    if (long_press) KeyLongPress(key_code);
+}
+
 void RecoveryUI::EnqueueKey(int key_code) {
     pthread_mutex_lock(&key_queue_mutex);
     const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
@@ -242,3 +264,6 @@
 
 void RecoveryUI::NextCheckKeyIsLong(bool is_long_press) {
 }
+
+void RecoveryUI::KeyLongPress(int key) {
+}
diff --git a/ui.h b/ui.h
index aca7b7b..6c8987a 100644
--- a/ui.h
+++ b/ui.h
@@ -80,8 +80,17 @@
     enum KeyAction { ENQUEUE, TOGGLE, REBOOT, IGNORE };
     virtual KeyAction CheckKey(int key);
 
+    // Called immediately before each call to CheckKey(), tell you if
+    // the key was long-pressed.
     virtual void NextCheckKeyIsLong(bool is_long_press);
 
+    // Called when a key is held down long enough to have been a
+    // long-press (but before the key is released).  This means that
+    // if the key is eventually registered (released without any other
+    // keys being pressed in the meantime), NextCheckKeyIsLong() will
+    // be called with "true".
+    virtual void KeyLongPress(int key);
+
     // --- menu display ---
 
     // Display some header text followed by a menu of items, which appears
@@ -108,15 +117,25 @@
     int key_queue[256], key_queue_len;
     char key_pressed[KEY_MAX + 1];     // under key_queue_mutex
     int key_last_down;                 // under key_queue_mutex
-    clock_t key_down_time;             // under key_queue_mutex
+    bool key_long_press;               // under key_queue_mutex
+    int key_down_count;                // under key_queue_mutex
     int rel_sum;
 
+    typedef struct {
+        RecoveryUI* ui;
+        int key_code;
+        int count;
+    } key_timer_t;
+
     pthread_t input_t;
 
     static void* input_thread(void* cookie);
     static int input_callback(int fd, short revents, void* data);
     void process_key(int key_code, int updown);
     bool usb_connected();
+
+    static void* time_key_helper(void* cookie);
+    void time_key(int key_code, int count);
 };
 
 #endif  // RECOVERY_UI_H
diff --git a/updater/install.c b/updater/install.c
index 1fc4fd3..061aa7b 100644
--- a/updater/install.c
+++ b/updater/install.c
@@ -27,6 +27,12 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <time.h>
+#include <selinux/selinux.h>
+#include <ftw.h>
+#include <sys/capability.h>
+#include <sys/xattr.h>
+#include <linux/xattr.h>
+#include <inttypes.h>
 
 #include "cutils/misc.h"
 #include "cutils/properties.h"
@@ -97,13 +103,13 @@
         const MtdPartition* mtd;
         mtd = mtd_find_partition_by_name(location);
         if (mtd == NULL) {
-            fprintf(stderr, "%s: no mtd partition named \"%s\"",
+            printf("%s: no mtd partition named \"%s\"",
                     name, location);
             result = strdup("");
             goto done;
         }
         if (mtd_mount_partition(mtd, mount_point, fs_type, 0 /* rw */) != 0) {
-            fprintf(stderr, "mtd mount of %s failed: %s\n",
+            printf("mtd mount of %s failed: %s\n",
                     location, strerror(errno));
             result = strdup("");
             goto done;
@@ -112,7 +118,7 @@
     } else {
         if (mount(location, mount_point, fs_type,
                   MS_NOATIME | MS_NODEV | MS_NODIRATIME, "") < 0) {
-            fprintf(stderr, "%s: failed to mount %s at %s: %s\n",
+            printf("%s: failed to mount %s at %s: %s\n",
                     name, location, mount_point, strerror(errno));
             result = strdup("");
         } else {
@@ -175,7 +181,7 @@
     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);
+        printf("unmount of %s failed; no such volume\n", mount_point);
         result = strdup("");
     } else {
         unmount_mounted_volume(vol);
@@ -233,25 +239,25 @@
         mtd_scan_partitions();
         const MtdPartition* mtd = mtd_find_partition_by_name(location);
         if (mtd == NULL) {
-            fprintf(stderr, "%s: no mtd partition named \"%s\"",
+            printf("%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);
+            printf("%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);
+            printf("%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);
+            printf("%s: failed to close \"%s\"", name, location);
             result = strdup("");
             goto done;
         }
@@ -260,7 +266,7 @@
     } else if (strcmp(fs_type, "ext4") == 0) {
         int status = make_ext4fs(location, atoll(fs_size), mount_point, sehandle);
         if (status != 0) {
-            fprintf(stderr, "%s: make_ext4fs failed (%d) on %s",
+            printf("%s: make_ext4fs failed (%d) on %s",
                     name, status, location);
             result = strdup("");
             goto done;
@@ -268,7 +274,7 @@
         result = location;
 #endif
     } else {
-        fprintf(stderr, "%s: unsupported fs_type \"%s\" partition_type \"%s\"",
+        printf("%s: unsupported fs_type \"%s\" partition_type \"%s\"",
                 name, fs_type, partition_type);
     }
 
@@ -394,13 +400,13 @@
         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);
+            printf("%s: no %s in package\n", name, zip_path);
             goto done2;
         }
 
         FILE* f = fopen(dest_path, "wb");
         if (f == NULL) {
-            fprintf(stderr, "%s: can't open %s for write: %s\n",
+            printf("%s: can't open %s for write: %s\n",
                     name, dest_path, strerror(errno));
             goto done2;
         }
@@ -426,14 +432,14 @@
         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);
+            printf("%s: no %s in package\n", name, zip_path);
             goto done1;
         }
 
         v->size = mzGetZipEntryUncompLen(entry);
         v->data = malloc(v->size);
         if (v->data == NULL) {
-            fprintf(stderr, "%s: failed to allocate %ld bytes for %s\n",
+            printf("%s: failed to allocate %ld bytes for %s\n",
                     name, (long)v->size, zip_path);
             goto done1;
         }
@@ -460,13 +466,13 @@
         *p = '\0';
         if (make_parents(name) < 0) return -1;
         int result = mkdir(name, 0700);
-        if (result == 0) fprintf(stderr, "symlink(): created [%s]\n", name);
+        if (result == 0) printf("symlink(): created [%s]\n", name);
         *p = '/';
         if (result == 0 || errno == EEXIST) {
             // successfully created or already existed; we're done
             return 0;
         } else {
-            fprintf(stderr, "failed to mkdir %s: %s\n", name, strerror(errno));
+            printf("failed to mkdir %s: %s\n", name, strerror(errno));
             return -1;
         }
     }
@@ -494,18 +500,18 @@
     for (i = 0; i < argc-1; ++i) {
         if (unlink(srcs[i]) < 0) {
             if (errno != ENOENT) {
-                fprintf(stderr, "%s: failed to remove %s: %s\n",
+                printf("%s: failed to remove %s: %s\n",
                         name, srcs[i], strerror(errno));
                 ++bad;
             }
         }
         if (make_parents(srcs[i])) {
-            fprintf(stderr, "%s: failed to symlink %s to %s: making parents failed\n",
+            printf("%s: failed to symlink %s to %s: making parents failed\n",
                     name, srcs[i], target);
             ++bad;
         }
         if (symlink(target, srcs[i]) < 0) {
-            fprintf(stderr, "%s: failed to symlink %s to %s: %s\n",
+            printf("%s: failed to symlink %s to %s: %s\n",
                     name, srcs[i], target, strerror(errno));
             ++bad;
         }
@@ -518,74 +524,247 @@
     return StringValue(strdup(""));
 }
 
+struct perm_parsed_args {
+    bool has_uid;
+    uid_t uid;
+    bool has_gid;
+    gid_t gid;
+    bool has_mode;
+    mode_t mode;
+    bool has_fmode;
+    mode_t fmode;
+    bool has_dmode;
+    mode_t dmode;
+    bool has_selabel;
+    char* selabel;
+    bool has_capabilities;
+    uint64_t capabilities;
+};
 
-Value* SetPermFn(const char* name, State* state, int argc, Expr* argv[]) {
-    char* result = NULL;
-    bool recursive = (strcmp(name, "set_perm_recursive") == 0);
+static struct perm_parsed_args ParsePermArgs(int argc, char** args) {
+    int i;
+    struct perm_parsed_args parsed;
+    int bad = 0;
+    static int max_warnings = 20;
 
-    int min_args = 4 + (recursive ? 1 : 0);
-    if (argc < min_args) {
-        return ErrorAbort(state, "%s() expects %d+ args, got %d",
-                          name, min_args, argc);
+    memset(&parsed, 0, sizeof(parsed));
+
+    for (i = 1; i < argc; i += 2) {
+        if (strcmp("uid", args[i]) == 0) {
+            int64_t uid;
+            if (sscanf(args[i+1], "%" SCNd64, &uid) == 1) {
+                parsed.uid = uid;
+                parsed.has_uid = true;
+            } else {
+                printf("ParsePermArgs: invalid UID \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("gid", args[i]) == 0) {
+            int64_t gid;
+            if (sscanf(args[i+1], "%" SCNd64, &gid) == 1) {
+                parsed.gid = gid;
+                parsed.has_gid = true;
+            } else {
+                printf("ParsePermArgs: invalid GID \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("mode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.mode = mode;
+                parsed.has_mode = true;
+            } else {
+                printf("ParsePermArgs: invalid mode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("dmode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.dmode = mode;
+                parsed.has_dmode = true;
+            } else {
+                printf("ParsePermArgs: invalid dmode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("fmode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.fmode = mode;
+                parsed.has_fmode = true;
+            } else {
+                printf("ParsePermArgs: invalid fmode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("capabilities", args[i]) == 0) {
+            int64_t capabilities;
+            if (sscanf(args[i+1], "%" SCNi64, &capabilities) == 1) {
+                parsed.capabilities = capabilities;
+                parsed.has_capabilities = true;
+            } else {
+                printf("ParsePermArgs: invalid capabilities \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("selabel", args[i]) == 0) {
+            if (args[i+1][0] != '\0') {
+                parsed.selabel = args[i+1];
+                parsed.has_selabel = true;
+            } else {
+                printf("ParsePermArgs: invalid selabel \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (max_warnings != 0) {
+            printf("ParsedPermArgs: unknown key \"%s\", ignoring\n", args[i]);
+            max_warnings--;
+            if (max_warnings == 0) {
+                printf("ParsedPermArgs: suppressing further warnings\n");
+            }
+        }
+    }
+    return parsed;
+}
+
+static int ApplyParsedPerms(
+        const char* filename,
+        const struct stat *statptr,
+        struct perm_parsed_args parsed)
+{
+    int bad = 0;
+
+    /* ignore symlinks */
+    if (S_ISLNK(statptr->st_mode)) {
+        return 0;
+    }
+
+    if (parsed.has_uid) {
+        if (chown(filename, parsed.uid, -1) < 0) {
+            printf("ApplyParsedPerms: chown of %s to %d failed: %s\n",
+                   filename, parsed.uid, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_gid) {
+        if (chown(filename, -1, parsed.gid) < 0) {
+            printf("ApplyParsedPerms: chgrp of %s to %d failed: %s\n",
+                   filename, parsed.gid, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_mode) {
+        if (chmod(filename, parsed.mode) < 0) {
+            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                   filename, parsed.mode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_dmode && S_ISDIR(statptr->st_mode)) {
+        if (chmod(filename, parsed.dmode) < 0) {
+            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                   filename, parsed.dmode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_fmode && S_ISREG(statptr->st_mode)) {
+        if (chmod(filename, parsed.fmode) < 0) {
+            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                   filename, parsed.fmode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_selabel) {
+        // TODO: Don't silently ignore ENOTSUP
+        if (lsetfilecon(filename, parsed.selabel) && (errno != ENOTSUP)) {
+            printf("ApplyParsedPerms: lsetfilecon of %s to %s failed: %s\n",
+                   filename, parsed.selabel, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_capabilities && S_ISREG(statptr->st_mode)) {
+        if (parsed.capabilities == 0) {
+            if ((removexattr(filename, XATTR_NAME_CAPS) == -1) && (errno != ENODATA)) {
+                // Report failure unless it's ENODATA (attribute not set)
+                printf("ApplyParsedPerms: removexattr of %s to %" PRIx64 " failed: %s\n",
+                       filename, parsed.capabilities, strerror(errno));
+                bad++;
+            }
+        } else {
+            struct vfs_cap_data cap_data;
+            memset(&cap_data, 0, sizeof(cap_data));
+            cap_data.magic_etc = VFS_CAP_REVISION | VFS_CAP_FLAGS_EFFECTIVE;
+            cap_data.data[0].permitted = (uint32_t) (parsed.capabilities & 0xffffffff);
+            cap_data.data[0].inheritable = 0;
+            cap_data.data[1].permitted = (uint32_t) (parsed.capabilities >> 32);
+            cap_data.data[1].inheritable = 0;
+            if (setxattr(filename, XATTR_NAME_CAPS, &cap_data, sizeof(cap_data), 0) < 0) {
+                printf("ApplyParsedPerms: setcap of %s to %" PRIx64 " failed: %s\n",
+                       filename, parsed.capabilities, strerror(errno));
+                bad++;
+            }
+        }
+    }
+
+    return bad;
+}
+
+// nftw doesn't allow us to pass along context, so we need to use
+// global variables.  *sigh*
+static struct perm_parsed_args recursive_parsed_args;
+
+static int do_SetMetadataRecursive(const char* filename, const struct stat *statptr,
+        int fileflags, struct FTW *pfwt) {
+    return ApplyParsedPerms(filename, statptr, recursive_parsed_args);
+}
+
+static Value* SetMetadataFn(const char* name, State* state, int argc, Expr* argv[]) {
+    int i;
+    int bad = 0;
+    static int nwarnings = 0;
+    struct stat sb;
+    Value* result = NULL;
+
+    bool recursive = (strcmp(name, "set_metadata_recursive") == 0);
+
+    if ((argc % 2) != 1) {
+        return ErrorAbort(state, "%s() expects an odd number of arguments, got %d",
+                          name, argc);
     }
 
     char** args = ReadVarArgs(state, argc, argv);
     if (args == NULL) return NULL;
 
-    char* end;
-    int i;
-    int bad = 0;
-
-    int uid = strtoul(args[0], &end, 0);
-    if (*end != '\0' || args[0][0] == 0) {
-        ErrorAbort(state, "%s: \"%s\" not a valid uid", name, args[0]);
+    if (lstat(args[0], &sb) == -1) {
+        result = ErrorAbort(state, "%s: Error on lstat of \"%s\": %s", name, args[0], strerror(errno));
         goto done;
     }
 
-    int gid = strtoul(args[1], &end, 0);
-    if (*end != '\0' || args[1][0] == 0) {
-        ErrorAbort(state, "%s: \"%s\" not a valid gid", name, args[1]);
-        goto done;
-    }
+    struct perm_parsed_args parsed = ParsePermArgs(argc, args);
 
     if (recursive) {
-        int dir_mode = strtoul(args[2], &end, 0);
-        if (*end != '\0' || args[2][0] == 0) {
-            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(state, "%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);
-        }
+        recursive_parsed_args = parsed;
+        bad += nftw(args[0], do_SetMetadataRecursive, 30, FTW_CHDIR | FTW_DEPTH | FTW_PHYS);
+        memset(&recursive_parsed_args, 0, sizeof(recursive_parsed_args));
     } else {
-        int mode = strtoul(args[2], &end, 0);
-        if (*end != '\0' || args[2][0] == 0) {
-            ErrorAbort(state, "%s: \"%s\" not a valid mode", name, args[2]);
-            goto done;
-        }
-
-        for (i = 3; i < argc; ++i) {
-            if (chown(args[i], uid, gid) < 0) {
-                fprintf(stderr, "%s: chown of %s to %d %d failed: %s\n",
-                        name, args[i], uid, gid, strerror(errno));
-                ++bad;
-            }
-            if (chmod(args[i], mode) < 0) {
-                fprintf(stderr, "%s: chmod of %s to %o failed: %s\n",
-                        name, args[i], mode, strerror(errno));
-                ++bad;
-            }
-        }
+        bad += ApplyParsedPerms(args[0], &sb, parsed);
     }
-    result = strdup("");
 
 done:
     for (i = 0; i < argc; ++i) {
@@ -593,13 +772,16 @@
     }
     free(args);
 
-    if (bad) {
-        free(result);
+    if (result != NULL) {
+        return result;
+    }
+
+    if (bad > 0) {
         return ErrorAbort(state, "%s: some changes failed", name);
     }
-    return StringValue(result);
-}
 
+    return StringValue(strdup(""));
+}
 
 Value* GetPropFn(const char* name, State* state, int argc, Expr* argv[]) {
     if (argc != 1) {
@@ -720,7 +902,7 @@
                                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));
+    printf("%s\n", strerror(errno));
     return false;
 }
 
@@ -752,14 +934,14 @@
     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);
+        printf("%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",
+        printf("%s: can't write mtd partition \"%s\"\n",
                 name, partition);
         result = strdup("");
         goto done;
@@ -772,7 +954,7 @@
         char* filename = contents->data;
         FILE* f = fopen(filename, "rb");
         if (f == NULL) {
-            fprintf(stderr, "%s: can't open %s: %s\n",
+            printf("%s: can't open %s: %s\n",
                     name, filename, strerror(errno));
             result = strdup("");
             goto done;
@@ -793,15 +975,15 @@
         success = (wrote == contents->size);
     }
     if (!success) {
-        fprintf(stderr, "mtd_write_data to %s failed: %s\n",
+        printf("mtd_write_data to %s failed: %s\n",
                 partition, strerror(errno));
     }
 
     if (mtd_erase_blocks(ctx, -1) == -1) {
-        fprintf(stderr, "%s: error erasing blocks of %s\n", name, partition);
+        printf("%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: error closing write of %s\n", name, partition);
     }
 
     printf("%s %s partition\n",
@@ -988,23 +1170,23 @@
     memcpy(args2, args, sizeof(char*) * argc);
     args2[argc] = NULL;
 
-    fprintf(stderr, "about to run program [%s] with %d args\n", args2[0], argc);
+    printf("about to run program [%s] with %d args\n", args2[0], argc);
 
     pid_t child = fork();
     if (child == 0) {
         execv(args2[0], args2);
-        fprintf(stderr, "run_program: execv failed: %s\n", strerror(errno));
+        printf("run_program: execv failed: %s\n", strerror(errno));
         _exit(1);
     }
     int status;
     waitpid(child, &status, 0);
     if (WIFEXITED(status)) {
         if (WEXITSTATUS(status) != 0) {
-            fprintf(stderr, "run_program: child exited with status %d\n",
+            printf("run_program: child exited with status %d\n",
                     WEXITSTATUS(status));
         }
     } else if (WIFSIGNALED(status)) {
-        fprintf(stderr, "run_program: child terminated by signal %d\n",
+        printf("run_program: child terminated by signal %d\n",
                 WTERMSIG(status));
     }
 
@@ -1053,7 +1235,7 @@
     }
 
     if (args[0]->size < 0) {
-        fprintf(stderr, "%s(): no file contents received", name);
+        printf("%s(): no file contents received", name);
         return StringValue(strdup(""));
     }
     uint8_t digest[SHA_DIGEST_SIZE];
@@ -1068,12 +1250,12 @@
     uint8_t* arg_digest = malloc(SHA_DIGEST_SIZE);
     for (i = 1; i < argc; ++i) {
         if (args[i]->type != VAL_STRING) {
-            fprintf(stderr, "%s(): arg %d is not a string; skipping",
+            printf("%s(): arg %d is not a string; skipping",
                     name, i);
         } else if (ParseSha1(args[i]->data, arg_digest) != 0) {
             // Warn about bad args and skip them.
-            fprintf(stderr, "%s(): error parsing \"%s\" as sha-1; skipping",
-                    name, args[i]->data);
+            printf("%s(): error parsing \"%s\" as sha-1; skipping",
+                   name, args[i]->data);
         } else if (memcmp(digest, arg_digest, SHA_DIGEST_SIZE) == 0) {
             break;
         }
@@ -1133,8 +1315,18 @@
     RegisterFunction("package_extract_dir", PackageExtractDirFn);
     RegisterFunction("package_extract_file", PackageExtractFileFn);
     RegisterFunction("symlink", SymlinkFn);
-    RegisterFunction("set_perm", SetPermFn);
-    RegisterFunction("set_perm_recursive", SetPermFn);
+
+    // Usage:
+    //   set_metadata("filename", "key1", "value1", "key2", "value2", ...)
+    // Example:
+    //   set_metadata("/system/bin/netcfg", "uid", 0, "gid", 3003, "mode", 02750, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
+    RegisterFunction("set_metadata", SetMetadataFn);
+
+    // Usage:
+    //   set_metadata_recursive("dirname", "key1", "value1", "key2", "value2", ...)
+    // Example:
+    //   set_metadata_recursive("/system", "uid", 0, "gid", 0, "fmode", 0644, "dmode", 0755, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
+    RegisterFunction("set_metadata_recursive", SetMetadataFn);
 
     RegisterFunction("getprop", GetPropFn);
     RegisterFunction("file_getprop", FileGetPropFn);
diff --git a/updater/updater.c b/updater/updater.c
index 58ac27f..c7009fe 100644
--- a/updater/updater.c
+++ b/updater/updater.c
@@ -36,13 +36,14 @@
 
 int main(int argc, char** argv) {
     // Various things log information to stdout or stderr more or less
-    // at random.  The log file makes more sense if buffering is
-    // turned off so things appear in the right order.
+    // at random (though we've tried to standardize on stdout).  The
+    // log file makes more sense if buffering is turned off so things
+    // appear in the right order.
     setbuf(stdout, NULL);
     setbuf(stderr, NULL);
 
     if (argc != 4) {
-        fprintf(stderr, "unexpected number of arguments (%d)\n", argc);
+        printf("unexpected number of arguments (%d)\n", argc);
         return 1;
     }
 
@@ -50,7 +51,7 @@
     if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||
         version[1] != '\0') {
         // We support version 1, 2, or 3.
-        fprintf(stderr, "wrong updater binary API; expected 1, 2, or 3; "
+        printf("wrong updater binary API; expected 1, 2, or 3; "
                         "got %s\n",
                 argv[1]);
         return 2;
@@ -69,20 +70,20 @@
     int err;
     err = mzOpenZipArchive(package_data, &za);
     if (err != 0) {
-        fprintf(stderr, "failed to open package %s: %s\n",
+        printf("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);
+        printf("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");
+        printf("failed to read script from package\n");
         return 5;
     }
     script[script_entry->uncompLen] = '\0';
@@ -101,7 +102,7 @@
     yy_scan_string(script);
     int error = yyparse(&root, &error_count);
     if (error != 0 || error_count > 0) {
-        fprintf(stderr, "%d parse errors\n", error_count);
+        printf("%d parse errors\n", error_count);
         return 6;
     }
 
@@ -112,7 +113,6 @@
     sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);
 
     if (!sehandle) {
-        fprintf(stderr, "Warning:  No file_contexts\n");
         fprintf(cmd_pipe, "ui_print Warning: No file_contexts\n");
     }
 
@@ -131,10 +131,10 @@
     char* result = Evaluate(&state, root);
     if (result == NULL) {
         if (state.errmsg == NULL) {
-            fprintf(stderr, "script aborted (no error message)\n");
+            printf("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);
+            printf("script aborted: %s\n", state.errmsg);
             char* line = strtok(state.errmsg, "\n");
             while (line) {
                 fprintf(cmd_pipe, "ui_print %s\n", line);
@@ -145,7 +145,7 @@
         free(state.errmsg);
         return 7;
     } else {
-        fprintf(stderr, "script result was [%s]\n", result);
+        fprintf(cmd_pipe, "ui_print script succeeded: result was [%s]\n", result);
         free(result);
     }