gpg: Reflow long texts.
authorNeal H. Walfield <neal@g10code.com>
Mon, 23 Nov 2015 21:20:28 +0000 (22:20 +0100)
committerNeal H. Walfield <neal@g10code.com>
Mon, 23 Nov 2015 21:23:38 +0000 (22:23 +0100)
* common/stringhelp.c (format_text): New function.
* common/t-stringhelp.c (stresc): New function.
(test_format_text): New function.  Test format_text.
* g10/tofu.c (get_trust): Use format_text to reflow long texts.
(show_statistics): Likewise.

--
Signed-off-by: Neal H. Walfield <neal@g10code.com>
common/stringhelp.c
common/stringhelp.h
common/t-stringhelp.c
g10/tofu.c

index d0b5561..6748d1e 100644 (file)
@@ -1327,3 +1327,132 @@ strtokenize (const char *string, const char *delim)
 
   return result;
 }
+
+char *
+format_text (char *text, int in_place, int target_cols, int max_cols)
+{
+  const int do_debug = 0;
+
+  /* The character under consideration.  */
+  char *p;
+  /* The start of the current line.  */
+  char *line;
+  /* The last space that we saw.  */
+  char *last_space = NULL;
+  int last_space_cols = 0;
+  int copied_last_space = 0;
+
+  if (! in_place)
+    text = xstrdup (text);
+
+  p = line = text;
+  while (1)
+    {
+      /* The number of columns including any trailing space.  */
+      int cols;
+
+      p = p + strcspn (p, "\n ");
+      if (! p)
+        /* P now points to the NUL character.  */
+        p = &text[strlen (text)];
+
+      if (*p == '\n')
+        /* Pass through any newlines.  */
+        {
+          p ++;
+          line = p;
+          last_space = NULL;
+          last_space_cols = 0;
+          copied_last_space = 1;
+          continue;
+        }
+
+      /* Have a space or a NUL.  Note: we don't count the trailing
+         space.  */
+      cols = utf8_charcount (line, (uintptr_t) p - (uintptr_t) line);
+      if (cols < target_cols)
+        {
+          if (! *p)
+            /* Nothing left to break.  */
+            break;
+
+          last_space = p;
+          last_space_cols = cols;
+          p ++;
+          /* Skip any immediately following spaces.  If we break:
+             "... foo bar ..." between "foo" and "bar" then we want:
+             "... foo\nbar ...", which means that the left space has
+             to be the first space after foo, not the last space
+             before bar.  */
+          while (*p == ' ')
+            p ++;
+        }
+      else
+        {
+          int cols_with_left_space;
+          int cols_with_right_space;
+          int left_penalty;
+          int right_penalty;
+
+          cols_with_left_space = last_space_cols;
+          cols_with_right_space = cols;
+
+          if (do_debug)
+            log_debug ("Breaking: '%.*s'\n",
+                       (int) ((uintptr_t) p - (uintptr_t) line), line);
+
+          /* The number of columns away from TARGET_COLS.  We prefer
+             to underflow than to overflow.  */
+          left_penalty = target_cols - cols_with_left_space;
+          right_penalty = 2 * (cols_with_right_space - target_cols);
+
+          if (cols_with_right_space > max_cols)
+            /* Add a large penalty for each column that exceeds
+               max_cols.  */
+            right_penalty += 4 * (cols_with_right_space - max_cols);
+
+          if (do_debug)
+            log_debug ("Left space => %d cols (penalty: %d); right space => %d cols (penalty: %d)\n",
+                       cols_with_left_space, left_penalty,
+                       cols_with_right_space, right_penalty);
+          if (last_space_cols && left_penalty <= right_penalty)
+            /* Prefer the left space.  */
+            {
+              if (do_debug)
+                log_debug ("Breaking at left space.\n");
+              p = last_space;
+            }
+          else
+            {
+              if (do_debug)
+                log_debug ("Breaking at right space.\n");
+            }
+
+          if (! *p)
+            break;
+
+          *p = '\n';
+          p ++;
+          if (*p == ' ')
+            {
+              int spaces;
+              for (spaces = 1; p[spaces] == ' '; spaces ++)
+                ;
+              memmove (p, &p[spaces], strlen (&p[spaces]) + 1);
+            }
+          line = p;
+          last_space = NULL;
+          last_space_cols = 0;
+          copied_last_space = 0;
+        }
+    }
+
+  /* Chop off any trailing space.  */
+  while (text[strlen (text) - 1] == ' ')
+    text[strlen (text) - 1] = '\0';
+  /* If we inserted the trailing newline, then remove it.  */
+  if (! copied_last_space && text[strlen (text) - 1] == '\n')
+    text[strlen (text) - 1] = '\0';
+
+  return text;
+}
index b34d28b..9ff062b 100644 (file)
@@ -148,6 +148,11 @@ char **strsplit (char *string, char delim, char replacement, int *count);
 /* Tokenize STRING using the set of delimiters in DELIM.  */
 char **strtokenize (const char *string, const char *delim);
 
+/* Format a string so that it fits within about TARGET_COLS columns.
+   If IN_PLACE is 0, then TEXT is copied to a new buffer, which is
+   returned.  Otherwise, TEXT is modified in place and returned.
+   Normally, target_cols will be 72 and max_cols is 80.  */
+char *format_text (char *text, int in_place, int target_cols, int max_cols);
 
 /*-- mapstrings.c --*/
 const char *map_static_macro_string (const char *string);
index 13f3afa..9e5410b 100644 (file)
@@ -677,6 +677,142 @@ test_strtokenize (void)
     }
 }
 
