aboutsummaryrefslogtreecommitdiffstats
path: root/filters/colorize.c
diff options
context:
space:
mode:
Diffstat (limited to 'filters/colorize.c')
-rw-r--r--filters/colorize.c681
1 files changed, 681 insertions, 0 deletions
diff --git a/filters/colorize.c b/filters/colorize.c
new file mode 100644
index 00000000..17eb548a
--- /dev/null
+++ b/filters/colorize.c
@@ -0,0 +1,681 @@
+/* SPDX-License-Identifier: MIT */
+/* Copyright (c) 2023 Robin Jarry */
+
+#include <errno.h>
+#include <fnmatch.h>
+#include <getopt.h>
+#include <regex.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void usage(void)
+{
+ puts("usage: colorize [-h] [-s FILE] [-f FILE]");
+ puts("");
+ puts("options:");
+ puts(" -h show this help message");
+ puts(" -s FILE use styleset file (default $AERC_STYLESET)");
+ puts(" -f FILE read from filename (default stdin)");
+}
+
+enum color_type {
+ NONE = 0,
+ DEFAULT,
+ RGB,
+ PALETTE,
+};
+
+struct color {
+ enum color_type type;
+ uint32_t rgb;
+ uint32_t index;
+};
+
+struct style {
+ struct color fg;
+ struct color bg;
+ int bold;
+ int blink;
+ int underline;
+ int reverse;
+ int italic;
+ int dim;
+ char *sequence;
+};
+
+__attribute__((malloc,returns_nonnull))
+static void *xmalloc(size_t s)
+{
+ void *ptr = malloc(s);
+ if (ptr == NULL) {
+ perror("fatal: cannot allocate buffer");
+ abort();
+ }
+ return ptr;
+}
+
+#define BOLD "\x1b[1m"
+#define RESET "\x1b[0m"
+#define LONGEST_SEQ "\x1b[1;2;3;4;5;7;38;2;255;255;255;48;2;255;255;255m"
+
+const char *seq(struct style *s) {
+ if (!s->sequence) {
+ char *b, *buf = xmalloc(strlen(LONGEST_SEQ) + 1);
+ const char *sep = "";
+
+ b = buf;
+ b += sprintf(b, "%s", "\x1b[");
+ if (s->bold) {
+ b += sprintf(b, "%s1", sep);
+ sep = ";";
+ }
+ if (s->dim) {
+ b += sprintf(b, "%s2", sep);
+ sep = ";";
+ }
+ if (s->italic) {
+ b += sprintf(b, "%s3", sep);
+ sep = ";";
+ }
+ if (s->underline) {
+ b += sprintf(b, "%s4", sep);
+ sep = ";";
+ }
+ if (s->blink) {
+ b += sprintf(b, "%s5", sep);
+ sep = ";";
+ }
+ if (s->reverse) {
+ b += sprintf(b, "%s7", sep);
+ sep = ";";
+ }
+ switch (s->fg.type) {
+ case NONE:
+ break;
+ case DEFAULT:
+ b += sprintf(b, "%s39", sep);
+ break;
+ case RGB:
+ b += sprintf(b, "%s38;2;%d;%d;%d", sep,
+ (s->fg.rgb >> 16) & 0xff,
+ (s->fg.rgb >> 8) & 0xff,
+ s->fg.rgb & 0xff);
+ sep = ";";
+ break;
+ case PALETTE:
+ b += sprintf(b, (s->fg.index < 8) ?
+ "%s3%d" : "%s38;5;%d", sep, s->fg.index);
+ sep = ";";
+ break;
+ }
+ switch (s->bg.type) {
+ case NONE:
+ break;
+ case DEFAULT:
+ b += sprintf(b, "%s49", sep);
+ break;
+ case RGB:
+ b += sprintf(b, "%s48;2;%d;%d;%d", sep,
+ (s->bg.rgb >> 16) & 0xff,
+ (s->bg.rgb >> 8) & 0xff,
+ s->bg.rgb & 0xff);
+ break;
+ case PALETTE:
+ b += sprintf(b, (s->bg.index < 8) ?
+ "%s4%d" : "%s48;5;%d", sep, s->bg.index);
+ break;
+ }
+ if (strcmp(buf, "\x1b[") == 0) {
+ b += sprintf(b, "0");
+ }
+ sprintf(b, "m");
+ s->sequence = buf;
+ }
+ return s->sequence;
+}
+
+struct styles {
+ struct style url;
+ struct style header;
+ struct style signature;
+ struct style diff_meta;
+ struct style diff_chunk;
+ struct style diff_add;
+ struct style diff_del;
+ struct style quote_1;
+ struct style quote_2;
+ struct style quote_3;
+ struct style quote_4;
+ struct style quote_x;
+};
+
+static FILE *in_file;
+static const char *styleset;
+static struct styles styles = {
+ .url = { .underline = 1 },
+ .header = { .bold = 1 },
+ .signature = { .dim = 1 },
+ .diff_meta = { .bold = 1 },
+ .diff_chunk = { .dim = 1 },
+ .diff_add = { .fg = { .type = PALETTE, .index = 2 } },
+ .diff_del = { .fg = { .type = PALETTE, .index = 1 } },
+ .quote_1 = { .fg = { .type = PALETTE, .index = 6 } },
+ .quote_2 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 },
+ .quote_3 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 },
+ .quote_4 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 },
+ .quote_x = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 },
+};
+
+static inline int startswith(const char *s, const char *prefix)
+{
+ return !strncmp(s, prefix, strlen(prefix));
+}
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+static struct { const char *n; uint32_t c; } color_names[] = {
+ {"aliceblue", 0xf0f8ff}, {"antiquewhite", 0xfaebd7}, {"aqua", 0x00ffff},
+ {"aquamarine", 0x7fffd4}, {"azure", 0xf0ffff}, {"beige", 0xf5f5dc},
+ {"bisque", 0xffe4c4}, {"black", 0x000000}, {"blanchedalmond", 0xffebcd},
+ {"blue", 0x0000ff}, {"blueviolet", 0x8a2be2}, {"brown", 0xa52a2a},
+ {"burlywood", 0xdeb887}, {"cadetblue", 0x5f9ea0}, {"chartreuse", 0x7fff00},
+ {"chocolate", 0xd2691e}, {"coral", 0xff7f50}, {"cornflowerblue", 0x6495ed},
+ {"cornsilk", 0xfff8dc}, {"crimson", 0xdc143c}, {"darkblue", 0x00008b},
+ {"darkcyan", 0x008b8b}, {"darkgoldenrod", 0xb8860b}, {"darkgray", 0xa9a9a9},
+ {"darkgreen", 0x006400}, {"darkkhaki", 0xbdb76b}, {"darkmagenta", 0x8b008b},
+ {"darkolivegreen", 0x556b2f}, {"darkorange", 0xff8c00}, {"darkorchid", 0x9932cc},
+ {"darkred", 0x8b0000}, {"darksalmon", 0xe9967a}, {"darkseagreen", 0x8fbc8f},
+ {"darkslateblue", 0x483d8b}, {"darkslategray", 0x2f4f4f}, {"darkturquoise", 0x00ced1},
+ {"darkviolet", 0x9400d3}, {"deeppink", 0xff1493}, {"deepskyblue", 0x00bfff},
+ {"dimgray", 0x696969}, {"dodgerblue", 0x1e90ff}, {"firebrick", 0xb22222},
+ {"floralwhite", 0xfffaf0}, {"forestgreen", 0x228b22}, {"fuchsia", 0xff00ff},
+ {"gainsboro", 0xdcdcdc}, {"ghostwhite", 0xf8f8ff}, {"gold", 0xffd700},
+ {"goldenrod", 0xdaa520}, {"gray", 0x808080}, {"green", 0x008000},
+ {"greenyellow", 0xadff2f}, {"honeydew", 0xf0fff0}, {"hotpink", 0xff69b4},
+ {"indianred", 0xcd5c5c}, {"indigo", 0x4b0082}, {"ivory", 0xfffff0},
+ {"khaki", 0xf0e68c}, {"lavender", 0xe6e6fa}, {"lavenderblush", 0xfff0f5},
+ {"lawngreen", 0x7cfc00}, {"lemonchiffon", 0xfffacd}, {"lightblue", 0xadd8e6},
+ {"lightcoral", 0xf08080}, {"lightcyan", 0xe0ffff}, {"lightgoldenrodyellow", 0xfafad2},
+ {"lightgray", 0xd3d3d3}, {"lightgreen", 0x90ee90}, {"lightpink", 0xffb6c1},
+ {"lightsalmon", 0xffa07a}, {"lightseagreen", 0x20b2aa}, {"lightskyblue", 0x87cefa},
+ {"lightslategray", 0x778899}, {"lightsteelblue", 0xb0c4de}, {"lightyellow", 0xffffe0},
+ {"lime", 0x00ff00}, {"limegreen", 0x32cd32}, {"linen", 0xfaf0e6},
+ {"maroon", 0x800000}, {"mediumaquamarine", 0x66cdaa}, {"mediumblue", 0x0000cd},
+ {"mediumorchid", 0xba55d3}, {"mediumpurple", 0x9370db}, {"mediumseagreen", 0x3cb371},
+ {"mediumslateblue", 0x7b68ee}, {"mediumspringgreen", 0x00fa9a}, {"mediumturquoise", 0x48d1cc},
+ {"mediumvioletred", 0xc71585}, {"midnightblue", 0x191970}, {"mintcream", 0xf5fffa},
+ {"mistyrose", 0xffe4e1}, {"moccasin", 0xffe4b5}, {"navajowhite", 0xffdead},
+ {"navy", 0x000080}, {"oldlace", 0xfdf5e6}, {"olive", 0x808000},
+ {"olivedrab", 0x6b8e23}, {"orange", 0xffa500}, {"orangered", 0xff4500},
+ {"orchid", 0xda70d6}, {"palegoldenrod", 0xeee8aa}, {"palegreen", 0x98fb98},
+ {"paleturquoise", 0xafeeee}, {"palevioletred", 0xdb7093}, {"papayawhip", 0xffefd5},
+ {"peachpuff", 0xffdab9}, {"peru", 0xcd853f}, {"pink", 0xffc0cb},
+ {"plum", 0xdda0dd}, {"powderblue", 0xb0e0e6}, {"purple", 0x800080},
+ {"rebeccapurple", 0x663399}, {"red", 0xff0000}, {"rosybrown", 0xbc8f8f},
+ {"royalblue", 0x4169e1}, {"saddlebrown", 0x8b4513}, {"salmon", 0xfa8072},
+ {"sandybrown", 0xf4a460}, {"seagreen", 0x2e8b57}, {"seashell", 0xfff5ee},
+ {"sienna", 0xa0522d}, {"silver", 0xc0c0c0}, {"skyblue", 0x87ceeb},
+ {"slateblue", 0x6a5acd}, {"slategray", 0x708090}, {"snow", 0xfffafa},
+ {"springgreen", 0x00ff7f}, {"steelblue", 0x4682b4}, {"tan", 0xd2b48c},
+ {"teal", 0x008080}, {"thistle", 0xd8bfd8}, {"tomato", 0xff6347},
+ {"turquoise", 0x40e0d0}, {"violet", 0xee82ee}, {"wheat", 0xf5deb3},
+ {"white", 0xffffff}, {"whitesmoke", 0xf5f5f5}, {"yellow", 0xffff00},
+ {"yellowgreen", 0x9acd32},
+};
+
+static int color_name(const char *name, uint32_t *color)
+{
+ for (size_t c = 0; c < ARRAY_SIZE(color_names); c++) {
+ if (!strcmp(name, color_names[c].n)) {
+ *color = color_names[c].c;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int parse_color(struct color *c, const char *val)
+{
+ uint32_t color = 0;
+ if (!strcmp(val, "default")) {
+ c->type = DEFAULT;
+ } else if (sscanf(val, "#%x", &color) == 1 && color <= 0xffffff) {
+ c->type = RGB;
+ c->rgb = color;
+ } else if (sscanf(val, "%d", &color) == 1 && color <= 256) {
+ c->type = PALETTE;
+ c->index = color;
+ } else if (!color_name(val, &color)) {
+ c->type = RGB;
+ c->rgb = color;
+ } else {
+ fprintf(stderr, "error: invalid color value %s\n", val);
+ return 1;
+ }
+ return 0;
+}
+
+static int parse_bool(int *b, const char *val)
+{
+ if (!strcmp(val, "true")) {
+ *b = 1;
+ } else if (!strcmp(val, "false")) {
+ *b = 0;
+ } else if (!strcmp(val, "toggle")) {
+ *b = !*b;
+ } else {
+ fprintf(stderr, "error: invalid bool value %s\n", val);
+ return 1;
+ }
+ return 0;
+}
+
+static int set_attr(struct style *s, const char *attr, const char *val)
+{
+ if (!strcmp(attr, "fg")) {
+ if (parse_color(&s->fg, val))
+ return 1;
+ } else if (!strcmp(attr, "bg")) {
+ if (parse_color(&s->fg, val))
+ return 1;
+ } else if (!strcmp(attr, "bold")) {
+ if (parse_bool(&s->bold, val))
+ return 1;
+ } else if (!strcmp(attr, "blink")) {
+ if (parse_bool(&s->blink, val))
+ return 1;
+ } else if (!strcmp(attr, "underline")) {
+ if (parse_bool(&s->underline, val))
+ return 1;
+ } else if (!strcmp(attr, "reverse")) {
+ if (parse_bool(&s->reverse, val))
+ return 1;
+ } else if (!strcmp(attr, "italic")) {
+ if (parse_bool(&s->italic, val))
+ return 1;
+ } else if (!strcmp(attr, "dim")) {
+ if (parse_bool(&s->dim, val))
+ return 1;
+ } else if (!strcmp(attr, "normal")) {
+ s->bold = 0;
+ s->underline = 0;
+ s->reverse = 0;
+ s->italic = 0;
+ s->dim = 0;
+ } else if (!strcmp(attr, "default")) {
+ s->fg.type = NONE;
+ s->fg.type = NONE;
+ } else {
+ fprintf(stderr, "error: invalid style attribute %s\n", attr);
+ return 1;
+ }
+ return 0;
+}
+
+static struct {const char *n; struct style *s;} ini_objects[] = {
+ {"url", &styles.url},
+ {"header", &styles.header},
+ {"signature", &styles.signature},
+ {"diff_meta", &styles.diff_meta},
+ {"diff_chunk", &styles.diff_chunk},
+ {"diff_add", &styles.diff_add},
+ {"diff_del", &styles.diff_del},
+ {"quote_1", &styles.quote_1},
+ {"quote_2", &styles.quote_2},
+ {"quote_3", &styles.quote_3},
+ {"quote_4", &styles.quote_4},
+ {"quote_x", &styles.quote_x},
+};
+
+static int parse_styleset(void)
+{
+ int in_section = 0;
+ char buf[BUFSIZ];
+ int err = 0;
+ FILE *f;
+
+ if (!styleset)
+ return 0;
+
+ f = fopen(styleset, "r");
+ if (!f) {
+ perror("error: failed to open styleset");
+ return 1;
+ }
+
+ while (fgets(buf, sizeof(buf), f)) {
+ /* strip LF, CR, CRLF, LFCR */
+ buf[strcspn(buf, "\r\n")] = '\0';
+ if (in_section) {
+ char obj[64], attr[64], val[64];
+ int changed = 0;
+
+ if (sscanf(buf, "%63[^.].%63[^=] = %63s", obj, attr, val) != 3)
+ continue;
+
+ for (size_t o = 0; o < ARRAY_SIZE(ini_objects); o++) {
+ if (fnmatch(obj, ini_objects[o].n, 0))
+ continue;
+ if (set_attr(ini_objects[o].s, attr, val)) {
+ err = 1;
+ goto end;
+ }
+ changed++;
+ }
+ if (!changed) {
+ fprintf(stderr,
+ "error: unknown style object %s\n",
+ obj);
+ err = 1;
+ goto end;
+ }
+ } else if (!strcmp(buf, "[viewer]")) {
+ in_section = 1;
+ /* only disable the default theme if there is
+ * a [viewer] section in the styleset */
+ memset(&styles, 0, sizeof(styles));
+ }
+ }
+
+end:
+ fclose(f);
+ return err;
+}
+
+static inline void print(const char *in)
+{
+ fputs(in, stdout);
+}
+
+static inline size_t print_notabs(const char *in, size_t max_len)
+{
+ size_t len = 0;
+ while (*in != '\0' && len < max_len) {
+ char c = *in++;
+ if (c == '\t') {
+ /* Tabs are interpreted as cursor movement and are not
+ * colored like regular characters. Replace them with
+ * 8 spaces. */
+ fputs(" ", stdout);
+ } else {
+ fputc(c, stdout);
+ }
+ len++;
+ }
+ return len;
+}
+
+static void diff_chunk(const char *in)
+{
+ size_t len = 0;
+ print(seq(&styles.diff_chunk));
+ while (in[len] == '@')
+ len++;
+ while (in[len] != '\0' && in[len] != '@')
+ len++;
+ while (in[len] == '@')
+ len++;
+ in += print_notabs(in, len);
+ print(RESET);
+ print_notabs(in, BUFSIZ);
+}
+
+#define URL_RE \
+ "[a-z]{2,8}://[[:graph:]]{4,}" \
+ "|(mailto:)?[[:alnum:]_\\+\\.~/-]*[[:alnum:]]@[a-z][[:alnum:]\\.-]*[a-z]"
+static regex_t url_re;
+
+static void urls(const char *in, struct style *ctx)
+{
+ regmatch_t groups[2];
+ size_t len;
+ int trim;
+
+ while (!regexec(&url_re, in, 2, groups, 0)) {
+ in += print_notabs(in, groups[0].rm_so);
+ print(seq(&styles.url));
+ len = groups[0].rm_eo - groups[0].rm_so;
+ /* Heuristic to remove trailing characters that are valid URL
+ * characters, but typically not at the end of the URL */
+ trim = 1;
+ while (trim && len > 0) {
+ switch (in[len - 1]) {
+ case '>': case '.': case ',': case ';': case ')':
+ case '!': case '?': case '"': case '\'':
+ len--;
+ break;
+ default:
+ trim = 0;
+ break;
+ }
+ }
+ in += print_notabs(in, len);
+ print(RESET);
+ if (ctx) {
+ print(seq(ctx));
+ }
+ }
+ print_notabs(in, BUFSIZ);
+}
+
+static inline void signature(const char *in)
+{
+ print(seq(&styles.signature));
+ urls(in, &styles.signature);
+ print(RESET);
+}
+
+#define HEADER_RE "^[A-Z][[:alnum:]_-]+:"
+static regex_t header_re;
+
+static void header(const char *in)
+{
+ regmatch_t groups[1];
+
+ if (!regexec(&header_re, in, 1, groups, 0)) {
+ print(seq(&styles.header));
+ in += print_notabs(in, groups[0].rm_eo);
+ print(RESET);
+ }
+ urls(in, NULL);
+}
+
+#define DIFF_META_RE \
+ "^(diff --git|(new|deleted) file|similarity" \
+ " index|(rename|copy) (to|from)|index|---|\\+\\+\\+) "
+static regex_t diff_meta_re;
+
+static void quote(const char *in)
+{
+ regmatch_t groups[8];
+ struct style *s;
+ int q, level;
+
+ q = level = 0;
+ while (in[q] == '>') {
+ level++;
+ q++;
+ if (in[q] == ' ')
+ q++;
+ }
+ switch (level) {
+ case 1:
+ s = &styles.quote_1;
+ break;
+ case 2:
+ s = &styles.quote_2;
+ break;
+ case 3:
+ s = &styles.quote_3;
+ break;
+ case 4:
+ s = &styles.quote_4;
+ break;
+ default:
+ s = &styles.quote_x;
+ break;
+ }
+
+ print(seq(s));
+ in += print_notabs(in, q);
+ if (startswith(in, "+")) {
+ printf("%s%s", RESET, seq(&styles.diff_add));
+ print_notabs(in, BUFSIZ);
+ } else if (startswith(in, "-")) {
+ printf("%s%s", RESET, seq(&styles.diff_del));
+ print_notabs(in, BUFSIZ);
+ } else if (!regexec(&diff_meta_re, in, 8, groups, 0)) {
+ print(BOLD);
+ print_notabs(in, BUFSIZ);
+ } else {
+ urls(in, s);
+ }
+ print(RESET);
+}
+
+static void print_style(const char *in, struct style *s)
+{
+ print(seq(s));
+ print_notabs(in, BUFSIZ);
+ print(RESET);
+}
+
+enum state { INIT, DIFF, SIGNATURE, BODY };
+
+static void colorize_line(const char *in)
+{
+ static enum state state = INIT;
+ regmatch_t groups[8]; /* enough groups to cover all expressions */
+
+ switch (state) {
+ case DIFF:
+ if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
+ state = SIGNATURE;
+ signature(in);
+ } else if (startswith(in, "@@ ")) {
+ diff_chunk(in);
+ } else if (!regexec(&diff_meta_re, in, 8, groups, 0)) {
+ print_style(in, &styles.diff_meta);
+ } else if (startswith(in, "+")) {
+ print_style(in, &styles.diff_add);
+ } else if (startswith(in, "-")) {
+ print_style(in, &styles.diff_del);
+ } else if (!startswith(in, " ") && strcmp(in, "") != 0) {
+ state = BODY;
+ if (startswith(in, ">")) {
+ quote(in);
+ } else {
+ urls(in, NULL);
+ }
+ } else {
+ print_notabs(in, BUFSIZ);
+ }
+ break;
+ case SIGNATURE:
+ signature(in);
+ break;
+ case BODY:
+ if (startswith(in, ">")) {
+ quote(in);
+ } else if (startswith(in, "diff --git ")) {
+ state = DIFF;
+ print_style(in, &styles.diff_meta);
+ } else if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
+ state = SIGNATURE;
+ signature(in);
+ } else if (!regexec(&header_re, in, 8, groups, 0)) {
+ header(in);
+ } else {
+ urls(in, NULL);
+ }
+ break;
+ default: /* INIT */
+ if (startswith(in, "diff --git ")) {
+ state = DIFF;
+ print_style(in, &styles.diff_meta);
+ } else if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
+ state = SIGNATURE;
+ signature(in);
+ } else {
+ state = BODY;
+ if (startswith(in, ">")) {
+ quote(in);
+ } else if (!regexec(&header_re, in, 8, groups, 0)) {
+ header(in);
+ } else {
+ urls(in, NULL);
+ }
+ }
+ break;
+ }
+}
+
+int parse_args(int argc, char **argv)
+{
+ const char *filename = NULL;
+ char c;
+
+ styleset = getenv("AERC_STYLESET");
+
+ while ((c = getopt(argc, argv, "hs:f:")) != -1) {
+ switch (c) {
+ case 's':
+ styleset = optarg;
+ break;
+ case 'f':
+ filename = optarg;
+ break;
+ default:
+ usage();
+ return 1;
+ }
+ }
+ if (optind < argc) {
+ fprintf(stderr, "%s: unexpected argument -- '%s'\n",
+ argv[0], argv[optind]);
+ usage();
+ return 1;
+ }
+ if (filename == NULL || !strcmp(filename, "-")) {
+ in_file = stdin;
+ } else {
+ in_file = fopen(filename, "r");
+ if (!in_file) {
+ perror("error: cannot open file");
+ return 1;
+ }
+ }
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ char buf[BUFSIZ];
+ int err;
+
+ regcomp(&header_re, HEADER_RE, REG_EXTENDED);
+ regcomp(&diff_meta_re, DIFF_META_RE, REG_EXTENDED);
+ regcomp(&url_re, URL_RE, REG_EXTENDED);
+
+ err = parse_args(argc, argv);
+ if (err) {
+ goto end;
+ }
+ err = parse_styleset();
+ if (err) {
+ goto end;
+ }
+ while (fgets(buf, sizeof(buf), in_file)) {
+ /* strip LF, CR, CRLF, LFCR */
+ buf[strcspn(buf, "\r\n")] = '\0';
+ colorize_line(buf);
+ printf("\n");
+ }
+end:
+ if (in_file) {
+ fclose(in_file);
+ }
+ return err;
+}