Branch data Line data Source code
1 : : /*
2 : : * Copyright © 2014 Canonical Limited
3 : : *
4 : : * SPDX-License-Identifier: LGPL-2.1-or-later
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library 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 GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General Public
17 : : * License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Author: Ryan Lortie <desrt@desrt.ca>
20 : : */
21 : :
22 : : #include <config.h>
23 : :
24 : : #include "gstrfuncs.h"
25 : :
26 : : #include <glib.h>
27 : : #include <locale.h>
28 : : #include <stdlib.h>
29 : : #include <string.h>
30 : :
31 : : struct mapping_entry
32 : : {
33 : : guint16 src;
34 : : guint16 ascii;
35 : : };
36 : :
37 : : struct mapping_range
38 : : {
39 : : guint16 start;
40 : : guint16 length;
41 : : };
42 : :
43 : : struct locale_entry
44 : : {
45 : : guint8 name_offset;
46 : : guint8 item_id;
47 : : };
48 : :
49 : : #include "gtranslit-data.h"
50 : :
51 : : #define get_src_char(array, encoded, index) ((encoded & 0x8000) ? (array)[((encoded) & 0xfff) + index] : encoded)
52 : : #define get_length(encoded) ((encoded & 0x8000) ? ((encoded & 0x7000) >> 12) : 1)
53 : :
54 : : #if G_BYTE_ORDER == G_BIG_ENDIAN
55 : : #define get_ascii_item(array, encoded) ((encoded & 0x8000) ? &(array)[(encoded) & 0xfff] : (gpointer) (((char *) &(encoded)) + 1))
56 : : #else
57 : : #define get_ascii_item(array, encoded) ((encoded & 0x8000) ? &(array)[(encoded) & 0xfff] : (gpointer) &(encoded))
58 : : #endif
59 : :
60 : : static const gchar * lookup_in_item (guint item_id,
61 : : const gunichar *key,
62 : : gint *result_len,
63 : : gint *key_consumed);
64 : :
65 : : static gint
66 : 891 : compare_mapping_entry (gconstpointer user_data,
67 : : gconstpointer data)
68 : : {
69 : 891 : const struct mapping_entry *entry = data;
70 : 891 : const gunichar *key = user_data;
71 : : gunichar src_0;
72 : :
73 : : G_STATIC_ASSERT(MAX_KEY_SIZE == 2);
74 : :
75 : 891 : src_0 = get_src_char (src_table, entry->src, 0);
76 : :
77 : 891 : if (key[0] > src_0)
78 : 306 : return 1;
79 : 585 : else if (key[0] < src_0)
80 : 493 : return -1;
81 : :
82 : 92 : if (get_length (entry->src) > 1)
83 : 4 : {
84 : : gunichar src_1;
85 : :
86 : 12 : src_1 = get_src_char (src_table, entry->src, 1);
87 : :
88 : 12 : if (key[1] > src_1)
89 : 4 : return 1;
90 : 8 : else if (key[1] < src_1)
91 : 4 : return -1;
92 : : }
93 : 80 : else if (key[1])
94 : 20 : return 1;
95 : :
96 : 64 : return 0;
97 : : }
98 : :
99 : : static const gchar *
100 : 115 : lookup_in_mapping (const struct mapping_entry *mapping,
101 : : gint mapping_size,
102 : : const gunichar *key,
103 : : gint *result_len,
104 : : gint *key_consumed)
105 : : {
106 : : const struct mapping_entry *hit;
107 : :
108 : 115 : hit = bsearch (key, mapping, mapping_size, sizeof (struct mapping_entry), compare_mapping_entry);
109 : :
110 : 115 : if (hit == NULL)
111 : 51 : return NULL;
112 : :
113 : 64 : *key_consumed = get_length (hit->src);
114 : 64 : *result_len = get_length (hit->ascii);
115 : :
116 : 64 : return get_ascii_item(ascii_table, hit->ascii);
117 : : }
118 : :
119 : : static const gchar *
120 : 87 : lookup_in_chain (const guint8 *chain,
121 : : const gunichar *key,
122 : : gint *result_len,
123 : : gint *key_consumed)
124 : : {
125 : : const gchar *result;
126 : :
127 : 137 : while (*chain != 0xff)
128 : : {
129 : 114 : result = lookup_in_item (*chain, key, result_len, key_consumed);
130 : :
131 : 114 : if (result)
132 : 64 : return result;
133 : :
134 : 50 : chain++;
135 : : }
136 : :
137 : 23 : return NULL;
138 : : }
139 : :
140 : : static const gchar *
141 : 202 : lookup_in_item (guint item_id,
142 : : const gunichar *key,
143 : : gint *result_len,
144 : : gint *key_consumed)
145 : : {
146 : 202 : if (item_id & 0x80)
147 : : {
148 : 87 : const guint8 *chain = chains_table + chain_starts[item_id & 0x7f];
149 : :
150 : 87 : return lookup_in_chain (chain, key, result_len, key_consumed);
151 : : }
152 : : else
153 : : {
154 : 115 : const struct mapping_range *range = &mapping_ranges[item_id];
155 : :
156 : 115 : return lookup_in_mapping (mappings_table + range->start, range->length, key, result_len, key_consumed);
157 : : }
158 : : }
159 : :
160 : : static gint
161 : 158 : compare_locale_entry (gconstpointer user_data,
162 : : gconstpointer data)
163 : : {
164 : 158 : const struct locale_entry *entry = data;
165 : 158 : const gchar *key = user_data;
166 : :
167 : 158 : return strcmp (key, &locale_names[entry->name_offset]);
168 : : }
169 : :
170 : : static gboolean
171 : 27 : lookup_item_id_for_one_locale (const gchar *key,
172 : : guint *item_id)
173 : : {
174 : : const struct locale_entry *hit;
175 : :
176 : 27 : hit = bsearch (key, locale_index, G_N_ELEMENTS (locale_index), sizeof (struct locale_entry), compare_locale_entry);
177 : :
178 : 27 : if (hit == NULL)
179 : 15 : return FALSE;
180 : :
181 : 12 : *item_id = hit->item_id;
182 : 12 : return TRUE;
183 : : }
184 : :
185 : : static guint
186 : 22 : lookup_item_id_for_locale (const gchar *locale)
187 : : {
188 : : gchar key[MAX_LOCALE_NAME + 1];
189 : : const gchar *language;
190 : : size_t language_len;
191 : 22 : const gchar *territory = NULL;
192 : 22 : size_t territory_len = 0;
193 : 22 : const gchar *modifier = NULL;
194 : 22 : size_t modifier_len = 0;
195 : : const gchar *next_char;
196 : : guint id;
197 : :
198 : : /* As per POSIX, a valid locale looks like:
199 : : *
200 : : * language[_territory][.codeset][@modifier]
201 : : */
202 : 22 : language = locale;
203 : 22 : language_len = strcspn (language, "_.@");
204 : 22 : next_char = language + language_len;
205 : :
206 : 22 : if (*next_char == '_')
207 : : {
208 : 10 : territory = next_char;
209 : 10 : territory_len = strcspn (territory + 1, "_.@") + 1;
210 : 10 : next_char = territory + territory_len;
211 : : }
212 : :
213 : 22 : if (*next_char == '.')
214 : : {
215 : : const gchar *codeset;
216 : : guint codeset_len;
217 : :
218 : 5 : codeset = next_char;
219 : 5 : codeset_len = strcspn (codeset + 1, "_.@") + 1;
220 : 5 : next_char = codeset + codeset_len;
221 : : }
222 : :
223 : 22 : if (*next_char == '@')
224 : : {
225 : 5 : modifier = next_char;
226 : 5 : modifier_len = strcspn (modifier + 1, "_.@") + 1;
227 : 5 : next_char = modifier + modifier_len;
228 : : }
229 : :
230 : : /* What madness is this? */
231 : 22 : if (language_len == 0 || *next_char)
232 : 2 : return default_item_id;
233 : :
234 : : /* We are not interested in codeset.
235 : : *
236 : : * For this locale:
237 : : *
238 : : * aa_BB@cc
239 : : *
240 : : * try in this order:
241 : : *
242 : : * Note: we have no locales of the form aa_BB@cc in the database.
243 : : *
244 : : * 1. aa@cc
245 : : * 2. aa_BB
246 : : * 3. aa
247 : : */
248 : :
249 : : /* 1. */
250 : 20 : if (modifier_len && language_len + modifier_len <= MAX_LOCALE_NAME)
251 : : {
252 : 3 : memcpy (key, language, language_len);
253 : 3 : memcpy (key + language_len, modifier, modifier_len);
254 : 3 : key[language_len + modifier_len] = '\0';
255 : :
256 : 3 : if (lookup_item_id_for_one_locale (key, &id))
257 : 1 : return id;
258 : : }
259 : :
260 : : /* 2. */
261 : 19 : if (territory_len && language_len + territory_len <= MAX_LOCALE_NAME)
262 : : {
263 : 8 : memcpy (key, language, language_len);
264 : 8 : memcpy (key + language_len, territory, territory_len);
265 : 8 : key[language_len + territory_len] = '\0';
266 : :
267 : 8 : if (lookup_item_id_for_one_locale (key, &id))
268 : 1 : return id;
269 : : }
270 : :
271 : : /* 3. */
272 : 18 : if (language_len <= MAX_LOCALE_NAME)
273 : : {
274 : 16 : memcpy (key, language, language_len);
275 : 16 : key[language_len] = '\0';
276 : :
277 : 16 : if (lookup_item_id_for_one_locale (key, &id))
278 : 10 : return id;
279 : : }
280 : :
281 : 8 : return default_item_id;
282 : : }
283 : :
284 : : static guint
285 : 22 : get_default_item_id (void)
286 : : {
287 : : static guint item_id;
288 : : static gboolean done;
289 : :
290 : : /* Doesn't need to be locked -- no harm in doing it twice. */
291 : 22 : if (!done)
292 : : {
293 : : const gchar *locale;
294 : :
295 : 4 : locale = setlocale (LC_CTYPE, NULL);
296 : 4 : item_id = lookup_item_id_for_locale (locale);
297 : 4 : done = TRUE;
298 : : }
299 : :
300 : 22 : return item_id;
301 : : }
302 : :
303 : : /**
304 : : * g_str_to_ascii:
305 : : * @str: a string, in UTF-8
306 : : * @from_locale: (nullable): the source locale, if known
307 : : *
308 : : * Transliterate @str to plain ASCII.
309 : : *
310 : : * For best results, @str should be in composed normalised form.
311 : : *
312 : : * This function performs a reasonably good set of character
313 : : * replacements. The particular set of replacements that is done may
314 : : * change by version or even by runtime environment.
315 : : *
316 : : * If the source language of @str is known, it can used to improve the
317 : : * accuracy of the translation by passing it as @from_locale. It should
318 : : * be a valid POSIX locale string (of the form
319 : : * `language[_territory][.codeset][@modifier]`).
320 : : *
321 : : * If @from_locale is %NULL then the current locale is used.
322 : : *
323 : : * If you want to do translation for no specific locale, and you want it
324 : : * to be done independently of the currently locale, specify `"C"` for
325 : : * @from_locale.
326 : : *
327 : : * Returns: a string in plain ASCII
328 : : *
329 : : * Since: 2.40
330 : : **/
331 : : gchar *
332 : 41 : g_str_to_ascii (const gchar *str,
333 : : const gchar *from_locale)
334 : : {
335 : : GString *result;
336 : : guint item_id;
337 : :
338 : 41 : g_return_val_if_fail (str != NULL, NULL);
339 : :
340 : 41 : if (g_str_is_ascii (str))
341 : 1 : return g_strdup (str);
342 : :
343 : 40 : if (from_locale)
344 : 18 : item_id = lookup_item_id_for_locale (from_locale);
345 : : else
346 : 22 : item_id = get_default_item_id ();
347 : :
348 : 40 : result = g_string_sized_new (strlen (str));
349 : :
350 : 233 : while (*str)
351 : : {
352 : : /* We only need to transliterate non-ASCII values... */
353 : 193 : if (*str & 0x80)
354 : : {
355 : : gunichar key[MAX_KEY_SIZE];
356 : : const gchar *r;
357 : : gint consumed;
358 : : gint r_len;
359 : : gunichar c;
360 : :
361 : : G_STATIC_ASSERT(MAX_KEY_SIZE == 2);
362 : :
363 : 67 : c = g_utf8_get_char (str);
364 : :
365 : : /* This is where it gets evil...
366 : : *
367 : : * We know that MAX_KEY_SIZE is 2. We also know that we
368 : : * only want to try another character if it's non-ascii.
369 : : */
370 : 67 : str = g_utf8_next_char (str);
371 : :
372 : 67 : key[0] = c;
373 : 67 : if (*str & 0x80)
374 : 25 : key[1] = g_utf8_get_char (str);
375 : : else
376 : 42 : key[1] = 0;
377 : :
378 : 67 : r = lookup_in_item (item_id, key, &r_len, &consumed);
379 : :
380 : : /* If we failed to map two characters, try again with one.
381 : : *
382 : : * gconv behaviour is a bit weird here -- it seems to
383 : : * depend in the randomness of the binary search and the
384 : : * size of the input buffer as to what result we get here.
385 : : *
386 : : * Doing it this way is more work, but should be
387 : : * more-correct.
388 : : */
389 : 67 : if (r == NULL && key[1])
390 : : {
391 : 21 : key[1] = 0;
392 : 21 : r = lookup_in_item (item_id, key, &r_len, &consumed);
393 : : }
394 : :
395 : 67 : if (r != NULL)
396 : : {
397 : 64 : g_string_append_len (result, r, r_len);
398 : 64 : if (consumed == 2)
399 : : /* If it took both then skip again */
400 : 4 : str = g_utf8_next_char (str);
401 : : }
402 : : else /* no match found */
403 : : g_string_append_c (result, '?');
404 : : }
405 : : else /* ASCII case */
406 : 126 : g_string_append_c (result, *str++);
407 : : }
408 : :
409 : 40 : return g_string_free (result, FALSE);
410 : : }
|