libc: minimal: add strerror and strerror_r function

Add simple strerror() and strerror_r() implementations.

Fixes #46099

Signed-off-by: Christopher Friedt <cfriedt@fb.com>
diff --git a/lib/libc/Kconfig b/lib/libc/Kconfig
index ed06e7c..d4124e1 100644
--- a/lib/libc/Kconfig
+++ b/lib/libc/Kconfig
@@ -217,6 +217,17 @@
 	  In order to make use of the non-reentrant gmtime(), it is necessary
 	  to set CONFIG_MINIMAL_LIBC_NON_REENTRANT_FUNCTIONS=y.
 
+config MINIMAL_LIBC_STRING_ERROR_TABLE
+	bool "String error table for strerror() and strerror_r()"
+	help
+	  Select this option to ensure that streror(), strerror_r()
+	  produce strings corresponding to the descriptions in errno.h.
+
+	  The string error table can add ~2kiB to ROM. As such, it is
+	  disabled by default. In this case, strerror() and strerror_r()
+	  symbols are still present, but the functions produce an empty
+	  string.
+
 endif # MINIMAL_LIBC
 
 config STDOUT_CONSOLE
diff --git a/lib/libc/minimal/CMakeLists.txt b/lib/libc/minimal/CMakeLists.txt
index 6fd36e7..6d857fe 100644
--- a/lib/libc/minimal/CMakeLists.txt
+++ b/lib/libc/minimal/CMakeLists.txt
@@ -3,6 +3,10 @@
 zephyr_system_include_directories(include)
 
 zephyr_library()
+
+set(GEN_DIR ${ZEPHYR_BINARY_DIR}/include/generated)
+set(STRERROR_TABLE_H ${GEN_DIR}/libc/minimal/strerror_table.h)
+
 zephyr_library_sources(
   source/stdlib/abort.c
   source/stdlib/atoi.c
@@ -14,6 +18,7 @@
   source/stdlib/bsearch.c
   source/stdlib/exit.c
   source/stdlib/qsort.c
+  source/string/strerror.c
   source/string/strncasecmp.c
   source/string/strstr.c
   source/string/string.c
@@ -23,6 +28,7 @@
   source/stdout/fprintf.c
   source/math/sqrtf.c
   source/math/sqrt.c
+  ${STRERROR_TABLE_H}
 )
 
 if(CONFIG_MINIMAL_LIBC_TIME)
@@ -31,3 +37,14 @@
 endif()
 
 zephyr_library_sources_ifdef(CONFIG_MINIMAL_LIBC_RAND source/stdlib/rand.c)
