liblog: add __android_log_pmsg_file_read

(cherry pick from commit 864e8e80e4f6b325ddacc25b0bd5d621eff0fb3a)

- This is considered an Android Private function, not exported
  for general use.
- goal is to retreive a file's content from a series of log
  messages from pmsg, to be retrieved after a reboot for
  transfer to a persistent location.
- files are presented in reverse sorted order, first based on
  _any_ numerical content, then by alphanumeric order.
- Add a gTest for this function, relies on gTest for
  liblog.__android_log_pmsg_file_write from prior to reboot.

Bug: 27176738
Change-Id: If37ef423009bd28b598b233af3bccef3429bdc22
diff --git a/include/private/android_logger.h b/include/private/android_logger.h
index a637ca5..c3ea1ed 100644
--- a/include/private/android_logger.h
+++ b/include/private/android_logger.h
@@ -111,6 +111,20 @@
         const char *filename,
         const char *buf, size_t len);
 
+#define LOG_ID_ANY      ((log_id_t)-1)
+#define ANDROID_LOG_ANY ANDROID_LOG_UNKNOWN
+
+/* first 5 arguments match __android_log_msg_file_write, a cast is safe */
+typedef ssize_t (*__android_log_pmsg_file_read_fn)(
+        log_id_t logId,
+        char prio,
+        const char *filename,
+        const char *buf, size_t len, void *arg);
+
+ssize_t __android_log_pmsg_file_read(
+        log_id_t logId, char prio, const char *prefix,
+        __android_log_pmsg_file_read_fn fn, void *arg);
+
 #if defined(__cplusplus)
 }
 #endif
diff --git a/liblog/pmsg_reader.c b/liblog/pmsg_reader.c
index de435f7..5695e8a 100644
--- a/liblog/pmsg_reader.c
+++ b/liblog/pmsg_reader.c
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
+#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <stdbool.h>
+#include <stdlib.h>
 #include <string.h>
 #include <sys/types.h>
 
@@ -253,3 +255,333 @@
     }
     transp->context.fd = 0;
 }
