More missing _unlocked <stdio.h> functions.

Also simplify trivial one-liners like perror/puts/fputs, and clean up
fread/fwrite slightly.

Fix perror to match POSIX.

Add basic perror and *_unlocked tests.

Bug: N/A
Test: ran tests
Change-Id: I63f83c8e0c15c3c4096509d17421ac331b6fc23d
diff --git a/libc/stdio/stdio.cpp b/libc/stdio/stdio.cpp
index cf97a3f..c10bc00 100644
--- a/libc/stdio/stdio.cpp
+++ b/libc/stdio/stdio.cpp
@@ -91,7 +91,7 @@
 FILE* stdout = &__sF[1];
 FILE* stderr = &__sF[2];
 
-struct glue __sglue = { NULL, 3, __sF };
+struct glue __sglue = { nullptr, 3, __sF };
 static struct glue* lastglue = &__sglue;
 
 class ScopedFileLock {
@@ -116,7 +116,7 @@
   glue* g = reinterpret_cast<glue*>(data);
   FILE* p = reinterpret_cast<FILE*>(ALIGN(data + sizeof(*g)));
   __sfileext* pext = reinterpret_cast<__sfileext*>(ALIGN(data + sizeof(*g)) + n * sizeof(FILE));
-  g->next = NULL;
+  g->next = nullptr;
   g->niobs = n;
   g->iobs = p;
   while (--n >= 0) {
@@ -144,7 +144,7 @@
 	struct glue *g;
 
 	_THREAD_PRIVATE_MUTEX_LOCK(__sfp_mutex);
-	for (g = &__sglue; g != NULL; g = g->next) {
+	for (g = &__sglue; g != nullptr; g = g->next) {
 		for (fp = g->iobs, n = g->niobs; --n >= 0; fp++)
 			if (fp->_flags == 0)
 				goto found;
@@ -152,8 +152,7 @@
 
 	/* release lock while mallocing */
 	_THREAD_PRIVATE_MUTEX_UNLOCK(__sfp_mutex);
-	if ((g = moreglue(NDYNAMIC)) == NULL)
-		return (NULL);
+	if ((g = moreglue(NDYNAMIC)) == nullptr) return nullptr;
 	_THREAD_PRIVATE_MUTEX_LOCK(__sfp_mutex);
 	lastglue->next = g;
 	lastglue = g;
@@ -161,15 +160,15 @@
 found:
 	fp->_flags = 1;		/* reserve this slot; caller sets real flags */
 	_THREAD_PRIVATE_MUTEX_UNLOCK(__sfp_mutex);
-	fp->_p = NULL;		/* no current pointer */
+	fp->_p = nullptr;		/* no current pointer */
 	fp->_w = 0;		/* nothing to read or write */
 	fp->_r = 0;
-	fp->_bf._base = NULL;	/* no buffer */
+	fp->_bf._base = nullptr;	/* no buffer */
 	fp->_bf._size = 0;
 	fp->_lbfsize = 0;	/* not line buffered */
 	fp->_file = -1;		/* no file */
 
-	fp->_lb._base = NULL;	/* no line buffer */
+	fp->_lb._base = nullptr;	/* no line buffer */
 	fp->_lb._size = 0;
 	_FILEEXT_INIT(fp);
 
@@ -288,8 +287,8 @@
     // Flush the stream; ANSI doesn't require this.
     if (fp->_flags & __SWR) __sflush(fp);
 
-    // If close is NULL, closing is a no-op, hence pointless.
-    isopen = fp->_close != NULL;
+    // If close is null, closing is a no-op, hence pointless.
+    isopen = (fp->_close != nullptr);
     if ((wantfd = fp->_file) < 0 && isopen) {
         (*fp->_close)(fp->_cookie);
         isopen = 0;
@@ -316,8 +315,8 @@
   if (fp->_flags & __SMBF) free(fp->_bf._base);
   fp->_w = 0;
   fp->_r = 0;
-  fp->_p = NULL;
-  fp->_bf._base = NULL;
+  fp->_p = nullptr;
+  fp->_bf._base = nullptr;
   fp->_bf._size = 0;
   fp->_lbfsize = 0;
   if (HASUB(fp)) FREEUB(fp);
@@ -374,7 +373,7 @@
   ScopedFileLock sfl(fp);
   WCIO_FREE(fp);
   int r = fp->_flags & __SWR ? __sflush(fp) : 0;
-  if (fp->_close != NULL && (*fp->_close)(fp->_cookie) < 0) {
+  if (fp->_close != nullptr && (*fp->_close)(fp->_cookie) < 0) {
     r = EOF;
   }
   if (fp->_flags & __SMBF) free(fp->_bf._base);
@@ -438,6 +437,36 @@
   return ferror_unlocked(fp);
 }
 
+int __sflush(FILE* fp) {
+  // Flushing a read-only file is a no-op.
+  if ((fp->_flags & __SWR) == 0) return 0;
+
+  // Flushing a file without a buffer is a no-op.
+  unsigned char* p = fp->_bf._base;
+  if (p == nullptr) return 0;
+
+  // Set these immediately to avoid problems with longjmp and to allow
+  // exchange buffering (via setvbuf) in user write function.
+  int n = fp->_p - p;
+  fp->_p = p;
+  fp->_w = (fp->_flags & (__SLBF|__SNBF)) ? 0 : fp->_bf._size;
+
+  while (n > 0) {
+    int written = (*fp->_write)(fp->_cookie, reinterpret_cast<char*>(p), n);
+    if (written <= 0) {
+      fp->_flags |= __SERR;
+      return EOF;
+    }
+    n -= written, p += written;
+  }
+  return 0;
+}
+
+int __sflush_locked(FILE* fp) {
+  ScopedFileLock sfl(fp);
+  return __sflush(fp);
+}
+
 int __sread(void* cookie, char* buf, int n) {
   FILE* fp = reinterpret_cast<FILE*>(cookie);
   return TEMP_FAILURE_RETRY(read(fp->_file, buf, n));
@@ -495,7 +524,7 @@
     // smaller than that in the underlying object.
     result -= fp->_r;
     if (HASUB(fp)) result -= fp->_ur;
-  } else if (fp->_flags & __SWR && fp->_p != NULL) {
+  } else if (fp->_flags & __SWR && fp->_p != nullptr) {
     // Writing.  Any buffered characters cause the
     // position to be greater than that in the
     // underlying object.
@@ -527,7 +556,7 @@
     return -1;
   }
 
-  if (fp->_bf._base == NULL) __smakebuf(fp);
+  if (fp->_bf._base == nullptr) __smakebuf(fp);
 
   // Flush unwritten data and attempt the seek.
   if (__sflush(fp) || __seek_unlocked(fp, offset, whence) == -1) {
@@ -670,11 +699,90 @@
   return getc(fp);
 }
 
+int fgetc_unlocked(FILE* fp) {
+  CHECK_FP(fp);
+  return getc_unlocked(fp);
+}
+
+/*
+ * Read at most n-1 characters from the given file.
+ * Stop when a newline has been read, or the count runs out.
+ * Return first argument, or NULL if no characters were read.
+ * Do not return NULL if n == 1.
+ */
+char* fgets(char* buf, int n, FILE* fp) __overloadable {
+  CHECK_FP(fp);
+  ScopedFileLock sfl(fp);
+  return fgets_unlocked(buf, n, fp);
+}
+
+char* fgets_unlocked(char* buf, int n, FILE* fp) {
+  if (n <= 0) {
+    errno = EINVAL;
+    return nullptr;
+  }
+
+  _SET_ORIENTATION(fp, -1);
+
+  char* s = buf;
+  n--; // Leave space for NUL.
+  while (n != 0) {
+    // If the buffer is empty, refill it.
+    if (fp->_r <= 0) {
+      if (__srefill(fp)) {
+        // EOF/error: stop with partial or no line.
+        if (s == buf) return nullptr;
+        break;
+      }
+    }
+    size_t len = fp->_r;
+    unsigned char* p = fp->_p;
+
+    // Scan through at most n bytes of the current buffer,
+    // looking for '\n'.  If found, copy up to and including
+    // newline, and stop.  Otherwise, copy entire chunk and loop.
+    if (len > static_cast<size_t>(n)) len = n;
+    unsigned char* t = static_cast<unsigned char*>(memchr(p, '\n', len));
+    if (t != nullptr) {
+      len = ++t - p;
+      fp->_r -= len;
+      fp->_p = t;
+      memcpy(s, p, len);
+      s[len] = '\0';
+      return buf;
+    }
+    fp->_r -= len;
+    fp->_p += len;
+    memcpy(s, p, len);
+    s += len;
+    n -= len;
+  }
+  *s = '\0';
+  return buf;
+}
+
 int fputc(int c, FILE* fp) {
   CHECK_FP(fp);
   return putc(c, fp);
 }
 
+int fputc_unlocked(int c, FILE* fp) {
+  CHECK_FP(fp);
+  return putc_unlocked(c, fp);
+}
+
+int fputs(const char* s, FILE* fp) {
+  CHECK_FP(fp);
+  ScopedFileLock sfl(fp);
+  return fputs_unlocked(s, fp);
+}
+
+int fputs_unlocked(const char* s, FILE* fp) {
+  CHECK_FP(fp);
+  size_t length = strlen(s);
+  return (fwrite_unlocked(s, 1, length, fp) == length) ? 0 : EOF;
+}
+
 int fscanf(FILE* fp, const char* fmt, ...) {
   CHECK_FP(fp);
   PRINTF_IMPL(vfscanf(fp, fmt, ap));
@@ -723,6 +831,11 @@
   return fgetwc(stdin);
 }
 
+void perror(const char* msg) {
+  if (msg == nullptr) msg = "";
+  fprintf(stderr, "%s%s%s\n", msg, (*msg == '\0') ? "" : ": ", strerror(errno));
+}
+
 int printf(const char* fmt, ...) {
   PRINTF_IMPL(vfprintf(stdout, fmt, ap));
 }
@@ -754,6 +867,13 @@
   return putc_unlocked(c, stdout);
 }
 
+int puts(const char* s) {
+  size_t length = strlen(s);
+  ScopedFileLock sfl(stdout);
+  return (fwrite_unlocked(s, 1, length, stdout) == length &&
+          putc_unlocked('\n', stdout) != EOF) ? 0 : EOF;
+}
+
 wint_t putwc(wchar_t wc, FILE* fp) {
   CHECK_FP(fp);
   return fputwc(wc, fp);
@@ -869,6 +989,116 @@
   PRINTF_IMPL(vfwscanf(stdin, fmt, ap));
 }
 
+static int fflush_all() {
+  return _fwalk(__sflush_locked);
+}
+
+int fflush(FILE* fp) {
+  if (fp == nullptr) return fflush_all();
+  ScopedFileLock sfl(fp);
+  return fflush_unlocked(fp);
+}
+
+int fflush_unlocked(FILE* fp) {
+  if (fp == nullptr) return fflush_all();
+  if ((fp->_flags & (__SWR | __SRW)) == 0) {
+    errno = EBADF;
+    return EOF;
+  }
+  return __sflush(fp);
+}
+
+size_t fread(void* buf, size_t size, size_t count, FILE* fp) __overloadable {
+  CHECK_FP(fp);
+  ScopedFileLock sfl(fp);
+  return fread_unlocked(buf, size, count, fp);
+}
+
+size_t fread_unlocked(void* buf, size_t size, size_t count, FILE* fp) {
+  CHECK_FP(fp);
+
+  size_t desired_total;
+  if (__builtin_mul_overflow(size, count, &desired_total)) {
+    errno = EOVERFLOW;
+    fp->_flags |= __SERR;
+    return 0;
+  }
+
+  size_t total = desired_total;
+  if (total == 0) return 0;
+
+  _SET_ORIENTATION(fp, -1);
+
+  // TODO: how can this ever happen?!
+  if (fp->_r < 0) fp->_r = 0;
+
+  // Ensure _bf._size is valid.
+  if (fp->_bf._base == nullptr) __smakebuf(fp);
+
+  char* dst = static_cast<char*>(buf);
+
+  while (total > 0) {
+    // Copy data out of the buffer.
+    size_t buffered_bytes = MIN(static_cast<size_t>(fp->_r), total);
+    memcpy(dst, fp->_p, buffered_bytes);
+    fp->_p += buffered_bytes;
+    fp->_r -= buffered_bytes;
+    dst += buffered_bytes;
+    total -= buffered_bytes;
+
+    // Are we done?
+    if (total == 0) goto out;
+
+    // Do we have so much more to read that we should avoid copying it through the buffer?
+    if (total > static_cast<size_t>(fp->_bf._size)) break;
+
+    // Less than a buffer to go, so refill the buffer and go around the loop again.
+    if (__srefill(fp)) goto out;
+  }
+
+  // Read directly into the caller's buffer.
+  while (total > 0) {
+    ssize_t bytes_read = (*fp->_read)(fp->_cookie, dst, total);
+    if (bytes_read <= 0) {
+      fp->_flags |= (bytes_read == 0) ? __SEOF : __SERR;
+      break;
+    }
+    dst += bytes_read;
+    total -= bytes_read;
+  }
+
+out:
+  return ((desired_total - total) / size);
+}
+
+size_t fwrite(const void* buf, size_t size, size_t count, FILE* fp) {
+  CHECK_FP(fp);
+  ScopedFileLock sfl(fp);
+  return fwrite_unlocked(buf, size, count, fp);
+}
+
+size_t fwrite_unlocked(const void* buf, size_t size, size_t count, FILE* fp) {
+  CHECK_FP(fp);
+
+  size_t n;
+  if (__builtin_mul_overflow(size, count, &n)) {
+    errno = EOVERFLOW;
+    fp->_flags |= __SERR;
+    return 0;
+  }
+
+  if (n == 0) return 0;
+
+  __siov iov = { .iov_base = const_cast<void*>(buf), .iov_len = n };
+  __suio uio = { .uio_iov = &iov, .uio_iovcnt = 1, .uio_resid = n };
+
+  _SET_ORIENTATION(fp, -1);
+
+  // The usual case is success (__sfvwrite returns 0); skip the divide if this happens,
+  // since divides are generally slow.
+  return (__sfvwrite(fp, &uio) == 0) ? count : ((n - uio.uio_resid) / size);
+}
+
 namespace {
 
 namespace phony {