Commit 98f6c00dad96d3150a9b969a0ee67addc78ac5f0

Authored by Jay Berkenbilt
1 parent ef127001

Protect numeric conversion against user's locale (fixes #459)

ChangeLog
1 2020-10-21 Jay Berkenbilt <ejb@ql.org> 1 2020-10-21 Jay Berkenbilt <ejb@ql.org>
2 2
  3 + * Ensure that numeric conversion is not affected by the user's
  4 + global locale setting. Fixes #459.
  5 +
3 * Add qpdf-<version>-linux-x86_64.zip to the list of built 6 * Add qpdf-<version>-linux-x86_64.zip to the list of built
4 distributions. This is a simple zip file that contains just the 7 distributions. This is a simple zip file that contains just the
5 qpdf executables and the dependent shared libraries that would not 8 qpdf executables and the dependent shared libraries that would not
README-maintainer
@@ -83,6 +83,14 @@ CODING RULES @@ -83,6 +83,14 @@ CODING RULES
83 83
84 * Use QIntC for type conversions -- see casting policy in docs. 84 * Use QIntC for type conversions -- see casting policy in docs.
85 85
  86 +* Remember to imbue ostringstreams with std::locale::classic() before
  87 + outputting numbers. This protects against the user's global locale
  88 + altering otherwise deterministic values. (See github issue #459.)
  89 + One could argue that error messages containing numbers should
  90 + respect the user's locale, but I think it's more important for
  91 + output to be consistent, since the messages in question are not
  92 + really targetted at the end user.
  93 +
86 * Use QPDF_DLL on all methods that are to be exported in the shared 94 * Use QPDF_DLL on all methods that are to be exported in the shared
87 library/DLL. Use QPDF_DLL_CLASS for all classes whose type 95 library/DLL. Use QPDF_DLL_CLASS for all classes whose type
88 information is needed. This is important for exception classes and 96 information is needed. This is important for exception classes and
@@ -7,7 +7,6 @@ Candidates for upcoming release @@ -7,7 +7,6 @@ Candidates for upcoming release
7 * Open "next" issues 7 * Open "next" issues
8 * bugs 8 * bugs
9 * #473: zsh completion with directories 9 * #473: zsh completion with directories
10 - * #459: locale-sensitivity  
11 * #449: internal error with case to reproduce (from pikepdf) 10 * #449: internal error with case to reproduce (from pikepdf)
12 * #444: concatenated stream/whitespace bug 11 * #444: concatenated stream/whitespace bug
13 * Non-bugs 12 * Non-bugs
include/qpdf/QIntC.hh
@@ -29,6 +29,7 @@ @@ -29,6 +29,7 @@
29 #include <limits> 29 #include <limits>
30 #include <sstream> 30 #include <sstream>
31 #include <cassert> 31 #include <cassert>
  32 +#include <locale>
32 #include <type_traits> 33 #include <type_traits>
33 34
34 // This namespace provides safe integer conversion that detects 35 // This namespace provides safe integer conversion that detects
@@ -67,6 +68,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion @@ -67,6 +68,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion
67 if (i > std::numeric_limits<To>::max()) 68 if (i > std::numeric_limits<To>::max())
68 { 69 {
69 std::ostringstream msg; 70 std::ostringstream msg;
  71 + msg.imbue(std::locale::classic());
70 msg << "integer out of range converting " << i 72 msg << "integer out of range converting " << i
71 << " from a " 73 << " from a "
72 << sizeof(From) << "-byte unsigned type to a " 74 << sizeof(From) << "-byte unsigned type to a "
@@ -88,6 +90,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion @@ -88,6 +90,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion
88 (i > std::numeric_limits<To>::max())) 90 (i > std::numeric_limits<To>::max()))
89 { 91 {
90 std::ostringstream msg; 92 std::ostringstream msg;
  93 + msg.imbue(std::locale::classic());
91 msg << "integer out of range converting " << i 94 msg << "integer out of range converting " << i
92 << " from a " 95 << " from a "
93 << sizeof(From) << "-byte signed type to a " 96 << sizeof(From) << "-byte signed type to a "
@@ -111,6 +114,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion @@ -111,6 +114,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion
111 if ((i < 0) || (ii > std::numeric_limits<To>::max())) 114 if ((i < 0) || (ii > std::numeric_limits<To>::max()))
112 { 115 {
113 std::ostringstream msg; 116 std::ostringstream msg;
  117 + msg.imbue(std::locale::classic());
114 msg << "integer out of range converting " << i 118 msg << "integer out of range converting " << i
115 << " from a " 119 << " from a "
116 << sizeof(From) << "-byte signed type to a " 120 << sizeof(From) << "-byte signed type to a "
@@ -134,6 +138,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion @@ -134,6 +138,7 @@ namespace QIntC // QIntC = qpdf Integer Conversion
134 if (i > maxval) 138 if (i > maxval)
135 { 139 {
136 std::ostringstream msg; 140 std::ostringstream msg;
  141 + msg.imbue(std::locale::classic());
137 msg << "integer out of range converting " << i 142 msg << "integer out of range converting " << i
138 << " from a " 143 << " from a "
139 << sizeof(From) << "-byte unsigned type to a " 144 << sizeof(From) << "-byte unsigned type to a "
libqpdf/BufferInputSource.cc
@@ -108,6 +108,7 @@ BufferInputSource::range_check(qpdf_offset_t cur, qpdf_offset_t delta) @@ -108,6 +108,7 @@ BufferInputSource::range_check(qpdf_offset_t cur, qpdf_offset_t delta)
108 ((std::numeric_limits<qpdf_offset_t>::max() - cur) < delta)) 108 ((std::numeric_limits<qpdf_offset_t>::max() - cur) < delta))
109 { 109 {
110 std::ostringstream msg; 110 std::ostringstream msg;
  111 + msg.imbue(std::locale::classic());
111 msg << "seeking forward from " << cur 112 msg << "seeking forward from " << cur
112 << " by " << delta 113 << " by " << delta
113 << " would cause an overflow of the offset type"; 114 << " would cause an overflow of the offset type";
libqpdf/OffsetInputSource.cc
@@ -47,6 +47,7 @@ OffsetInputSource::seek(qpdf_offset_t offset, int whence) @@ -47,6 +47,7 @@ OffsetInputSource::seek(qpdf_offset_t offset, int whence)
47 if (offset > this->max_safe_offset) 47 if (offset > this->max_safe_offset)
48 { 48 {
49 std::ostringstream msg; 49 std::ostringstream msg;
  50 + msg.imbue(std::locale::classic());
50 msg << "seeking to " << offset 51 msg << "seeking to " << offset
51 << " offset by " << global_offset 52 << " offset by " << global_offset
52 << " would cause an overflow of the offset type"; 53 << " would cause an overflow of the offset type";
libqpdf/QPDF.cc
@@ -1220,6 +1220,7 @@ QPDF::processXRefStream(qpdf_offset_t xref_offset, QPDFObjectHandle&amp; xref_obj) @@ -1220,6 +1220,7 @@ QPDF::processXRefStream(qpdf_offset_t xref_offset, QPDFObjectHandle&amp; xref_obj)
1220 ((std::numeric_limits<int>::max() - obj) < chunk_count)) 1220 ((std::numeric_limits<int>::max() - obj) < chunk_count))
1221 { 1221 {
1222 std::ostringstream msg; 1222 std::ostringstream msg;
  1223 + msg.imbue(std::locale::classic());
1223 msg << "adding " << chunk_count << " to " << obj 1224 msg << "adding " << chunk_count << " to " << obj
1224 << " while computing index in xref stream would cause" 1225 << " while computing index in xref stream would cause"
1225 << " an integer overflow"; 1226 << " an integer overflow";
libqpdf/QUtil.cc
@@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
21 #include <string.h> 21 #include <string.h>
22 #include <fcntl.h> 22 #include <fcntl.h>
23 #include <memory> 23 #include <memory>
  24 +#include <locale>
24 #ifndef QPDF_NO_WCHAR_T 25 #ifndef QPDF_NO_WCHAR_T
25 # include <cwchar> 26 # include <cwchar>
26 #endif 27 #endif
@@ -267,6 +268,7 @@ int_to_string_base_internal(T num, int base, int length) @@ -267,6 +268,7 @@ int_to_string_base_internal(T num, int base, int length)
267 "int_to_string_base called with unsupported base"); 268 "int_to_string_base called with unsupported base");
268 } 269 }
269 std::ostringstream buf; 270 std::ostringstream buf;
  271 + buf.imbue(std::locale::classic());
270 buf << std::setbase(base) << std::nouppercase << num; 272 buf << std::setbase(base) << std::nouppercase << num;
271 std::string result; 273 std::string result;
272 int str_length = QIntC::to_int(buf.str().length()); 274 int str_length = QIntC::to_int(buf.str().length());
@@ -318,6 +320,7 @@ QUtil::double_to_string(double num, int decimal_places) @@ -318,6 +320,7 @@ QUtil::double_to_string(double num, int decimal_places)
318 decimal_places = 6; 320 decimal_places = 6;
319 } 321 }
320 std::ostringstream buf; 322 std::ostringstream buf;
  323 + buf.imbue(std::locale::classic());
321 buf << std::setprecision(decimal_places) << std::fixed << num; 324 buf << std::setprecision(decimal_places) << std::fixed << num;
322 return buf.str(); 325 return buf.str();
323 } 326 }
libtests/qutil.cc
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 #include <limits.h> 10 #include <limits.h>
11 #include <assert.h> 11 #include <assert.h>
12 #include <fstream> 12 #include <fstream>
  13 +#include <locale>
13 14
14 #ifdef _WIN32 15 #ifdef _WIN32
15 # include <io.h> 16 # include <io.h>
@@ -80,8 +81,38 @@ void test_to_ull(char const* str, unsigned long long wanted, bool error) @@ -80,8 +81,38 @@ void test_to_ull(char const* str, unsigned long long wanted, bool error)
80 test_to_number(str, wanted, error, QUtil::string_to_ull); 81 test_to_number(str, wanted, error, QUtil::string_to_ull);
81 } 82 }
82 83
  84 +static void set_locale()
  85 +{
  86 + try
  87 + {
  88 + // First try a locale known to put commas in numbers.
  89 + std::locale::global(std::locale("en_US.UTF-8"));
  90 + }
  91 + catch (std::runtime_error&)
  92 + {
  93 + try
  94 + {
  95 + // If that fails, fall back to the user's default locale.
  96 + std::locale::global(std::locale(""));
  97 + }
  98 + catch (std::runtime_error& e)
  99 + {
  100 + // Ignore this error on Windows without MSVC. We get
  101 + // enough test coverage on other platforms, and mingw
  102 + // seems to have limited locale support (as of
  103 + // 2020-10).
  104 +#if ! defined(_WIN32) || defined(_MSC_VER)
  105 + throw e;
  106 +#endif
  107 + }
  108 + }
  109 +}
  110 +
83 void string_conversion_test() 111 void string_conversion_test()
84 { 112 {
  113 + // Make sure the code produces consistent results even if we load
  114 + // a non-C locale.
  115 + set_locale();
85 std::cout << QUtil::int_to_string(16059) << std::endl 116 std::cout << QUtil::int_to_string(16059) << std::endl
86 << QUtil::int_to_string(16059, 7) << std::endl 117 << QUtil::int_to_string(16059, 7) << std::endl
87 << QUtil::int_to_string(16059, -7) << std::endl 118 << QUtil::int_to_string(16059, -7) << std::endl