+
+LIBLOG_ABI_PRIVATE ssize_t __android_log_pmsg_file_read(
+        log_id_t logId,
+        char prio,
+        const char *prefix,
+        __android_log_pmsg_file_read_fn fn, void *arg) {
+    ssize_t ret;
+    struct android_log_logger_list logger_list;
+    struct android_log_transport_context transp;
+    struct content {
+        struct listnode node;
+        union {
+            struct logger_entry_v4 entry;
+            struct logger_entry_v4 entry_v4;
+            struct logger_entry_v3 entry_v3;
+            struct logger_entry_v2 entry_v2;
+            struct logger_entry    entry_v1;
+        };
+    } *content;
+    struct names {
+        struct listnode node;
+        struct listnode content;
+        log_id_t id;
+        char prio;
+        char name[];
+    } *names;
+    struct listnode name_list;
+    struct listnode *node, *n;
+    size_t len, prefix_len;
+
+    if (!fn) {
+        return -EINVAL;
+    }
+
+    /* Add just enough clues in logger_list and transp to make API function */
+    memset(&logger_list, 0, sizeof(logger_list));
+    memset(&transp, 0, sizeof(transp));
+
+    logger_list.mode = ANDROID_LOG_PSTORE |
+                       ANDROID_LOG_NONBLOCK |
+                       ANDROID_LOG_RDONLY;
+    transp.logMask = (unsigned)-1;
+    if (logId != LOG_ID_ANY) {
+        transp.logMask = (1 << logId);
+    }
+    transp.logMask &= ~((1 << LOG_ID_KERNEL) |
+                        (1 << LOG_ID_EVENTS) |
+                        (1 << LOG_ID_SECURITY));
+    if (!transp.logMask) {
+        return -EINVAL;
+    }
+
+    /* Initialize name list */
+    list_init(&name_list);
+
+    ret = SSIZE_MAX;
+
+    /* Validate incoming prefix, shift until it contains only 0 or 1 : or / */
+    prefix_len = 0;
+    if (prefix) {
+        const char *prev = NULL, *last = NULL, *cp = prefix;
+        while ((cp = strpbrk(cp, "/:"))) {
+            prev = last;
+            last = cp;
+            cp = cp + 1;
+        }
+        if (prev) {
+            prefix = prev + 1;
+        }
+        prefix_len = strlen(prefix);
+    }
+
+    /* Read the file content */
+    while (pmsgRead(&logger_list, &transp, &transp.logMsg) > 0) {
+        char *cp;
+        size_t hdr_size = transp.logMsg.entry.hdr_size ?
+            transp.logMsg.entry.hdr_size : sizeof(transp.logMsg.entry_v1);
+        char *msg = (char *)&transp.logMsg + hdr_size;
+        char *split = NULL;
+
+        /* Check for invalid sequence number */
+        if ((transp.logMsg.entry.nsec % ANDROID_LOG_PMSG_FILE_SEQUENCE) ||
+                ((transp.logMsg.entry.nsec / ANDROID_LOG_PMSG_FILE_SEQUENCE) >=
+                    ANDROID_LOG_PMSG_FILE_MAX_SEQUENCE)) {
+            continue;
+        }
+
+        /* Determine if it has <dirbase>:<filebase> format for tag */
+        len = transp.logMsg.entry.len - sizeof(prio);
+        for (cp = msg + sizeof(prio);
+                *cp && isprint(*cp) && !isspace(*cp) && --len;
+                ++cp) {
+            if (*cp == ':') {
+                if (split) {
+                    break;
+                }
+                split = cp;
+            }
+        }
+        if (*cp || !split) {
+            continue;
+        }
+
+        /* Filters */
+        if (prefix_len && strncmp(msg + sizeof(prio), prefix, prefix_len)) {
+            size_t offset;
+            /*
+             *   Allow : to be a synonym for /
+             * Things we do dealing with const char * and do not alloc
+             */
+            split = strchr(prefix, ':');
+            if (split) {
+                continue;
+            }
+            split = strchr(prefix, '/');
+            if (!split) {
+                continue;
+            }
+            offset = split - prefix;
+            if ((msg[offset + sizeof(prio)] != ':') ||
+                    strncmp(msg + sizeof(prio), prefix, offset)) {
+                continue;
+            }
+            ++offset;
+            if ((prefix_len > offset) &&
+                    strncmp(&msg[offset + sizeof(prio)], split + 1, prefix_len - offset)) {
+                continue;
+            }
+        }
+
+        if ((prio != ANDROID_LOG_ANY) && (*msg < prio)) {
+            continue;
+        }
+
+        /* check if there is an existing entry */
+        list_for_each(node, &name_list) {
+            names = node_to_item(node, struct names, node);
+            if (!strcmp(names->name, msg + sizeof(prio)) &&
+                    (names->id == transp.logMsg.entry.lid) &&
+                    (names->prio == *msg)) {
+                break;
+            }
+        }
+
+        /* We do not have an existing entry, create and add one */
+        if (node == &name_list) {
+            static const char numbers[] = "0123456789";
+            unsigned long long nl;
+
+            len = strlen(msg + sizeof(prio)) + 1;
+            names = calloc(1, sizeof(*names) + len);
+            if (!names) {
+                ret = -ENOMEM;
+                break;
+            }
+            strcpy(names->name, msg + sizeof(prio));
+            names->id = transp.logMsg.entry.lid;
+            names->prio = *msg;
+            list_init(&names->content);
+            /*
+             * Insert in reverse numeric _then_ alpha sorted order as
+             * representative of log rotation:
+             *
+             *   log.10
+             *   klog.10
+             *   . . .
+             *   log.2
+             *   klog.2
+             *   log.1
+             *   klog.1
+             *   log
+             *   klog
+             *
+             * thus when we present the content, we are provided the oldest
+             * first, which when 'refreshed' could spill off the end of the
+             * pmsg FIFO but retaining the newest data for last with best
+             * chances to survive.
+             */
+            nl = 0;
+            cp = strpbrk(names->name, numbers);
+            if (cp) {
+                nl = strtoull(cp, NULL, 10);
+            }
+            list_for_each_reverse(node, &name_list) {
+                struct names *a_name = node_to_item(node, struct names, node);
+                const char *r = a_name->name;
+                int compare = 0;
+
+                unsigned long long nr = 0;
+                cp = strpbrk(r, numbers);
+                if (cp) {
+                    nr = strtoull(cp, NULL, 10);
+                }
+                if (nr != nl) {
+                    compare = (nl > nr) ? 1 : -1;
+                }
+                if (compare == 0) {
+                    compare = strcmp(names->name, r);
+                }
+                if (compare <= 0) {
+                    break;
+                }
+            }
+            list_add_head(node, &names->node);
+        }
+
+        /* Remove any file fragments that match our sequence number */
+        list_for_each_safe(node, n, &names->content) {
+            content = node_to_item(node, struct content, node);
+            if (transp.logMsg.entry.nsec == content->entry.nsec) {
+                list_remove(&content->node);
+                free(content);
+            }
+        }
+
+        /* Add content */
+        content = calloc(1, sizeof(content->node) +
+                hdr_size + transp.logMsg.entry.len);
+        if (!content) {
+            ret = -ENOMEM;
+            break;
+        }
+        memcpy(&content->entry, &transp.logMsg.entry,
+               hdr_size + transp.logMsg.entry.len);
+
+        /* Insert in sequence number sorted order, to ease reconstruction */
+        list_for_each_reverse(node, &names->content) {
+            if ((node_to_item(node, struct content, node))->entry.nsec <
+                    transp.logMsg.entry.nsec) {
+                break;
+            }
+        }
+        list_add_head(node, &content->node);
+    }
+    pmsgClose(&logger_list, &transp);
+
+    /* Progress through all the collected files */
+    list_for_each_safe(node, n, &name_list) {
+        struct listnode *content_node, *m;
+        char *buf;
+        size_t sequence, tag_len;
+
+        names = node_to_item(node, struct names, node);
+
+        /* Construct content into a linear buffer */
+        buf = NULL;
+        len = 0;
+        sequence = 0;
+        tag_len = strlen(names->name) + sizeof(char); /* tag + nul */
+        list_for_each_safe(content_node, m, &names->content) {
+            ssize_t add_len;
+
+            content = node_to_item(content_node, struct content, node);
+            add_len = content->entry.len - tag_len - sizeof(prio);
+            if (add_len <= 0) {
+                list_remove(content_node);
+                free(content);
+                continue;
+            }
+
+            if (!buf) {
+                buf = malloc(sizeof(char));
+                if (!buf) {
+                    ret = -ENOMEM;
+                    list_remove(content_node);
+                    free(content);
+                    continue;
+                }
+                *buf = '\0';
+            }
+
+            /* Missing sequence numbers */
+            while (sequence < content->entry.nsec) {
+                /* plus space for enforced nul */
+                buf = realloc(buf, len + sizeof(char) + sizeof(char));
+                if (!buf) {
+                    break;
+                }
+                buf[len] = '\f'; /* Mark missing content with a form feed */
+                buf[++len] = '\0';
+                sequence += ANDROID_LOG_PMSG_FILE_SEQUENCE;
+            }
+            if (!buf) {
+                ret = -ENOMEM;
+                list_remove(content_node);
+                free(content);
+                continue;
+            }
+            /* plus space for enforced nul */
+            buf = realloc(buf, len + add_len + sizeof(char));
+            if (!buf) {
+                ret = -ENOMEM;
+                list_remove(content_node);
+                free(content);
+                continue;
+            }
+            memcpy(buf + len,
+                   (char *)&content->entry + content->entry.hdr_size +
+                       tag_len + sizeof(prio),
+                   add_len);
+            len += add_len;
+            buf[len] = '\0'; /* enforce trailing hidden nul */
+            sequence = content->entry.nsec + ANDROID_LOG_PMSG_FILE_SEQUENCE;
+
+            list_remove(content_node);
+            free(content);
+        }
+        if (buf) {
+            if (len) {
+                /* Buffer contains enforced trailing nul just beyond length */
+                ssize_t r;
+                *strchr(names->name, ':') = '/'; /* Convert back to filename */
+                r = (*fn)(names->id, names->prio, names->name, buf, len, arg);
+                if ((ret >= 0) && (r > 0)) {
+                    if (ret == SSIZE_MAX) {
+                        ret = r;
+                    } else {
+                        ret += r;
+                    }
+                } else if (r < ret) {
+                    ret = r;
+                }
+            }
+            free(buf);
+        }
+        list_remove(node);
+        free(names);
+    }
+    return (ret == SSIZE_MAX) ? -ENOENT : ret;
+}
diff --git a/liblog/tests/liblog_test.cpp b/liblog/tests/liblog_test.cpp
index 97391d4..c25bdbb 100644
--- a/liblog/tests/liblog_test.cpp
+++ b/liblog/tests/liblog_test.cpp
@@ -2462,5 +2462,48 @@
     EXPECT_LT(0, __android_log_pmsg_file_write(
             LOG_ID_CRASH, ANDROID_LOG_VERBOSE,
             __pmsg_file, max_payload_buf, sizeof(max_payload_buf)));
