Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * unaccent.c
4 : * Text search unaccent dictionary
5 : *
6 : * Copyright (c) 2009-2023, 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 :
28 2 : 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 *
56 17788 : placeChar(TrieChar *node, const unsigned char *str, int lenstr,
57 : const char *replaceTo, int replacelen)
58 : {
59 : TrieChar *curnode;
60 :
61 17788 : if (!node)
62 252 : node = (TrieChar *) palloc0(sizeof(TrieChar) * 256);
63 :
64 : Assert(lenstr > 0); /* else str[0] doesn't exist */
65 :
66 17788 : curnode = node + *str;
67 :
68 17788 : if (lenstr <= 1)
69 : {
70 6600 : if (curnode->replaceTo)
71 0 : ereport(WARNING,
72 : (errcode(ERRCODE_CONFIG_FILE_ERROR),
73 : errmsg("duplicate source strings, first one will be used")));
74 : else
75 : {
76 6600 : curnode->replacelen = replacelen;
77 6600 : curnode->replaceTo = (char *) palloc(replacelen);
78 6600 : memcpy(curnode->replaceTo, replaceTo, replacelen);
79 : }
80 : }
81 : else
82 : {
83 11188 : curnode->nextChar = placeChar(curnode->nextChar, str + 1, lenstr - 1,
84 : replaceTo, replacelen);
85 : }
86 :
87 17788 : 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 *
96 4 : initTrie(const char *filename)
97 : {
98 4 : TrieChar *volatile rootTrie = NULL;
99 4 : MemoryContext ccxt = CurrentMemoryContext;
100 : tsearch_readline_state trst;
101 : volatile bool skip;
102 :
103 4 : filename = get_tsearch_config_filename(filename, "rules");
104 4 : if (!tsearch_readline_begin(&trst, filename))
105 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 : */
117 4 : skip = true;
118 :
119 4 : PG_TRY();
120 : {
121 : char *line;
122 :
123 6604 : 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.
131 : *
132 : * We use a simple state machine, with states
133 : * 0 initial (before src)
134 : * 1 in src
135 : * 2 in whitespace after src
136 : * 3 in trg
137 : * 4 in whitespace after trg
138 : * -1 syntax error detected
139 : *----------
140 : */
141 : int state;
142 : char *ptr;
143 6600 : char *src = NULL;
144 6600 : char *trg = NULL;
145 : int ptrlen;
146 6600 : int srclen = 0;
147 6600 : int trglen = 0;
148 :
149 6600 : state = 0;
150 34268 : for (ptr = line; *ptr; ptr += ptrlen)
151 : {
152 27668 : ptrlen = pg_mblen(ptr);
153 : /* ignore whitespace, but end src or trg */
154 27668 : if (t_isspace(ptr))
155 : {
156 12856 : if (state == 1)
157 6600 : state = 2;
158 6256 : else if (state == 3)
159 6176 : state = 4;
160 12856 : continue;
161 : }
162 14812 : switch (state)
163 : {
164 6600 : case 0:
165 : /* start of src */
166 6600 : src = ptr;
167 6600 : srclen = ptrlen;
168 6600 : state = 1;
169 6600 : break;
170 0 : case 1:
171 : /* continue src */
172 0 : srclen += ptrlen;
173 0 : break;
174 6176 : case 2:
175 : /* start of trg */
176 6176 : trg = ptr;
177 6176 : trglen = ptrlen;
178 6176 : state = 3;
179 6176 : break;
180 2036 : case 3:
181 : /* continue trg */
182 2036 : trglen += ptrlen;
183 2036 : break;
184 0 : default:
185 : /* bogus line format */
186 0 : state = -1;
187 0 : break;
188 : }
189 : }
190 :
191 6600 : if (state == 1 || state == 2)
192 : {
193 : /* trg was omitted, so use "" */
194 424 : trg = "";
195 424 : trglen = 0;
196 : }
197 :
198 6600 : if (state > 0)
199 6600 : rootTrie = placeChar(rootTrie,
200 : (unsigned char *) src, srclen,
201 : trg, trglen);
202 0 : else if (state < 0)
203 0 : ereport(WARNING,
204 : (errcode(ERRCODE_CONFIG_FILE_ERROR),
205 : errmsg("invalid syntax: more than two strings in unaccent rule")));
206 :
207 6600 : pfree(line);
208 : }
209 4 : skip = false;
210 : }
211 0 : PG_CATCH();
212 : {
213 : ErrorData *errdata;
214 : MemoryContext ecxt;
215 :
216 0 : ecxt = MemoryContextSwitchTo(ccxt);
217 0 : errdata = CopyErrorData();
218 0 : if (errdata->sqlerrcode == ERRCODE_UNTRANSLATABLE_CHARACTER)
219 : {
220 0 : FlushErrorState();
221 : }
222 : else
223 : {
224 0 : MemoryContextSwitchTo(ecxt);
225 0 : PG_RE_THROW();
226 : }
227 : }
228 4 : PG_END_TRY();
229 : }
230 4 : while (skip);
231 :
232 4 : tsearch_readline_end(&trst);
233 :
234 4 : return rootTrie;
235 : }
236 :
237 : /*
238 : * findReplaceTo - find longest possible match in trie
239 : *
240 : * On success, returns pointer to ending subnode, plus length of matched
241 : * source string in *p_matchlen. On failure, returns NULL.
242 : */
243 : static TrieChar *
244 140 : findReplaceTo(TrieChar *node, const unsigned char *src, int srclen,
245 : int *p_matchlen)
246 : {
247 140 : TrieChar *result = NULL;
248 140 : int matchlen = 0;
249 :
250 140 : *p_matchlen = 0; /* prevent uninitialized-variable warnings */
251 :
252 398 : while (node && matchlen < srclen)
253 : {
254 258 : node = node + src[matchlen];
255 258 : matchlen++;
256 :
257 258 : if (node->replaceTo)
258 : {
259 62 : result = node;
260 62 : *p_matchlen = matchlen;
261 : }
262 :
263 258 : node = node->nextChar;
264 : }
265 :
266 140 : return result;
267 : }
268 :
269 4 : PG_FUNCTION_INFO_V1(unaccent_init);
270 : Datum
271 4 : unaccent_init(PG_FUNCTION_ARGS)
272 : {
273 4 : List *dictoptions = (List *) PG_GETARG_POINTER(0);
274 4 : TrieChar *rootTrie = NULL;
275 4 : bool fileloaded = false;
276 : ListCell *l;
277 :
278 8 : foreach(l, dictoptions)
279 : {
280 4 : DefElem *defel = (DefElem *) lfirst(l);
281 :
282 4 : if (strcmp(defel->defname, "rules") == 0)
283 : {
284 4 : if (fileloaded)
285 0 : ereport(ERROR,
286 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
287 : errmsg("multiple Rules parameters")));
288 4 : rootTrie = initTrie(defGetString(defel));
289 4 : fileloaded = true;
290 : }
291 : else
292 : {
293 0 : ereport(ERROR,
294 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
295 : errmsg("unrecognized Unaccent parameter: \"%s\"",
296 : defel->defname)));
297 : }
298 : }
299 :
300 4 : if (!fileloaded)
301 : {
302 0 : ereport(ERROR,
303 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
304 : errmsg("missing Rules parameter")));
305 : }
306 :
307 4 : PG_RETURN_POINTER(rootTrie);
308 : }
309 :
310 4 : PG_FUNCTION_INFO_V1(unaccent_lexize);
311 : Datum
312 44 : unaccent_lexize(PG_FUNCTION_ARGS)
313 : {
314 44 : TrieChar *rootTrie = (TrieChar *) PG_GETARG_POINTER(0);
315 44 : char *srcchar = (char *) PG_GETARG_POINTER(1);
316 44 : int32 len = PG_GETARG_INT32(2);
317 44 : char *srcstart = srcchar;
318 : TSLexeme *res;
319 : StringInfoData buf;
320 :
321 : /* we allocate storage for the buffer only if needed */
322 44 : buf.data = NULL;
323 :
324 184 : while (len > 0)
325 : {
326 : TrieChar *node;
327 : int matchlen;
328 :
329 140 : node = findReplaceTo(rootTrie, (unsigned char *) srcchar, len,
330 : &matchlen);
331 140 : if (node && node->replaceTo)
332 : {
333 62 : if (buf.data == NULL)
334 : {
335 : /* initialize buffer */
336 38 : initStringInfo(&buf);
337 : /* insert any data we already skipped over */
338 38 : if (srcchar != srcstart)
339 6 : appendBinaryStringInfo(&buf, srcstart, srcchar - srcstart);
340 : }
341 62 : appendBinaryStringInfo(&buf, node->replaceTo, node->replacelen);
342 : }
343 : else
344 : {
345 78 : matchlen = pg_mblen(srcchar);
346 78 : if (buf.data != NULL)
347 36 : appendBinaryStringInfo(&buf, srcchar, matchlen);
348 : }
349 :
350 140 : srcchar += matchlen;
351 140 : len -= matchlen;
352 : }
353 :
354 : /* return a result only if we made at least one substitution */
355 44 : if (buf.data != NULL)
356 : {
357 38 : res = (TSLexeme *) palloc0(sizeof(TSLexeme) * 2);
358 38 : res->lexeme = buf.data;
359 38 : res->flags = TSL_FILTER;
360 : }
361 : else
362 6 : res = NULL;
363 :
364 44 : PG_RETURN_POINTER(res);
365 : }
366 :
367 : /*
368 : * Function-like wrapper for dictionary
369 : */
370 8 : PG_FUNCTION_INFO_V1(unaccent_dict);
371 : Datum
372 30 : unaccent_dict(PG_FUNCTION_ARGS)
373 : {
374 : text *str;
375 : int strArg;
376 : Oid dictOid;
377 : TSDictionaryCacheEntry *dict;
378 : TSLexeme *res;
379 :
380 30 : if (PG_NARGS() == 1)
381 : {
382 : /*
383 : * Use the "unaccent" dictionary that is in the same schema that this
384 : * function is in.
385 : */
386 16 : Oid procnspid = get_func_namespace(fcinfo->flinfo->fn_oid);
387 16 : const char *dictname = "unaccent";
388 :
389 16 : dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
390 : PointerGetDatum(dictname),
391 : ObjectIdGetDatum(procnspid));
392 16 : if (!OidIsValid(dictOid))
393 0 : ereport(ERROR,
394 : (errcode(ERRCODE_UNDEFINED_OBJECT),
395 : errmsg("text search dictionary \"%s.%s\" does not exist",
396 : get_namespace_name(procnspid), dictname)));
397 16 : strArg = 0;
398 : }
399 : else
400 : {
401 14 : dictOid = PG_GETARG_OID(0);
402 14 : strArg = 1;
403 : }
404 30 : str = PG_GETARG_TEXT_PP(strArg);
405 :
406 30 : dict = lookup_ts_dictionary_cache(dictOid);
407 :
408 30 : res = (TSLexeme *) DatumGetPointer(FunctionCall4(&(dict->lexize),
409 : PointerGetDatum(dict->dictData),
410 : PointerGetDatum(VARDATA_ANY(str)),
411 : Int32GetDatum(VARSIZE_ANY_EXHDR(str)),
412 : PointerGetDatum(NULL)));
413 :
414 30 : PG_FREE_IF_COPY(str, strArg);
415 :
416 30 : if (res == NULL)
417 : {
418 4 : PG_RETURN_TEXT_P(PG_GETARG_TEXT_P_COPY(strArg));
419 : }
420 26 : else if (res->lexeme == NULL)
421 : {
422 0 : pfree(res);
423 0 : PG_RETURN_TEXT_P(PG_GETARG_TEXT_P_COPY(strArg));
424 : }
425 : else
426 : {
427 26 : text *txt = cstring_to_text(res->lexeme);
428 :
429 26 : pfree(res->lexeme);
430 26 : pfree(res);
431 :
432 26 : PG_RETURN_TEXT_P(txt);
433 : }
434 : }
|