+
+add_custom_command(
+  OUTPUT ${STRERROR_TABLE_H}
+  COMMAND
+  ${PYTHON_EXECUTABLE}
+  ${ZEPHYR_BASE}/scripts/gen_strerror_table.py
+  -i include/errno.h
+  -o ${STRERROR_TABLE_H}
+  DEPENDS include/errno.h
+  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
diff --git a/lib/libc/minimal/include/string.h b/lib/libc/minimal/include/string.h
index ecf834d..35052c0 100644
--- a/lib/libc/minimal/include/string.h
+++ b/lib/libc/minimal/include/string.h
@@ -17,6 +17,8 @@
 #endif
 
 extern char  *strcpy(char *ZRESTRICT d, const char *ZRESTRICT s);
+extern char  *strerror(int errnum);
+extern int   strerror_r(int errnum, char *strerrbuf, size_t buflen);
 extern char  *strncpy(char *ZRESTRICT d, const char *ZRESTRICT s,
 		      size_t n);
 extern char  *strchr(const char *s, int c);
diff --git a/lib/libc/minimal/source/string/strerror.c b/lib/libc/minimal/source/string/strerror.c
new file mode 100644
index 0000000..ae2c02f
--- /dev/null
+++ b/lib/libc/minimal/source/string/strerror.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 Meta
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <errno.h>
+#include <string.h>
+
+#include <zephyr/sys/util.h>
+
+/*
+ * See scripts/gen_strerror_table.py
+ *
+ * #define sys_nerr N
+ * const char *const sys_errlist[sys_nerr];
+ * const uint8_t sys_errlen[sys_nerr];
+ */
+#include "libc/minimal/strerror_table.h"
+
+/*
+ * See https://pubs.opengroup.org/onlinepubs/9699919799/functions/strerror.html
+ */
+char *strerror(int errnum)
+{
+	if (IS_ENABLED(CONFIG_MINIMAL_LIBC_STRING_ERROR_TABLE) &&
+	    errnum >= 0 && errnum < sys_nerr) {
+		return (char *)sys_errlist[errnum];
+	}
+
+	return "";
+}
+
+/*
+ * See
+ * https://pubs.opengroup.org/onlinepubs/9699919799/functions/strerror_r.html
+ */
+int strerror_r(int errnum, char *strerrbuf, size_t buflen)
+{
+	const char *msg;
+	size_t msg_len;
+
+	if (errnum >= 0 && errnum < sys_nerr) {
+		if (IS_ENABLED(CONFIG_MINIMAL_LIBC_STRING_ERROR_TABLE)) {
+			msg = sys_errlist[errnum];
+			msg_len = sys_errlen[errnum];
+		} else {
+			msg = "";
+			msg_len = 1;
+		}
+
+		if (buflen < msg_len) {
+			return ERANGE;
+		}
+
+		strncpy(strerrbuf, msg, msg_len);
+	}
+
+	if (errnum < 0 || errnum >= sys_nerr) {
+		return EINVAL;
+	}
+
+	return 0;
+}
diff --git a/scripts/gen_strerror_table.py b/scripts/gen_strerror_table.py
new file mode 100755
index 0000000..4e04d79
--- /dev/null
+++ b/scripts/gen_strerror_table.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2022 Meta
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import argparse
+import os
+import re
+
+
+def front_matter(sys_nerr):
+    return f'''
+/*
+ * This file generated by {__file__}
+ */
+
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <zephyr/sys/util.h>
+
+#define sys_nerr {sys_nerr}'''
+
+
+def gen_strerror_table(input, output):
+    with open(input, 'r') as inf:
+
+        highest_errno = 0
+        symbols = []
+        msgs = {}
+
+        for line in inf.readlines():
+            # Select items of the form below (note: ERRNO is numeric)
+            # #define SYMBOL ERRNO /**< MSG */
+            pat = r'^#define[\s]+(E[A-Z_]*)[\s]+([1-9][0-9]*)[\s]+/\*\*<[\s]+(.*)[\s]+\*/[\s]*$'
+            match = re.match(pat, line)
+
+            if not match:
+                continue
+
+            symbol = match[1]
+            errno = int(match[2])
+            msg = match[3]
+
+            symbols.append(symbol)
+            msgs[symbol] = msg
+
+            highest_errno = max(int(errno), highest_errno)
+
+        try:
+            os.makedirs(os.path.dirname(output))
+        except BaseException:
+            # directory already present
+            pass
+
+        with open(output, 'w') as outf:
+
+            print(front_matter(highest_errno + 1), file=outf)
+
+            # Generate string table
+            print(
+                f'static const char *const sys_errlist[sys_nerr] = {{', file=outf)
+            print('[0] = "Success",', file=outf)
+            for symbol in symbols:
+                print(f'[{symbol}] = "{msgs[symbol]}",', file=outf)
+
+            print('};', file=outf)
+
+            # Generate string lengths (includes trailing '\0')
+            print(
+                f'static const uint8_t sys_errlen[sys_nerr] = {{', file=outf)
+            print('[0] = 8,', file=outf)
+            for symbol in symbols:
+                print(f'[{symbol}] = {len(msgs[symbol]) + 1},', file=outf)
+
+            print('};', file=outf)
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        '-i',
+        '--input',
+        dest='input',
+        required=True,
+        help='input file (e.g. lib/libc/minimal/include/errno.h)')
+    parser.add_argument(
+        '-o',
+        '--output',
+        dest='output',
+        required=True,
+        help='output file (e.g. build/zephyr/misc/generated/libc/minimal/strerror_table.h)')
+
+    args = parser.parse_args()
+
+    return args
+
+
+def main():
+    args = parse_args()
+    gen_strerror_table(args.input, args.output)
+
+
+if __name__ == '__main__':
+    main()