xapian-core  2.1.0
api_unicode.cc
Go to the documentation of this file.
1 
4 /* Copyright (C) 2006-2026 Olly Betts
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see
18  * <https://www.gnu.org/licenses/>.
19  */
20 
21 #include <config.h>
22 
23 #include "api_unicode.h"
24 
25 #include <xapian.h>
26 
27 #include "apitest.h"
28 #include "stringutils.h"
29 #include "testutils.h"
30 
31 #include <cctype>
32 #include <fstream>
33 #include <limits>
34 #include <string_view>
35 
36 using namespace std;
37 
38 struct testcase {
39  const char* a;
40  const char* b;
41 };
42 
43 static const testcase testcases[] = {
44  { "abcd", "abcd" }, // Sanity check!
45  { "a\x80""bcd", "a\xc2\x80""bcd" },
46  { "a\xa0", "a\xc2\xa0" },
47  { "a\xa0z", "a\xc2\xa0z" },
48  { "x\xc1yz", "x\xc3\x81yz" },
49  { "\xc2z", "\xc3\x82z" },
50  { "\xc2", "\xc3\x82" },
51  { "xy\xc3z", "xy\xc3\x83z" },
52  { "xy\xc3\xc3z", "xy\xc3\x83\xc3\x83z" },
53  { "xy\xc3\xc3", "xy\xc3\x83\xc3\x83" },
54  { "\xe0", "\xc3\xa0" },
55  { "\xe0\x80", "\xc3\xa0\xc2\x80" },
56  { "\xe0\xc0", "\xc3\xa0\xc3\x80" },
57  { "\xe0\xc0z", "\xc3\xa0\xc3\x80z" },
58  { "\xe0\xc0zz", "\xc3\xa0\xc3\x80zz" },
59  { "\xe0\xc0\x81", "\xc3\xa0\xc3\x80\xc2\x81" },
60  { "\xe0\x82\xc1", "\xc3\xa0\xc2\x82\xc3\x81" },
61  { "\xe0\xc5\xc7", "\xc3\xa0\xc3\x85\xc3\x87" },
62  { "\xf0", "\xc3\xb0" },
63  { "\xf0\x80", "\xc3\xb0\xc2\x80" },
64  { "\xf0\xc0", "\xc3\xb0\xc3\x80" },
65  { "\xf0\xc0z", "\xc3\xb0\xc3\x80z" },
66  { "\xf0\xc0zz", "\xc3\xb0\xc3\x80zz" },
67  { "\xf0\xc0\x81", "\xc3\xb0\xc3\x80\xc2\x81" },
68  { "\xf0\x82\xc1", "\xc3\xb0\xc2\x82\xc3\x81" },
69  { "\xf0\xc5\xc7", "\xc3\xb0\xc3\x85\xc3\x87" },
70  { "\xf0\xc0\x81\xc9", "\xc3\xb0\xc3\x80\xc2\x81\xc3\x89" },
71  { "\xf0\x82\xc1\xc8", "\xc3\xb0\xc2\x82\xc3\x81\xc3\x88" },
72  { "\xf0\xc5\xc7\xc6", "\xc3\xb0\xc3\x85\xc3\x87\xc3\x86" },
73  { "\xf0\xc0\x81\x89", "\xc3\xb0\xc3\x80\xc2\x81\xc2\x89" },
74  { "\xf0\x82\xc1\x88", "\xc3\xb0\xc2\x82\xc3\x81\xc2\x88" },
75  { "\xf0\xc5\xc7\xc6", "\xc3\xb0\xc3\x85\xc3\x87\xc3\x86" },
76  { "\xf4P\x80\x80", "\xc3\xb4P\xc2\x80\xc2\x80" },
77  { "\xf4\x80P\x80", "\xc3\xb4\xc2\x80P\xc2\x80" },
78  { "\xf4\x80\x80P", "\xc3\xb4\xc2\x80\xc2\x80P" },
79  { "\xfe\xffxyzzy", "\xc3\xbe\xc3\xbfxyzzy" },
80  // Overlong encodings:
81  { "\xc0\x80", "\xc3\x80\xc2\x80" },
82  { "\xc0\xbf", "\xc3\x80\xc2\xbf" },
83  { "\xc1\x80", "\xc3\x81\xc2\x80" },
84  { "\xc1\xbf", "\xc3\x81\xc2\xbf" },
85  { "\xe0\x80\x80", "\xc3\xa0\xc2\x80\xc2\x80" },
86  { "\xe0\x9f\xbf", "\xc3\xa0\xc2\x9f\xc2\xbf" },
87  { "\xf0\x80\x80\x80", "\xc3\xb0\xc2\x80\xc2\x80\xc2\x80" },
88  { "\xf0\x8f\xbf\xbf", "\xc3\xb0\xc2\x8f\xc2\xbf\xc2\xbf" },
89  // Above Unicode:
90  { "\xf4\x90\x80\x80", "\xc3\xb4\xc2\x90\xc2\x80\xc2\x80" },
91  // Surrogate pair cases:
92  { "\xed\xa0\x80", "\xc3\xad\xc2\xa0\xc2\x80" },
93  { "\xed\xbf\xbf", "\xc3\xad\xc2\xbf\xc2\xbf" },
94  { "\xed\xa0\x80" "\xed\xbf\xbf",
95  "\xc3\xad\xc2\xa0\xc2\x80" "\xc3\xad\xc2\xbf\xc2\xbf" },
96  { 0, 0 }
97 };
98 
99 // Test handling of invalid UTF-8 is as desired.
100 DEFINE_TESTCASE(utf8iterator1, !backend) {
101  const testcase* p;
102  for (p = testcases; p->a; ++p) {
103  tout.str(string());
104  tout << '"' << p->a << "\" and \"" << p->b << "\"\n";
105  // Exercise construction from pointer and length.
106  Xapian::Utf8Iterator a(p->a, strlen(p->a));
107  // Exercise construction from std::string_view.
108  Xapian::Utf8Iterator b(string_view(p->b));
109 
110  while (a != Xapian::Utf8Iterator() && b != Xapian::Utf8Iterator()) {
111  TEST_EQUAL(*a, *b);
112  ++a;
113  ++b;
114  }
115 
116  // Test that we don't reach the end of one before the other.
117  TEST(a == Xapian::Utf8Iterator());
118  TEST(b == Xapian::Utf8Iterator());
119  }
120 }
121 
122 struct testcase2 {
123  const char* a;
124  unsigned long n;
125 };
126 
127 static const testcase2 testcases2[] = {
128  { "a", 97 },
129  { "\x80", 128 },
130  { "\xa0", 160 },
131  { "\xc2\x80", 128 },
132  { "\xc2\xa0", 160 },
133  { "\xe0\xa0\x80", 0x0800 },
134  { "\xe1\x80\x80", 0x1000 },
135  { "\xf0\xa8\xa8\x8f", 166415 },
136  { "\xf3\x80\x80\x80", 0x0c0000 },
137  { "\xf4\x80\x80\x80", 0x100000 },
138  { 0, 0 }
139 };
140 
141 // Test decoding of UTF-8.
142 DEFINE_TESTCASE(utf8iterator2, !backend) {
143  const testcase2* p;
144  for (p = testcases2; p->a; ++p) {
145  Xapian::Utf8Iterator a(p->a);
146 
147  TEST(a != Xapian::Utf8Iterator());
148  TEST_EQUAL(*a, p->n);
149  TEST(++a == Xapian::Utf8Iterator());
150  }
151 }
152 
153 // Test we have the Unicode version we expect.
154 DEFINE_TESTCASE(unicode1, !backend) {
155  using namespace Xapian;
156  // We currently support Unicode 17.0.0 - check some codepoints which
157  // were added or changed category in this version.
178 }
179 
180 DEFINE_TESTCASE(caseconvert1, !backend) {
181  using namespace Xapian;
182  // Test Unicode case matches ISO C for ASCII subset.
183  for (unsigned ch = 0; ch < 128; ++ch) {
184  TEST_EQUAL(Unicode::tolower(ch), unsigned(tolower(ch)));
185  TEST_EQUAL(Unicode::toupper(ch), unsigned(toupper(ch)));
186  auto category = Unicode::get_category(ch);
187  TEST_EQUAL(category == Unicode::LOWERCASE_LETTER, !!islower(ch));
188  TEST_EQUAL(category == Unicode::UPPERCASE_LETTER, !!isupper(ch));
189  }
190 }
191 
192 DEFINE_TESTCASE(utf8convert1, !backend) {
193  string s;
197  Xapian::Unicode::append_utf8(s, 0xFFFF);
198  Xapian::Unicode::append_utf8(s, 166415);
199  Xapian::Unicode::append_utf8(s, 0x10345);
200  Xapian::Unicode::append_utf8(s, 0x10FFFD);
201  Xapian::Unicode::append_utf8(s, 0xFFFFFFFF);
203  TEST_STRINGS_EQUAL(s, "a"
204  "\xc2\x80"
205  "\xc2\xa0"
206  "\xef\xbf\xbf"
207  "\xf0\xa8\xa8\x8f"
208  "\xf0\x90\x8d\x85"
209  "\xf4\x8f\xbf\xbd"
210  ""
211  "z"
212  );
213 }
214 
215 static unsigned
216 decode_codepoint(const char** p) {
217  unsigned r = 0;
218  while (**p != ';') {
219  TEST(C_isxdigit(**p));
220  r = (r << 4) | hex_digit(**p);
221  ++*p;
222  }
223  return r;
224 }
225 
226 static void
227 test_codepoint(unsigned codepoint,
228  unsigned upper,
229  unsigned lower,
231 {
232  using namespace Xapian;
234  TEST_EQUAL(Unicode::toupper(codepoint), upper);
235  TEST_EQUAL(Unicode::tolower(codepoint), lower);
236 
237  switch (category) {
239  TEST(Unicode::is_currency(codepoint));
240  TEST(!Unicode::is_whitespace(codepoint));
241  TEST(!Unicode::is_wordchar(codepoint));
242  break;
243  case Unicode::CONTROL:
247  TEST(!Unicode::is_currency(codepoint));
248  TEST(Unicode::is_whitespace(codepoint));
249  TEST(!Unicode::is_wordchar(codepoint));
250  break;
263  TEST(!Unicode::is_currency(codepoint));
264  TEST(!Unicode::is_whitespace(codepoint));
265  TEST(Unicode::is_wordchar(codepoint));
266  break;
267  default:
268  TEST(!Unicode::is_currency(codepoint));
269  TEST(!Unicode::is_whitespace(codepoint));
270  TEST(!Unicode::is_wordchar(codepoint));
271  break;
272  }
273 }
274 
275 // Exhaustively test all codepoints in Unicode's assignable range, plus a
276 // subset above that range.
277 DEFINE_TESTCASE(unicodetables, !backend) {
278  using namespace Xapian;
279  string unicode_data_path =
280  test_driver::get_srcdir() + "/../unicode/UnicodeData.txt";
281  ifstream unicode_data(unicode_data_path, fstream::binary);
282  string line;
283  unsigned next_codepoint = 0;
284  while (getline(unicode_data, line), !unicode_data.eof()) {
285  tout.str(string());
286  const char* p = line.data();
287  unsigned codepoint = decode_codepoint(&p);
288  const char* desc = p + 1;
289  p = strchr(desc, ';');
290  TEST(p != nullptr);
291  bool end_of_range =
292  (*desc == '<' && p - desc > 5 && memcmp(p - 5, "Last>", 5) == 0);
293  ++p;
294  TEST(C_isupper(p[0]));
295  TEST(C_islower(p[1]));
296 #define ENCODE(C1, C2) ((C1 - 'A') * 26 + (C2 - 'a'))
298  switch (ENCODE(p[0], p[1])) {
299  case ENCODE('C', 'c'):
301  break;
302  case ENCODE('C', 'f'):
304  break;
305  case ENCODE('C', 'n'):
307  break;
308  case ENCODE('C', 'o'):
310  break;
311  case ENCODE('C', 's'):
313  break;
314  case ENCODE('L', 'l'):
316  break;
317  case ENCODE('L', 'm'):
319  break;
320  case ENCODE('L', 'o'):
322  break;
323  case ENCODE('L', 't'):
325  break;
326  case ENCODE('L', 'u'):
328  break;
329  case ENCODE('M', 'c'):
331  break;
332  case ENCODE('M', 'e'):
334  break;
335  case ENCODE('M', 'n'):
337  break;
338  case ENCODE('N', 'd'):
340  break;
341  case ENCODE('N', 'l'):
343  break;
344  case ENCODE('N', 'o'):
346  break;
347  case ENCODE('P', 'c'):
349  break;
350  case ENCODE('P', 'd'):
352  break;
353  case ENCODE('P', 'e'):
355  break;
356  case ENCODE('P', 'f'):
358  break;
359  case ENCODE('P', 'i'):
361  break;
362  case ENCODE('P', 'o'):
364  break;
365  case ENCODE('P', 's'):
367  break;
368  case ENCODE('S', 'c'):
370  break;
371  case ENCODE('S', 'k'):
373  break;
374  case ENCODE('S', 'm'):
376  break;
377  case ENCODE('S', 'o'):
379  break;
380  case ENCODE('Z', 'l'):
382  break;
383  case ENCODE('Z', 'p'):
385  break;
386  case ENCODE('Z', 's'):
388  break;
389  default:
390  FAIL_TEST("Unexpected Unicode category '" << p[0] << p[1] << "'");
391  }
392 
393  for (int i = 2; i < 12; ++i) {
394  p = strchr(p + 1, ';');
395  TEST(p != nullptr);
396  }
397  ++p;
398  unsigned upper = (*p != ';') ? decode_codepoint(&p) : codepoint;
399  ++p;
400  unsigned lower = (*p != ';') ? decode_codepoint(&p) : codepoint;
401 
402  if (end_of_range) {
403  tout << "[" << next_codepoint << ".." << codepoint
404  << "] category=" << category << '\n';
405  } else {
406  if (next_codepoint < codepoint) {
407  tout << "[" << next_codepoint << ".." << codepoint - 1 << "] "
408  "unassigned\n";
409  }
410  tout << codepoint << " category=" << category
411  << " upper=" << upper
412  << " lower=" << lower << '\n';
413  }
414 
415  while (next_codepoint < codepoint) {
416  if (end_of_range) {
417  test_codepoint(next_codepoint, next_codepoint, next_codepoint,
418  category);
419  } else {
420  test_codepoint(next_codepoint, next_codepoint, next_codepoint,
422  }
423  ++next_codepoint;
424  }
425 
426  test_codepoint(codepoint, upper, lower, category);
427  ++next_codepoint;
428  }
429 
430  // Test some invalid Unicode values.
431  TEST_EQUAL(next_codepoint, 0x10FFFE);
432 
433  tout.str(string());
434  tout << "Testing unassigned codepoints just above U+10FFFD\n";
435  unsigned codepoint = next_codepoint - 1;
436  while (++codepoint <= 0x110011) {
437  // Test some values just above the upper end of the valid range.
438  test_codepoint(codepoint, codepoint, codepoint, Unicode::UNASSIGNED);
439  }
440  tout.str(string());
441  tout << "Testing unassigned codepoints up to max unsigned int\n";
442  do {
443  // Test "all-F" values up to max of type.
444  codepoint = (codepoint << 1) | 0x0FFFFF;
445  test_codepoint(codepoint, codepoint, codepoint, Unicode::UNASSIGNED);
446  } while (codepoint < numeric_limits<decltype(codepoint)>::max());
447 }
static unsigned decode_codepoint(const char **p)
Definition: api_unicode.cc:216
static void test_codepoint(unsigned codepoint, unsigned upper, unsigned lower, Xapian::Unicode::category category)
Definition: api_unicode.cc:227
#define ENCODE(C1, C2)
static const testcase2 testcases2[]
Definition: api_unicode.cc:127
DEFINE_TESTCASE(utf8iterator1, !backend)
Definition: api_unicode.cc:100
static const testcase testcases[]
Definition: api_unicode.cc:43
test functionality of the Xapian API
An iterator which returns Unicode character values from a UTF-8 encoded string.
Definition: unicode.h:39
static std::string get_srcdir()
Read srcdir from environment and if not present, make a valiant attempt to guess a value.
Definition: testsuite.cc:135
PositionList * p
category get_category(int info)
Definition: unicode.h:283
void append_utf8(std::string &s, unsigned ch)
Append the UTF-8 representation of a single Unicode character to a std::string.
Definition: unicode.h:344
unsigned tolower(unsigned ch)
Convert a Unicode character to lowercase.
Definition: unicode.h:391
category
Each Unicode character is in exactly one of these categories.
Definition: unicode.h:228
@ MATH_SYMBOL
Symbol, math (Sm)
Definition: unicode.h:255
@ FORMAT
Other, format (Cf)
Definition: unicode.h:245
@ MODIFIER_SYMBOL
Symbol, modified (Sk)
Definition: unicode.h:257
@ FINAL_QUOTE_PUNCTUATION
Punctuation, final quote (Pf)
Definition: unicode.h:253
@ PRIVATE_USE
Other, private use (Co)
Definition: unicode.h:246
@ INITIAL_QUOTE_PUNCTUATION
Punctuation, initial quote (Pi)
Definition: unicode.h:252
@ CONNECTOR_PUNCTUATION
Punctuation, connector (Pc)
Definition: unicode.h:248
@ LOWERCASE_LETTER
Letter, lowercase (Ll)
Definition: unicode.h:231
@ MODIFIER_LETTER
Letter, modifier (Lm)
Definition: unicode.h:233
@ OTHER_SYMBOL
Symbol, other (So)
Definition: unicode.h:258
@ CURRENCY_SYMBOL
Symbol, currency (Sc)
Definition: unicode.h:256
@ UNASSIGNED
Other, not assigned (Cn)
Definition: unicode.h:229
@ ENCLOSING_MARK
Mark, enclosing (Me)
Definition: unicode.h:236
@ OTHER_LETTER
Letter, other (Lo)
Definition: unicode.h:234
@ DECIMAL_DIGIT_NUMBER
Number, decimal digit (Nd)
Definition: unicode.h:238
@ CONTROL
Other, control (Cc)
Definition: unicode.h:244
@ LINE_SEPARATOR
Separator, line (Zl)
Definition: unicode.h:242
@ CLOSE_PUNCTUATION
Punctuation, close (Pe)
Definition: unicode.h:251
@ SURROGATE
Other, surrogate (Cs)
Definition: unicode.h:247
@ PARAGRAPH_SEPARATOR
Separator, paragraph (Zp)
Definition: unicode.h:243
@ SPACE_SEPARATOR
Separator, space (Zs)
Definition: unicode.h:241
@ COMBINING_SPACING_MARK
Mark, spacing combining (Mc)
Definition: unicode.h:237
@ OPEN_PUNCTUATION
Punctuation, open (Ps)
Definition: unicode.h:250
@ NON_SPACING_MARK
Mark, nonspacing (Mn)
Definition: unicode.h:235
@ DASH_PUNCTUATION
Punctuation, dash (Pd)
Definition: unicode.h:249
@ OTHER_PUNCTUATION
Punctuation, other (Po)
Definition: unicode.h:254
@ OTHER_NUMBER
Number, other (No)
Definition: unicode.h:240
@ TITLECASE_LETTER
Letter, titlecase (Lt)
Definition: unicode.h:232
@ LETTER_NUMBER
Number, letter (Nl)
Definition: unicode.h:239
@ UPPERCASE_LETTER
Letter, uppercase (Lu)
Definition: unicode.h:230
bool is_wordchar(unsigned ch)
Test if a given Unicode character is "word character".
Definition: unicode.h:355
bool is_currency(unsigned ch)
Test if a given Unicode character is a currency symbol.
Definition: unicode.h:386
unsigned toupper(unsigned ch)
Convert a Unicode character to uppercase.
Definition: unicode.h:399
bool is_whitespace(unsigned ch)
Test if a given Unicode character is a whitespace character.
Definition: unicode.h:373
The Xapian namespace contains public interfaces for the Xapian library.
Definition: compactor.cc:82
Various handy string-related helpers.
bool C_isupper(char ch)
Definition: stringutils.h:193
int hex_digit(char ch)
Definition: stringutils.h:236
bool C_isxdigit(char ch)
Definition: stringutils.h:187
bool C_islower(char ch)
Definition: stringutils.h:198
unsigned long n
Definition: api_unicode.cc:124
const char * a
Definition: api_unicode.cc:123
const char * a
Definition: api_unicode.cc:39
const char * b
Definition: api_unicode.cc:40
std::ostringstream tout
The debug printing stream.
Definition: testsuite.cc:104
#define FAIL_TEST(MSG)
Fail the current testcase with message MSG.
Definition: testsuite.h:65
#define TEST_EQUAL(a, b)
Test for equality of two things.
Definition: testsuite.h:276
#define TEST_STRINGS_EQUAL(a, b)
Test for equality of two strings.
Definition: testsuite.h:285
#define TEST(a)
Test a condition, without an additional explanation for failure.
Definition: testsuite.h:273
Xapian-specific test helper functions and macros.
Public interfaces for the Xapian library.