-    fprintf(stderr, "Reboot, ensure file %s matches\n", __pmsg_file);
+    fprintf(stderr, "Reboot, ensure file %s matches\n"
+                    "with liblog.__android_log_msg_file_read test\n",
+                    __pmsg_file);
+}
+
+ssize_t __pmsg_fn(log_id_t logId, char prio, const char *filename,
+                  const char *buf, size_t len, void *arg) {
+    EXPECT_TRUE(NULL == arg);
+    EXPECT_EQ(LOG_ID_CRASH, logId);
+    EXPECT_EQ(ANDROID_LOG_VERBOSE, prio);
+    EXPECT_FALSE(NULL == strstr(__pmsg_file, filename));
+    EXPECT_EQ(len, sizeof(max_payload_buf));
+    EXPECT_EQ(0, strcmp(max_payload_buf, buf));
+
+    ++signaled;
+    if ((len != sizeof(max_payload_buf)) ||
+            strcmp(max_payload_buf, buf)) {
+        fprintf(stderr, "comparison fails on content \"%s\"\n", buf);
+    }
+    return !arg ||
+           (LOG_ID_CRASH != logId) ||
+           (ANDROID_LOG_VERBOSE != prio) ||
+           !strstr(__pmsg_file, filename) ||
+           (len != sizeof(max_payload_buf)) ||
+           !!strcmp(max_payload_buf, buf) ? -ENOEXEC : 1;
+}
+
+TEST(liblog, __android_log_pmsg_file_read) {
+    signaled = 0;
+
+    ssize_t ret = __android_log_pmsg_file_read(
+            LOG_ID_CRASH, ANDROID_LOG_VERBOSE,
+            __pmsg_file, __pmsg_fn, NULL);
+
+    if (ret == -ENOENT) {
+        fprintf(stderr,
+            "No pre-boot results of liblog.__android_log_mesg_file_write to "
+            "compare with,\n"
+            "false positive test result.\n");
+        return;
+    }
+
+    EXPECT_LT(0, ret);
+    EXPECT_EQ(1U, signaled);
 }