+static char *
+stresc (char *s)
+{
+  char *p;
+  int l = strlen (s) + 1;
+
+  for (p = s; *p; p ++)
+    if (*p == '\n')
+      l ++;
+
+  p = xmalloc (l);
+  for (l = 0; *s; s ++, l ++)
+    {
+      if (*s == ' ')
+        p[l] = '_';
+      else if (*p == '\n')
+        {
+          p[l ++] = '\\';
+          p[l ++] = 'n';
+          p[l] = '\n';
+        }
+      else
+        p[l] = *s;
+    }
+  p[l] = *s;
+
+  return p;
+}
+
+static void
+test_format_text (void)
+{
+  struct test
+  {
+    int target_cols, max_cols;
+    char *input;
+    char *expected;
+  };
+
+  struct test tests[] = {
+    {
+      10, 12,
+      "",
+      "",
+    },
+    {
+      10, 12,
+      " ",
+      "",
+    },
+    {
+      10, 12,
+      "  ",
+      "",
+    },
+    {
+      10, 12,
+      " \n ",
+      " \n",
+    },
+    {
+      10, 12,
+      " \n  \n ",
+      " \n  \n",
+    },
+    {
+      10, 12,
+      "0123456789 0123456789 0",
+      "0123456789\n0123456789\n0",
+    },
+    {
+      10, 12,
+      "   0123456789   0123456789   0  ",
+      "   0123456789\n0123456789\n0",
+    },
+    {
+      10, 12,
+      "01 34 67 90 23 56  89 12 45 67 89 1",
+      "01 34 67\n90 23 56\n89 12 45\n67 89 1"
+    },
+    {
+      10, 12,
+      "01 34 67 90 23 56  89 12 45 67 89 1",
+      "01 34 67\n90 23 56\n89 12 45\n67 89 1"
+    },
+    {
+      72, 80,
+      "Warning: if you think you've seen more than 10 messages "
+      "signed by this key, then this key might be a forgery!  "
+      "Carefully examine the email address for small variations "
+      "(e.g., additional white space).  If the key is suspect, "
+      "then use 'gpg --tofu-policy bad \"FINGERPRINT\"' to mark it as being bad.\n",
+      "Warning: if you think you've seen more than 10 messages signed by this\n"
+      "key, then this key might be a forgery!  Carefully examine the email\n"
+      "address for small variations (e.g., additional white space).  If the key\n"
+      "is suspect, then use 'gpg --tofu-policy bad \"FINGERPRINT\"' to mark it as\n"
+      "being bad.\n"
+
+    },
+    {
+      72, 80,
+      "Normally, there is only a single key associated with an email "
+      "address.  However, people sometimes generate a new key if "
+      "their key is too old or they think it might be compromised.  "
+      "Alternatively, a new key may indicate a man-in-the-middle "
+      "attack!  Before accepting this key, you should talk to or "
+      "call the person to make sure this new key is legitimate.",
+      "Normally, there is only a single key associated with an email "
+      "address.\nHowever, people sometimes generate a new key if "
+      "their key is too old or\nthey think it might be compromised.  "
+      "Alternatively, a new key may indicate\na man-in-the-middle "
+      "attack!  Before accepting this key, you should talk\nto or "
+      "call the person to make sure this new key is legitimate.",
+    }
+  };
+
+  int i;
+  int failed = 0;
+
+  for (i = 0; i < sizeof (tests) / sizeof (tests[0]); i ++)
+    {
+      struct test *test = &tests[i];
+      char *result =
+        format_text (test->input, 0, test->target_cols, test->max_cols);
+      if (strcmp (result, test->expected) != 0)
+        {
+          printf ("%s: Test #%d failed.\nExpected: '%s'\nResult: '%s'\n",
+                  __func__, i + 1, stresc (test->expected), stresc (result));
+          failed ++;
+        }
+      xfree (result);
+    }
+
+  if (failed)
+    fail(0);
+}
 
 int
 main (int argc, char **argv)
@@ -692,6 +828,7 @@ main (int argc, char **argv)
   test_make_absfilename_try ();
   test_strsplit ();
   test_strtokenize ();
+  test_format_text ();
 
   xfree (home_buffer);
   return 0;
index 5e38d21..d340bfe 100644 (file)
@@ -2038,7 +2038,9 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
            "Alternatively, a new key may indicate a man-in-the-middle "
            "attack!  Before accepting this key, you should talk to or "
            "call the person to make sure this new key is legitimate.";
+        text = format_text (text, 0, 72, 80);
        es_fprintf (fp, "\n%s\n", text);
+        xfree (text);
       }
 
     es_fputc ('\n', fp);
@@ -2440,7 +2442,8 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
          if (policy == TOFU_POLICY_AUTO && messages < 10)
            {
              char *set_policy_command;
-             const char *text;
+             char *text;
+              char *tmp;
 
              if (messages == 0)
                log_info (_("Warning: we've have yet to see"
@@ -2462,9 +2465,14 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
                  "Carefully examine the email address for small variations "
                  "(e.g., additional white space).  If the key is suspect, "
                  "then use '%s' to mark it as being bad.\n";
-             log_info (text,
-                       messages, messages == 1 ? _("message") : _("message"),
-                       set_policy_command);
+              tmp = xasprintf
+                (text,
+                 messages, messages == 1 ? _("message") : _("message"),
+                 set_policy_command);
+              text = format_text (tmp, 0, 72, 80);
+              xfree (tmp);
+             log_info ("%s", text);
+              xfree (text);
              free (set_policy_command);
            }
        }