Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * unaccent.c
4 : : * Text search unaccent dictionary
5 : : *
6 : : * Copyright (c) 2009-2024, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * contrib/unaccent/unaccent.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : :
14 : : #include "postgres.h"
15 : :
16 : : #include "catalog/namespace.h"
17 : : #include "catalog/pg_ts_dict.h"
18 : : #include "commands/defrem.h"
19 : : #include "lib/stringinfo.h"
20 : : #include "tsearch/ts_cache.h"
21 : : #include "tsearch/ts_locale.h"
22 : : #include "tsearch/ts_public.h"
23 : : #include "utils/builtins.h"
24 : : #include "utils/lsyscache.h"
25 : : #include "utils/regproc.h"
26 : : #include "utils/syscache.h"
27 : :
5353 teodor@sigaev.ru 28 :CBC 1 : PG_MODULE_MAGIC;
29 : :
30 : : /*
31 : : * An unaccent dictionary uses a trie to find a string to replace. Each node
32 : : * of the trie is an array of 256 TrieChar structs; the N-th element of the
33 : : * array corresponds to next byte value N. That element can contain both a
34 : : * replacement string (to be used if the source string ends with this byte)
35 : : * and a link to another trie node (to be followed if there are more bytes).
36 : : *
37 : : * Note that the trie search logic pays no attention to multibyte character
38 : : * boundaries. This is OK as long as both the data entered into the trie and
39 : : * the data we're trying to look up are validly encoded; no partial-character
40 : : * matches will occur.
41 : : */
42 : : typedef struct TrieChar
43 : : {
44 : : struct TrieChar *nextChar;
45 : : char *replaceTo;
46 : : int replacelen;
47 : : } TrieChar;
48 : :
49 : : /*
50 : : * placeChar - put str into trie's structure, byte by byte.
51 : : *
52 : : * If node is NULL, we need to make a new node, which will be returned;
53 : : * otherwise the return value is the same as node.
54 : : */
55 : : static TrieChar *
3576 tgl@sss.pgh.pa.us 56 : 8894 : placeChar(TrieChar *node, const unsigned char *str, int lenstr,
57 : : const char *replaceTo, int replacelen)
58 : : {
59 : : TrieChar *curnode;
60 : :
5161 bruce@momjian.us 61 [ + + ]: 8894 : if (!node)
3576 tgl@sss.pgh.pa.us 62 : 126 : node = (TrieChar *) palloc0(sizeof(TrieChar) * 256);
63 : :
64 [ - + ]: 8894 : Assert(lenstr > 0); /* else str[0] doesn't exist */
65 : :
5353 teodor@sigaev.ru 66 : 8894 : curnode = node + *str;
67 : :
3576 tgl@sss.pgh.pa.us 68 [ + + ]: 8894 : if (lenstr <= 1)
69 : : {
5161 bruce@momjian.us 70 [ - + ]: 3300 : if (curnode->replaceTo)
3576 tgl@sss.pgh.pa.us 71 [ # # ]:UBC 0 : ereport(WARNING,
72 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
73 : : errmsg("duplicate source strings, first one will be used")));
74 : : else
75 : : {
5353 teodor@sigaev.ru 76 :CBC 3300 : curnode->replacelen = replacelen;
3576 tgl@sss.pgh.pa.us 77 : 3300 : curnode->replaceTo = (char *) palloc(replacelen);
5353 teodor@sigaev.ru 78 : 3300 : memcpy(curnode->replaceTo, replaceTo, replacelen);
79 : : }
80 : : }
81 : : else
82 : : {
3576 tgl@sss.pgh.pa.us 83 : 5594 : curnode->nextChar = placeChar(curnode->nextChar, str + 1, lenstr - 1,
84 : : replaceTo, replacelen);
85 : : }
86 : :
5353 teodor@sigaev.ru 87 : 8894 : return node;
88 : : }
89 : :
90 : : /*
91 : : * initTrie - create trie from file.
92 : : *
93 : : * Function converts UTF8-encoded file into current encoding.
94 : : */
95 : : static TrieChar *
2357 peter_e@gmx.net 96 : 2 : initTrie(const char *filename)
97 : : {
3973 bruce@momjian.us 98 : 2 : TrieChar *volatile rootTrie = NULL;
5353 teodor@sigaev.ru 99 : 2 : MemoryContext ccxt = CurrentMemoryContext;
100 : : tsearch_readline_state trst;
101 : : volatile bool skip;
102 : :
103 : 2 : filename = get_tsearch_config_filename(filename, "rules");
104 [ - + ]: 2 : if (!tsearch_readline_begin(&trst, filename))
5353 teodor@sigaev.ru 105 [ # # ]:UBC 0 : ereport(ERROR,
106 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
107 : : errmsg("could not open unaccent file \"%s\": %m",
108 : : filename)));
109 : :
110 : : do
111 : : {
112 : : /*
113 : : * pg_do_encoding_conversion() (called by tsearch_readline()) will
114 : : * emit exception if it finds untranslatable characters in current
115 : : * locale. We just skip such lines, continuing with the next.
116 : : */
5353 teodor@sigaev.ru 117 :CBC 2 : skip = true;
118 : :
119 [ + - ]: 2 : PG_TRY();
120 : : {
121 : : char *line;
122 : :
123 [ + + ]: 3302 : while ((line = tsearch_readline(&trst)) != NULL)
124 : : {
125 : : /*----------
126 : : * The format of each line must be "src" or "src trg", where
127 : : * src and trg are sequences of one or more non-whitespace
128 : : * characters, separated by whitespace. Whitespace at start
129 : : * or end of line is ignored. If trg is omitted, an empty
130 : : * string is used as the replacement. trg can be optionally
131 : : * quoted, in which case whitespaces are included in it.
132 : : *
133 : : * We use a simple state machine, with states
134 : : * 0 initial (before src)
135 : : * 1 in src
136 : : * 2 in whitespace after src
137 : : * 3 in trg (non-quoted)
138 : : * 4 in trg (quoted)
139 : : * 5 in whitespace after trg
140 : : * -1 syntax error detected (two strings)
141 : : * -2 syntax error detected (unfinished quoted string)
142 : : *----------
143 : : */
144 : : int state;
145 : : char *ptr;
4542 tgl@sss.pgh.pa.us 146 : 3300 : char *src = NULL;
147 : 3300 : char *trg = NULL;
207 michael@paquier.xyz 148 :GNC 3300 : char *trgstore = NULL;
149 : : int ptrlen;
4542 tgl@sss.pgh.pa.us 150 :CBC 3300 : int srclen = 0;
151 : 3300 : int trglen = 0;
207 michael@paquier.xyz 152 :GNC 3300 : int trgstorelen = 0;
153 : 3300 : bool trgquoted = false;
154 : :
4542 tgl@sss.pgh.pa.us 155 :CBC 3300 : state = 0;
156 [ + + ]: 17246 : for (ptr = line; *ptr; ptr += ptrlen)
157 : : {
158 : 13946 : ptrlen = pg_mblen(ptr);
159 : : /* ignore whitespace, but end src or trg */
160 [ + + ]: 13946 : if (t_isspace(ptr))
161 : : {
162 [ + + ]: 6428 : if (state == 1)
163 : 3300 : state = 2;
164 [ + + ]: 3128 : else if (state == 3)
207 michael@paquier.xyz 165 :GNC 3032 : state = 5;
166 : : /* whitespaces are OK in quoted area */
167 [ + + ]: 6428 : if (state != 4)
168 : 6388 : continue;
169 : : }
4542 tgl@sss.pgh.pa.us 170 [ + - + + :CBC 7558 : switch (state)
+ - ]
171 : : {
172 : 3300 : case 0:
173 : : /* start of src */
174 : 3300 : src = ptr;
175 : 3300 : srclen = ptrlen;
176 : 3300 : state = 1;
177 : 3300 : break;
4542 tgl@sss.pgh.pa.us 178 :UBC 0 : case 1:
179 : : /* continue src */
180 : 0 : srclen += ptrlen;
181 : 0 : break;
4542 tgl@sss.pgh.pa.us 182 :CBC 3088 : case 2:
183 : : /* start of trg */
207 michael@paquier.xyz 184 [ + + ]:GNC 3088 : if (*ptr == '"')
185 : : {
186 : 56 : trgquoted = true;
187 : 56 : state = 4;
188 : : }
189 : : else
190 : 3032 : state = 3;
191 : :
4542 tgl@sss.pgh.pa.us 192 :CBC 3088 : trg = ptr;
193 : 3088 : trglen = ptrlen;
194 : 3088 : break;
195 : 938 : case 3:
196 : : /* continue non-quoted trg */
207 michael@paquier.xyz 197 : 938 : trglen += ptrlen;
198 : 938 : break;
207 michael@paquier.xyz 199 :GNC 232 : case 4:
200 : : /* continue quoted trg */
4542 tgl@sss.pgh.pa.us 201 : 232 : trglen += ptrlen;
202 : :
203 : : /*
204 : : * If this is a quote, consider it as the end of
205 : : * trg except if the follow-up character is itself
206 : : * a quote.
207 : : */
207 michael@paquier.xyz 208 [ + + ]: 232 : if (*ptr == '"')
209 : : {
210 [ + + ]: 72 : if (*(ptr + 1) == '"')
211 : : {
212 : 16 : ptr++;
213 : 16 : trglen += 1;
214 : : }
215 : : else
216 : 56 : state = 5;
217 : : }
4542 tgl@sss.pgh.pa.us 218 : 232 : break;
4542 tgl@sss.pgh.pa.us 219 :UBC 0 : default:
220 : : /* bogus line format */
221 : 0 : state = -1;
222 : 0 : break;
223 : : }
224 : : }
225 : :
3576 tgl@sss.pgh.pa.us 226 [ + - + + ]:CBC 3300 : if (state == 1 || state == 2)
227 : : {
228 : : /* trg was omitted, so use "" */
229 : 212 : trg = "";
230 : 212 : trglen = 0;
231 : : }
232 : :
233 : : /* If still in a quoted area, fallback to an error */
207 michael@paquier.xyz 234 [ - + ]:GNC 3300 : if (state == 4)
207 michael@paquier.xyz 235 :UNC 0 : state = -2;
236 : :
237 : : /* If trg was quoted, remove its quotes and unescape it */
207 michael@paquier.xyz 238 [ + + + - ]:GNC 3300 : if (trgquoted && state > 0)
239 : : {
240 : : /* Ignore first and end quotes */
202 241 : 56 : trgstore = (char *) palloc(sizeof(char) * (trglen - 2));
207 242 : 56 : trgstorelen = 0;
243 [ + + ]: 232 : for (int i = 1; i < trglen - 1; i++)
244 : : {
245 : 176 : trgstore[trgstorelen] = trg[i];
246 : 176 : trgstorelen++;
247 : : /* skip second double quotes */
248 [ + + + - ]: 176 : if (trg[i] == '"' && trg[i + 1] == '"')
249 : 16 : i++;
250 : : }
251 : : }
252 : : else
253 : : {
202 254 : 3244 : trgstore = (char *) palloc(sizeof(char) * trglen);
207 255 : 3244 : trgstorelen = trglen;
256 : 3244 : memcpy(trgstore, trg, trgstorelen);
257 : : }
258 : :
3576 tgl@sss.pgh.pa.us 259 [ + - ]:CBC 3300 : if (state > 0)
3994 heikki.linnakangas@i 260 : 3300 : rootTrie = placeChar(rootTrie,
261 : : (unsigned char *) src, srclen,
262 : : trgstore, trgstorelen);
207 michael@paquier.xyz 263 [ # # ]:UNC 0 : else if (state == -1)
3576 tgl@sss.pgh.pa.us 264 [ # # ]:UBC 0 : ereport(WARNING,
265 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
266 : : errmsg("invalid syntax: more than two strings in unaccent rule")));
207 michael@paquier.xyz 267 [ # # ]:UNC 0 : else if (state == -2)
268 [ # # ]: 0 : ereport(WARNING,
269 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
270 : : errmsg("invalid syntax: unfinished quoted string in unaccent rule")));
271 : :
207 michael@paquier.xyz 272 :GNC 3300 : pfree(trgstore);
5353 teodor@sigaev.ru 273 :CBC 3300 : pfree(line);
274 : : }
4542 tgl@sss.pgh.pa.us 275 : 2 : skip = false;
276 : : }
5353 teodor@sigaev.ru 277 :UBC 0 : PG_CATCH();
278 : : {
279 : : ErrorData *errdata;
280 : : MemoryContext ecxt;
281 : :
282 : 0 : ecxt = MemoryContextSwitchTo(ccxt);
283 : 0 : errdata = CopyErrorData();
284 [ # # ]: 0 : if (errdata->sqlerrcode == ERRCODE_UNTRANSLATABLE_CHARACTER)
285 : : {
286 : 0 : FlushErrorState();
287 : : }
288 : : else
289 : : {
290 : 0 : MemoryContextSwitchTo(ecxt);
291 : 0 : PG_RE_THROW();
292 : : }
293 : : }
5353 teodor@sigaev.ru 294 [ - + ]:CBC 2 : PG_END_TRY();
295 : : }
5161 bruce@momjian.us 296 [ - + ]: 2 : while (skip);
297 : :
5353 teodor@sigaev.ru 298 : 2 : tsearch_readline_end(&trst);
299 : :
3994 heikki.linnakangas@i 300 : 2 : return rootTrie;
301 : : }
302 : :
303 : : /*
304 : : * findReplaceTo - find longest possible match in trie
305 : : *
306 : : * On success, returns pointer to ending subnode, plus length of matched
307 : : * source string in *p_matchlen. On failure, returns NULL.
308 : : */
309 : : static TrieChar *
3576 tgl@sss.pgh.pa.us 310 : 79 : findReplaceTo(TrieChar *node, const unsigned char *src, int srclen,
311 : : int *p_matchlen)
312 : : {
313 : 79 : TrieChar *result = NULL;
314 : 79 : int matchlen = 0;
315 : :
316 : 79 : *p_matchlen = 0; /* prevent uninitialized-variable warnings */
317 : :
318 [ + + + - ]: 226 : while (node && matchlen < srclen)
319 : : {
320 : 147 : node = node + src[matchlen];
321 : 147 : matchlen++;
322 : :
323 [ + + ]: 147 : if (node->replaceTo)
324 : : {
325 : 37 : result = node;
326 : 37 : *p_matchlen = matchlen;
327 : : }
328 : :
5353 teodor@sigaev.ru 329 : 147 : node = node->nextChar;
330 : : }
331 : :
3576 tgl@sss.pgh.pa.us 332 : 79 : return result;
333 : : }
334 : :
5353 teodor@sigaev.ru 335 : 2 : PG_FUNCTION_INFO_V1(unaccent_init);
336 : : Datum
337 : 2 : unaccent_init(PG_FUNCTION_ARGS)
338 : : {
5161 bruce@momjian.us 339 : 2 : List *dictoptions = (List *) PG_GETARG_POINTER(0);
3994 heikki.linnakangas@i 340 : 2 : TrieChar *rootTrie = NULL;
5161 bruce@momjian.us 341 : 2 : bool fileloaded = false;
342 : : ListCell *l;
343 : :
5353 teodor@sigaev.ru 344 [ + - + + : 4 : foreach(l, dictoptions)
+ + ]
345 : : {
346 : 2 : DefElem *defel = (DefElem *) lfirst(l);
347 : :
2270 tgl@sss.pgh.pa.us 348 [ + - ]: 2 : if (strcmp(defel->defname, "rules") == 0)
349 : : {
5353 teodor@sigaev.ru 350 [ - + ]: 2 : if (fileloaded)
5353 teodor@sigaev.ru 351 [ # # ]:UBC 0 : ereport(ERROR,
352 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
353 : : errmsg("multiple Rules parameters")));
3994 heikki.linnakangas@i 354 :CBC 2 : rootTrie = initTrie(defGetString(defel));
5161 bruce@momjian.us 355 : 2 : fileloaded = true;
356 : : }
357 : : else
358 : : {
5353 teodor@sigaev.ru 359 [ # # ]:UBC 0 : ereport(ERROR,
360 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
361 : : errmsg("unrecognized Unaccent parameter: \"%s\"",
362 : : defel->defname)));
363 : : }
364 : : }
365 : :
5353 teodor@sigaev.ru 366 [ - + ]:CBC 2 : if (!fileloaded)
367 : : {
5353 teodor@sigaev.ru 368 [ # # ]:UBC 0 : ereport(ERROR,
369 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
370 : : errmsg("missing Rules parameter")));
371 : : }
372 : :
3994 heikki.linnakangas@i 373 :CBC 2 : PG_RETURN_POINTER(rootTrie);
374 : : }
375 : :
5353 teodor@sigaev.ru 376 : 2 : PG_FUNCTION_INFO_V1(unaccent_lexize);
377 : : Datum
378 : 28 : unaccent_lexize(PG_FUNCTION_ARGS)
379 : : {
3994 heikki.linnakangas@i 380 : 28 : TrieChar *rootTrie = (TrieChar *) PG_GETARG_POINTER(0);
5161 bruce@momjian.us 381 : 28 : char *srcchar = (char *) PG_GETARG_POINTER(1);
5353 teodor@sigaev.ru 382 : 28 : int32 len = PG_GETARG_INT32(2);
3575 tgl@sss.pgh.pa.us 383 : 28 : char *srcstart = srcchar;
384 : : TSLexeme *res;
385 : : StringInfoData buf;
386 : :
387 : : /* we allocate storage for the buffer only if needed */
388 : 28 : buf.data = NULL;
389 : :
3576 390 [ + + ]: 107 : while (len > 0)
391 : : {
392 : : TrieChar *node;
393 : : int matchlen;
394 : :
395 : 79 : node = findReplaceTo(rootTrie, (unsigned char *) srcchar, len,
396 : : &matchlen);
5161 bruce@momjian.us 397 [ + + + - ]: 79 : if (node && node->replaceTo)
398 : : {
3575 tgl@sss.pgh.pa.us 399 [ + + ]: 37 : if (buf.data == NULL)
400 : : {
401 : : /* initialize buffer */
402 : 25 : initStringInfo(&buf);
403 : : /* insert any data we already skipped over */
5161 bruce@momjian.us 404 [ + + ]: 25 : if (srcchar != srcstart)
3575 tgl@sss.pgh.pa.us 405 : 6 : appendBinaryStringInfo(&buf, srcstart, srcchar - srcstart);
406 : : }
407 : 37 : appendBinaryStringInfo(&buf, node->replaceTo, node->replacelen);
408 : : }
409 : : else
410 : : {
3576 411 : 42 : matchlen = pg_mblen(srcchar);
3575 412 [ + + ]: 42 : if (buf.data != NULL)
413 : 18 : appendBinaryStringInfo(&buf, srcchar, matchlen);
414 : : }
415 : :
3576 416 : 79 : srcchar += matchlen;
417 : 79 : len -= matchlen;
418 : : }
419 : :
420 : : /* return a result only if we made at least one substitution */
3575 421 [ + + ]: 28 : if (buf.data != NULL)
422 : : {
423 : 25 : res = (TSLexeme *) palloc0(sizeof(TSLexeme) * 2);
424 : 25 : res->lexeme = buf.data;
425 : 25 : res->flags = TSL_FILTER;
426 : : }
427 : : else
428 : 3 : res = NULL;
429 : :
5353 teodor@sigaev.ru 430 : 28 : PG_RETURN_POINTER(res);
431 : : }
432 : :
433 : : /*
434 : : * Function-like wrapper for dictionary
435 : : */
436 : 4 : PG_FUNCTION_INFO_V1(unaccent_dict);
437 : : Datum
438 : 19 : unaccent_dict(PG_FUNCTION_ARGS)
439 : : {
440 : : text *str;
441 : : int strArg;
442 : : Oid dictOid;
443 : : TSDictionaryCacheEntry *dict;
444 : : TSLexeme *res;
445 : :
446 [ + + ]: 19 : if (PG_NARGS() == 1)
447 : : {
448 : : /*
449 : : * Use the "unaccent" dictionary that is in the same schema that this
450 : : * function is in.
451 : : */
2047 tgl@sss.pgh.pa.us 452 : 10 : Oid procnspid = get_func_namespace(fcinfo->flinfo->fn_oid);
453 : 10 : const char *dictname = "unaccent";
454 : :
1972 andres@anarazel.de 455 : 10 : dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
456 : : PointerGetDatum(dictname),
457 : : ObjectIdGetDatum(procnspid));
2047 tgl@sss.pgh.pa.us 458 [ - + ]: 10 : if (!OidIsValid(dictOid))
2047 tgl@sss.pgh.pa.us 459 [ # # ]:UBC 0 : ereport(ERROR,
460 : : (errcode(ERRCODE_UNDEFINED_OBJECT),
461 : : errmsg("text search dictionary \"%s.%s\" does not exist",
462 : : get_namespace_name(procnspid), dictname)));
5353 teodor@sigaev.ru 463 :CBC 10 : strArg = 0;
464 : : }
465 : : else
466 : : {
467 : 9 : dictOid = PG_GETARG_OID(0);
468 : 9 : strArg = 1;
469 : : }
2590 noah@leadboat.com 470 : 19 : str = PG_GETARG_TEXT_PP(strArg);
471 : :
5353 teodor@sigaev.ru 472 : 19 : dict = lookup_ts_dictionary_cache(dictOid);
473 : :
474 [ - + - - : 19 : res = (TSLexeme *) DatumGetPointer(FunctionCall4(&(dict->lexize),
- - - - -
+ - + ]
475 : : PointerGetDatum(dict->dictData),
476 : : PointerGetDatum(VARDATA_ANY(str)),
477 : : Int32GetDatum(VARSIZE_ANY_EXHDR(str)),
478 : : PointerGetDatum(NULL)));
479 : :
480 [ - + ]: 19 : PG_FREE_IF_COPY(str, strArg);
481 : :
5161 bruce@momjian.us 482 [ + + ]: 19 : if (res == NULL)
483 : : {
5353 teodor@sigaev.ru 484 : 2 : PG_RETURN_TEXT_P(PG_GETARG_TEXT_P_COPY(strArg));
485 : : }
5161 bruce@momjian.us 486 [ - + ]: 17 : else if (res->lexeme == NULL)
487 : : {
5353 teodor@sigaev.ru 488 :UBC 0 : pfree(res);
489 : 0 : PG_RETURN_TEXT_P(PG_GETARG_TEXT_P_COPY(strArg));
490 : : }
491 : : else
492 : : {
5161 bruce@momjian.us 493 :CBC 17 : text *txt = cstring_to_text(res->lexeme);
494 : :
5353 teodor@sigaev.ru 495 : 17 : pfree(res->lexeme);
496 : 17 : pfree(res);
497 : :
498 : 17 : PG_RETURN_TEXT_P(txt);
499 : : }
500 : : }
|