... so my preliminary result is that there's a *boatload* of "invalid"
format strings in our translation files, all of them apparently with
the extra-space disease. See attached.
While it looks like every one of these is a typo, I'm beginning to
have second thoughts about the premise here. Presumably, the reason
we've not noticed this issue before is that almost all non-Windows
platforms were using libc's printf, which is going to implement the
full set of POSIX-specified flags including space. As of HEAD that
is no longer the case, because we use snprintf.c everywhere. While
it might not be a problem for our own code if snprintf.c is a couple
of flags short of full POSIX, I'm worrying that third-party code
might be unhappy if *printf in the PG environment behaves oddly.
So maybe the best compromise is to upgrade snprintf.c. I think it'd
only take a few more lines to implement the space flag per spec.
The other flags that we aren't implementing are '#' ("alternate form")
and "'" (thousands grouping). For those, I'd be a bit inclined to accept
them but treat 'em as no-ops. On the other hand, that still leaves
some daylight between us and POSIX, in the form of stuff like "long
double" support, and I still don't want to go near that.
Thoughts?
For the archives' sake, I attach the code I used for this.
I'm now thinking maybe we wouldn't commit it, though.
regards, tom lane
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/restricted_token.c:77
"תכנית %s: לא ניתן לפתוח את התהליך token: קוד שגיאה % lu
"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/restricted_token.c:90
"תכנית %s: לא ניתן להקצות SID: קוד שגיאה % lu
"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/restricted_token.c:132
"תכנית %s: לא ניתן להפעיל תהליך עבור הפקודה "%s": קוד שגיאה % lu
"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/restricted_token.c:170
"תכנית %s: לא ניתן לבצע מחדש עם אסימון גישה מוגבל: קוד שגיאה % lu
"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/restricted_token.c:186
"תכנית %s: לא ניתן לקבל קוד היציאה מן תהליך משנה: קוד שגיאה % lu
"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/username.c:43
"לא יכול לחפש יעיל את המשתמש עם מזהה % ld: %s"
invalid format string in file "src/bin/initdb/po/he.po" for ../../common/username.c:60
"כישלון בדיקה עבור שם המשתמש: קוד שגיאה % lu"
invalid format string in file "src/bin/initdb/po/tr.po" for initdb.c:2171
"% s: geçersiz yerel ayarlar; LANG ve LC_ * ortam değişkenlerini denetleyin.
"
invalid format string in file "src/bin/initdb/po/tr.po" for initdb.c:3195
"% s: superuser adı "% s" izin verilmiyor; rol adları "pg_" ile başlayamaz
"
invalid format string in file "src/bin/scripts/po/he.po" for ../../common/username.c:43
"לא יכול לחפש יעיל את המשתמש עם מזהה % ld: %s"
invalid format string in file "src/bin/scripts/po/he.po" for ../../common/username.c:60
"כישלון בדיקה עבור שם המשתמש: קוד שגיאה % lu"
invalid format string in file "src/bin/scripts/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורה)"
invalid format string in file "src/bin/scripts/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורה)"
invalid format string in file "src/bin/scripts/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורות)"
invalid format string in file "src/bin/psql/po/he.po" for ../../common/username.c:43
"לא יכול לחפש יעיל את המשתמש עם מזהה % ld: %s"
invalid format string in file "src/bin/psql/po/he.po" for ../../common/username.c:60
"כישלון בדיקה עבור שם המשתמש: קוד שגיאה % lu"
invalid format string in file "src/bin/psql/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורה)"
invalid format string in file "src/bin/psql/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורה)"
invalid format string in file "src/bin/psql/po/he.po" for ../../fe_utils/print.c:353
"(% lu שורות)"
invalid format string in file "src/bin/psql/po/he.po" for command.c:553
"לא ניתן לקבל את ספריית הבית עבור משתמש עם מזהה % ld: %s
"
invalid format string in file "src/bin/psql/po/he.po" for common.c:608
"זמן: %.3f ms (% 02d:%06.3f)
"
invalid format string in file "src/bin/psql/po/he.po" for common.c:617
"זמן: %.3f ms (% 02d:%02d:%06.3f)
"
invalid format string in file "src/bin/pg_basebackup/po/he.po" for pg_basebackup.c:965
"תכנתי %s: לא היתה אפשרות להגדיר את רמת הדחיסה % d: %s
"
invalid format string in file "src/bin/pg_basebackup/po/he.po" for pg_recvlogical.c:135
"תכנית %s: מאשר לכתוב עד % X/%X, סומק ל %X/%X (חריץ %s)
"
invalid format string in file "src/bin/pg_basebackup/po/he.po" for receivelog.c:278
"תכנית %s: שרת דיווח שם קובץ היסטוריה לא צפוי עבור ציר הזמן % u: %s
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:495
"תכנית %s: לא היתה אפשרות להפעיל שרת: קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:967
"תכנית %s: לא יכול להפסיק את השרת; מופעל שרת למשתמש יחיד (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:975
"תכנית %s: לא היתה אפשרות לשלוח אות עצירה (PID: % ld): %s
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1058
"תכנית %s: לא ניתן להפעיל מחדש את השרת; מופעל שרת למשתמש יחיד (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1120
"תכנית %s: תהליך השרת הישן (PID: % ld) כנראה פועל
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1143
"תכנית %s: לא ניתן להפעיל מחדש את השרת; מופעל שרת למשתמש יחיד (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1152
"תכנית %s: לא היתה אפשרות לשלוח אות להפעלה מחדש (PID: % ld): %s
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1182
"תכנית %s: לא ניתן לקדם שרת; מופעל שרת למשתמש יחיד (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1219
"תכנית %s: לא היתה אפשרות לשלוח את האות לקדם(PID: % ld): %s
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1301
"תכנית %s: שרת משתמש יחיד פועל (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1314
"תכנית %s: שרת פועל (PID: % ld)
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1348
"תכנית %s: לא היתה אפשרות לשלוח אות %d (PID: % ld): %s
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1502
"תכנית %s: אין אפשרות לרשום את השירות "%s": קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1532
"תכנית %s: לא היתה אפשרות לפתוח שירות '%s': קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1541
"תכנית %s: לא יכול לבטל את השירות "%s": קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1690
"תכנית %s: לא היתה אפשרות להפעיל שירות '%s': קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1777
"תכנית %s: לא ניתן לפתוח את התהליך token: קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_ctl/po/he.po" for pg_ctl.c:1791
"תכנית %s: לא ניתן להקצות SID: קוד שגיאה % lu
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1302
"לא היתה אפשרות ליצור אובייקט גדול % u: %s"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1307
"לא היתה אפשרות לפתוח את אובייקט גדול % u: %s"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1652
"נכתב % lu בית של נתוני האובייקט הגדול (תוצאה = % lu)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1652
"נכתב % lu בית של נתוני האובייקט הגדול (תוצאה = % lu)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1652
"נכתבו % lu בתים של נתוני האובייקט הגדול (תוצאה = % lu)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:1658
"לא מצליח לכתוב על אובייקט גדול (תוצאה: % lu, צפוי: % lu)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:2147
"קובץ הקלט קצר מדי (לקרוא % lu, מצופה 5)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:2562
"מזהה כניסה% d מחוץ לטווח - אולי TOC מושחת
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:3659
"גירסה לא נתמכת (% d.%d) בכותרת הקובץ
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_archiver.c:3664
"בדיקת שפיות על גודל מספר שלם (% lu) נכשלה
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_custom.c:451
"סוג בלוק נתונים לא מזוהה (% d) בעת חיפוש בארכיון
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_custom.c:472
"לא יכול למצוא את מזהה הבלוק % d בארכיון - אולי בשל בקשת שחזור שאינה מסודרת, שלא ניתן לטפל בה עקב חוסר קיזוז נתונים
בארכיון
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_custom.c:477
"לא יכול למצוא את מזהה הבלוק% d בארכיון - אולי בשל בקשת שחזור שאינה מסודרת, שלא ניתן לטפל בה עקב קובץ קלט שאינו ניתן
לחיפושבו
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_custom.c:482
"לא יכול למצוא את מזהה הבלוק% d בארכיון - אולי בשל בקשת שחזור שאינה מסודרת, שלא ניתן לטפל בה עקב קובץ ארכיון מושחת
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_tar.c:1237
"כותרת tar לא שלמה נמצאה (% lu בית)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_tar.c:1237
"כותרת tar לא שלמה נמצאה (% lu בית)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_backup_tar.c:1237
"כותרת tar לא שלמה נמצאה (% lu בתים)
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_dump.c:1000
"צוין קידוד לקוח לא חוקי "% s"
"
invalid format string in file "src/bin/pg_dump/po/he.po" for pg_dump.c:3095
"שגיאה בעת קריאת אובייקט גדול % u: %s"
invalid format string in file "src/backend/po/id.po" for commands/vacuumlazy.c:1093
"% .0f mati versi baris tidak dapat dihapus belum.
Ada % .0f item pointer yang tidak terpakai.
halaman%u sepenuhnya kosong.
%s."
invalid format string in file "src/backend/po/id.po" for executor/functions.c:225
"tidak bisa menentukan pernyataan tipe argumen yang sebenarnya % s"
invalid format string in file "src/backend/po/id.po" for executor/functions.c:638
"tidak bisa menentukan hasil yang sebenarnya untuk pernyataan kepada pengembalian tipe % s"
invalid format string in file "src/backend/po/id.po" for libpq/auth.c:2203
"tidak dapat melakukan pengumpulan inisialisasi LDAP untuk ldapbinddn « %s » pada server « %s »:% s"
invalid format string in file "src/backend/po/id.po" for libpq/auth.c:2283
"tidak dapat melepaskan setelah mencari pengguna « %s » di server « %s »:% s"
invalid format string in file "src/backend/po/fr.po" for catalog/objectaddress.c:1903
"le droit par défaut pour l'utilisateur « % s» dans le schéma « %s » de %s n'existe pas"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-auth.c:1045
"כישלון בדיקה עבור שם המשתמש: קוד שגיאה % lu
"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-auth.c:1055
"לא יכול לחפש משתמש מקומי עם מזהה % d: %s
"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-connect.c:1561
"נכשל WSAIoctl(SIO_KEEPALIVE_VALS): % ui
"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-misc.c:292
"מספר שלם בגודל % lu אינו נתמך על ידי pqGetInt"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-misc.c:328
"מספר שלם בגודל % lu אינו נתמך על ידי pqPutInt"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-protocol2.c:529
"תו לא צפוי% c בעקבות תגובת שאילתה ריקה (הודעת "I")"
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-protocol3.c:1316
"השורה % d: "
invalid format string in file "src/interfaces/libpq/po/he.po" for fe-secure-openssl.c:1465
"קוד שגיאת SSL % lu"
invalid format string in file "src/pl/plpython/po/vi.po" for plpy_typeio.c:1541
"thuộc tính "% s" không tồn tại trong đối tượng Python"
diff --git a/src/port/snprintf.c b/src/port/snprintf.c
index a773381..42aaea8 100644
*** a/src/port/snprintf.c
--- b/src/port/snprintf.c
***************
*** 110,115 ****
--- 110,129 ----
#undef printf
/*
+ * If this file is compiled with SNPRINTF_VALIDATE defined, it doesn't
+ * provide any of the normal *printf functions, but instead exports
+ * a function "pg_validate_printf_format()" that just detects whether a
+ * supplied format string is syntactically valid. We implement that
+ * by running the normal snprintf code with a fake output buffer, and
+ * suppressing all attempts to fetch variable arguments (since those
+ * won't be there).
+ */
+ #ifdef SNPRINTF_VALIDATE
+ #undef va_arg
+ #define va_arg(args, type) ((type) 0)
+ #endif
+
+ /*
* Info about where the formatted output is going.
*
* dopr and subroutines will not write at/past bufend, but snprintf
*************** static void dopr(PrintfTarget *target, c
*** 170,175 ****
--- 184,191 ----
* they not change the value of "errno" before reaching dopr().
*/
+ #ifndef SNPRINTF_VALIDATE
+
int
pg_vsnprintf(char *str, size_t count, const char *fmt, va_list args)
{
*************** pg_printf(const char *fmt,...)
*** 290,295 ****
--- 306,335 ----
return len;
}
+ #else /* SNPRINTF_VALIDATE */
+
+ /*
+ * Returns 1 if "fmt" is syntactically valid, -1 if not.
+ * In the latter case, errno is set (but it'll always be EINVAL).
+ */
+ int
+ pg_validate_printf_format(const char *fmt)
+ {
+ /* Code is basically the same as pg_vsnprintf with NULL str argument */
+ PrintfTarget target;
+ char onebyte[1];
+
+ target.bufstart = target.bufptr = onebyte;
+ target.bufend = onebyte;
+ target.stream = NULL;
+ target.nchars = 0;
+ target.failed = false;
+ dopr(&target, fmt, NULL);
+ return target.failed ? -1 : 1;
+ }
+
+ #endif /* SNPRINTF_VALIDATE */
+
/*
* Attempt to write the entire buffer to target->stream; discard the entire
* buffer in any case. Call this only when target->stream is defined.
*************** dopr(PrintfTarget *target, const char *f
*** 438,445 ****
--- 478,489 ----
if (*format == 's')
{
format++;
+ #ifndef SNPRINTF_VALIDATE
strvalue = va_arg(args, char *);
Assert(strvalue != NULL);
+ #else
+ strvalue = "";
+ #endif
dostr(strvalue, strlen(strvalue), target);
if (target->failed)
break;
*************** nextch2:
*** 666,677 ****
--- 710,726 ----
else
fieldwidth = accum;
}
+ #ifndef SNPRINTF_VALIDATE
if (have_dollar)
strvalue = argvalues[fmtpos].cptr;
else
strvalue = va_arg(args, char *);
/* Whine if someone tries to print a NULL string */
Assert(strvalue != NULL);
+ #else
+ /* va_arg would return NULL, so inject empty string instead */
+ strvalue = "";
+ #endif
fmtstr(strvalue, leftjust, fieldwidth, precision, pointflag,
target);
break;
*************** fail:
*** 1252,1257 ****
--- 1301,1308 ----
target->failed = true;
}
+ #ifndef SNPRINTF_VALIDATE
+
/*
* Nonstandard entry point to print a double value efficiently.
*
*************** fail:
*** 1352,1357 ****
--- 1403,1410 ----
+ target.nchars);
}
+ #endif /* SNPRINTF_VALIDATE */
+
static void
dostr(const char *str, int slen, PrintfTarget *target)
diff --git a/src/port/test_po_file.c b/src/port/test_po_file.c
index ...8dc063c .
*** a/src/port/test_po_file.c
--- b/src/port/test_po_file.c
***************
*** 0 ****
--- 1,150 ----
+ /*-------------------------------------------------------------------------
+ *
+ * test_po_file.c
+ * Test messages in a .po file to see if our version of snprintf likes them.
+ *
+ * Copyright (c) 2018, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/port/test_po_file.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+ #include "c.h"
+
+ #include <gettext-po.h>
+
+ /* snprintf.c will provide this below */
+ extern int pg_validate_printf_format(const char *fmt);
+
+
+ /* Error handlers required by recent libgettextpo versions */
+ static void
+ xerror(int severity,
+ po_message_t message,
+ const char *filename, size_t lineno, size_t column,
+ int multiline_p, const char *message_text)
+ {
+ fprintf(stderr, "PO error: %s\n", message_text);
+ if (filename)
+ fprintf(stderr, "Error occurred in file \"%s\" at line %d:\n",
+ filename, (int) lineno);
+ if (severity == PO_SEVERITY_FATAL_ERROR)
+ exit(1);
+ }
+
+ static void
+ xerror2(int severity,
+ po_message_t message1,
+ const char *filename1, size_t lineno1, size_t column1,
+ int multiline_p1, const char *message_text1,
+ po_message_t message2,
+ const char *filename2, size_t lineno2, size_t column2,
+ int multiline_p2, const char *message_text2)
+ {
+ fprintf(stderr, "PO error: %s ...\n", message_text1);
+ if (filename1)
+ fprintf(stderr, "Error occurred in file \"%s\" at line %d:\n",
+ filename1, (int) lineno1);
+ fprintf(stderr, "PO error: ... %s\n", message_text2);
+ if (filename2)
+ fprintf(stderr, "Error occurred in file \"%s\" at line %d:\n",
+ filename2, (int) lineno2);
+ if (severity == PO_SEVERITY_FATAL_ERROR)
+ exit(1);
+ }
+
+ static const struct po_xerror_handler handler = {
+ xerror, xerror2
+ };
+
+
+ /*
+ * Main program.
+ *
+ * Test each .po file named on the command line.
+ */
+ int
+ main(int argc, char **argv)
+ {
+ for (int i = 1; i < argc; i++)
+ {
+ const char *filename = argv[i];
+ po_file_t file = po_file_read(filename, &handler);
+ const char *const *domains;
+ const char *const *domainp;
+
+ if (file == NULL)
+ {
+ fprintf(stderr, "could not open file \"%s\": %s\n",
+ filename, strerror(errno));
+ exit(1);
+ }
+
+ domains = po_file_domains(file);
+ for (domainp = domains; *domainp; domainp++)
+ {
+ const char *domain = *domainp;
+ po_message_iterator_t iterator = po_message_iterator(file, domain);
+
+ for (;;)
+ {
+ po_message_t message = po_next_message(iterator);
+ po_filepos_t pos;
+ char location[256];
+ const char *fmt;
+
+ if (message == NULL)
+ break;
+
+ if (!po_message_is_format(message, "c-format"))
+ continue;
+
+ pos = po_message_filepos(message, 0);
+ if (pos)
+ snprintf(location, sizeof(location), "for %s:%ld",
+ po_filepos_file(pos),
+ (long) po_filepos_start_line(pos));
+ else
+ strcpy(location, "unknown location");
+
+ fmt = po_message_msgid(message);
+ if (fmt && pg_validate_printf_format(fmt) < 0)
+ fprintf(stderr, "invalid format string in file \"%s\" %s\n\"%s\"\n",
+ filename, location, fmt);
+ fmt = po_message_msgid_plural(message);
+ if (fmt && pg_validate_printf_format(fmt) < 0)
+ fprintf(stderr, "invalid format string in file \"%s\" %s\n\"%s\"\n",
+ filename, location, fmt);
+ fmt = po_message_msgstr(message);
+ if (fmt && pg_validate_printf_format(fmt) < 0)
+ fprintf(stderr, "invalid format string in file \"%s\" %s\n\"%s\"\n",
+ filename, location, fmt);
+ for (int ndx = 0;; ndx++)
+ {
+ fmt = po_message_msgstr_plural(message, ndx);
+ if (!fmt)
+ break;
+ if (pg_validate_printf_format(fmt) < 0)
+ fprintf(stderr, "invalid format string in file \"%s\" %s\n\"%s\"\n",
+ filename, location, fmt);
+ }
+ }
+ po_message_iterator_free(iterator);
+ }
+ po_file_free(file);
+ }
+
+ return 0;
+ }
+
+ /*
+ * Now include snprintf.c with the desired variant compilation flag.
+ * We put this at the end so that its messing about with #define's
+ * won't mess up code in this file.
+ */
+ #define SNPRINTF_VALIDATE 1
+
+ #include "snprintf.c"