Coverage Report

Created: 2026-06-07 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/hunspell/src/hunspell/hashmgr.cxx
Line
Count
Source
1
/* ***** BEGIN LICENSE BLOCK *****
2
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3
 *
4
 * Copyright (C) 2002-2022 Németh László
5
 *
6
 * The contents of this file are subject to the Mozilla Public License Version
7
 * 1.1 (the "License"); you may not use this file except in compliance with
8
 * the License. You may obtain a copy of the License at
9
 * http://www.mozilla.org/MPL/
10
 *
11
 * Software distributed under the License is distributed on an "AS IS" basis,
12
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13
 * for the specific language governing rights and limitations under the
14
 * License.
15
 *
16
 * Hunspell is based on MySpell which is Copyright (C) 2002 Kevin Hendricks.
17
 *
18
 * Contributor(s): David Einstein, Davide Prina, Giuseppe Modugno,
19
 * Gianluca Turconi, Simon Brouwer, Noll János, Bíró Árpád,
20
 * Goldman Eleonóra, Sarlós Tamás, Bencsáth Boldizsár, Halácsy Péter,
21
 * Dvornik László, Gefferth András, Nagy Viktor, Varga Dániel, Chris Halls,
22
 * Rene Engelhard, Bram Moolenaar, Dafydd Jones, Harri Pitkänen
23
 *
24
 * Alternatively, the contents of this file may be used under the terms of
25
 * either the GNU General Public License Version 2 or later (the "GPL"), or
26
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
 * in which case the provisions of the GPL or the LGPL are applicable instead
28
 * of those above. If you wish to allow use of your version of this file only
29
 * under the terms of either the GPL or the LGPL, and not to allow others to
30
 * use your version of this file under the terms of the MPL, indicate your
31
 * decision by deleting the provisions above and replace them with the notice
32
 * and other provisions required by the GPL or the LGPL. If you do not delete
33
 * the provisions above, a recipient may use your version of this file under
34
 * the terms of any one of the MPL, the GPL or the LGPL.
35
 *
36
 * ***** END LICENSE BLOCK ***** */
37
/*
38
 * Copyright 2002 Kevin B. Hendricks, Stratford, Ontario, Canada
39
 * And Contributors.  All rights reserved.
40
 *
41
 * Redistribution and use in source and binary forms, with or without
42
 * modification, are permitted provided that the following conditions
43
 * are met:
44
 *
45
 * 1. Redistributions of source code must retain the above copyright
46
 *    notice, this list of conditions and the following disclaimer.
47
 *
48
 * 2. Redistributions in binary form must reproduce the above copyright
49
 *    notice, this list of conditions and the following disclaimer in the
50
 *    documentation and/or other materials provided with the distribution.
51
 *
52
 * 3. All modifications to the source code must be clearly marked as
53
 *    such.  Binary redistributions based on modified source code
54
 *    must be clearly marked as modified versions in the documentation
55
 *    and/or other materials provided with the distribution.
56
 *
57
 * THIS SOFTWARE IS PROVIDED BY KEVIN B. HENDRICKS AND CONTRIBUTORS
58
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
59
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
60
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
61
 * KEVIN B. HENDRICKS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
62
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
63
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
64
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
65
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
66
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
67
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
68
 * SUCH DAMAGE.
69
 */
70
71
#include <algorithm>
72
#include <cassert>
73
#include <cstddef>
74
#include <cstdlib>
75
#include <cstring>
76
#include <cstdio>
77
#include <cctype>
78
#include <limits>
79
#include <sstream>
80
#include <type_traits>
81
#if __cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
82
#include <bit>
83
#endif
84
85
#include "hashmgr.hxx"
86
#include "csutil.hxx"
87
#include "atypes.hxx"
88
#include "langnum.hxx"
89
90
// build a hash table from a munched word list
91
92
HashMgr::HashMgr(const char* tpath, const char* apath, const char* key)
93
55.9k
    : flag_mode(FLAG_CHAR)
94
55.9k
    , complexprefixes(0)
95
55.9k
    , utf8(0)
96
55.9k
    , forbiddenword(FORBIDDENWORD)  // forbidden word signing flag
97
55.9k
    , langnum(0)
98
55.9k
    , csconv(nullptr) {
99
55.9k
  load_config(apath, key);
100
55.9k
  if (!csconv)
101
54.4k
    csconv = get_current_cs(SPELL_ENCODING);
102
55.9k
  int ec = load_tables(tpath, key);
103
55.9k
  if (ec) {
104
    /* error condition - what should we do here */
105
31.5k
    fprintf(stderr, "Hash Manager Error : %d\n", ec);
106
31.5k
    free_table();
107
    //keep table size to 1 to fix possible division with zero
108
31.5k
    tableptr.resize(1, nullptr);
109
31.5k
  }
110
55.9k
}
111
112
2.79M
void HashMgr::release_flags(unsigned short* astr, bool owned) {
113
2.79M
  if (owned)
114
53.8k
    delete[] astr;
115
2.79M
}
116
117
87.4k
void HashMgr::free_table() {
118
  // now pass through hash table freeing up everything
119
  // go through column by column of the table
120
26.0M
  for (auto ptr : tableptr) {
121
26.0M
    hentry* nt = nullptr;
122
27.4M
    while (ptr) {
123
1.33M
      nt = ptr->next;
124
1.33M
      release_flags(ptr->astr, ptr->var & H_OPT_OWNFLAGS);
125
1.33M
      arena_free(ptr);
126
1.33M
      ptr = nt;
127
1.33M
    }
128
26.0M
  }
129
87.4k
  tableptr.clear();
130
87.4k
}
131
132
55.9k
HashMgr::~HashMgr() {
133
55.9k
  free_table();
134
135
55.9k
  static_assert(std::is_trivially_destructible<unsigned short>::value,
136
55.9k
                "arena_free replaces delete[]; aliasf elements must have trivial destructors");
137
55.9k
  for (auto& j : aliasf)
138
4.21k
    arena_free(j);
139
55.9k
  aliasf.clear();
140
141
55.9k
  static_assert(std::is_trivially_destructible<char>::value,
142
55.9k
                "arena_free replaces delete[]; aliasm elements must have trivial destructors");
143
55.9k
  for (auto& j : aliasm)
144
3.87k
    arena_free(j);
145
55.9k
  aliasm.clear();
146
147
#ifdef MOZILLA_CLIENT
148
  delete[] csconv;
149
#endif
150
55.9k
}
151
152
// lookup a root word in the hashtable
153
154
5.25G
struct hentry* HashMgr::lookup(const char* word, size_t len) const {
155
5.25G
  struct hentry* dp = tableptr[hash(word, len)];
156
5.25G
  if (!dp)
157
4.58G
    return nullptr;
158
1.34G
  for (; dp != nullptr; dp = dp->next) {
159
989M
    if (strcmp(word, dp->word) == 0)
160
308M
      return dp;
161
989M
  }
162
359M
  return nullptr;
163
668M
}
164
165
// add a word to the hash table (private)
166
int HashMgr::add_word(const std::string& in_word,
167
                      int wcl,
168
                      unsigned short* aff,
169
                      int al,
170
                      const std::string* in_desc,
171
                      bool onlyupcase,
172
                      int captype,
173
2.78M
                      bool own_aff) {
174
175
2.78M
  if (al > std::numeric_limits<short>::max()) {
176
17
    HUNSPELL_WARNING(stderr, "error: affix len %d is over max limit\n", al);
177
17
    release_flags(aff, own_aff);
178
17
    return 1;
179
17
  }
180
181
2.78M
  const std::string* word = &in_word;
182
2.78M
  const std::string* desc = in_desc;
183
184
2.78M
  std::string* word_copy = nullptr;
185
2.78M
  std::string* desc_copy = nullptr;
186
2.78M
  if ((!ignorechars.empty() && !has_no_ignored_chars(in_word, ignorechars)) || complexprefixes) {
187
660k
    word_copy = new std::string(in_word);
188
189
660k
    if (!ignorechars.empty()) {
190
196k
      if (utf8) {
191
76.5k
        wcl = remove_ignored_chars_utf(*word_copy, ignorechars_utf16);
192
120k
      } else {
193
120k
        remove_ignored_chars(*word_copy, ignorechars);
194
120k
      }
195
196k
    }
196
197
660k
    if (complexprefixes) {
198
521k
      if (utf8)
199
71.1k
        wcl = reverseword_utf(*word_copy);
200
450k
      else
201
450k
        reverseword(*word_copy);
202
203
521k
      if (in_desc && aliasm.empty()) {
204
115k
        desc_copy = new std::string(*in_desc);
205
206
115k
        if (complexprefixes) {
207
115k
          if (utf8)
208
17.5k
            reverseword_utf(*desc_copy);
209
98.2k
          else
210
98.2k
            reverseword(*desc_copy);
211
115k
        }
212
115k
        desc = desc_copy;
213
115k
      }
214
521k
    }
215
216
660k
    word = word_copy;
217
660k
  }
218
219
  // limit of hp->blen
220
2.78M
  if (word->size() > std::numeric_limits<unsigned short>::max()) {
221
324
    HUNSPELL_WARNING(stderr, "error: word len %ld is over max limit\n", word->size());
222
324
    delete desc_copy;
223
324
    delete word_copy;
224
324
    release_flags(aff, own_aff);
225
324
    return 1;
226
324
  }
227
228
2.78M
  bool upcasehomonym = false;
229
2.78M
  int descl = desc ? (!aliasm.empty() ? sizeof(char*) : desc->size() + 1) : 0;
230
  // variable-length hash record with word and optional fields
231
2.78M
  auto hp =
232
2.78M
      (struct hentry*)arena_alloc(sizeof(struct hentry) + word->size() + descl,
233
2.78M
                                  alignof(struct hentry));
234
2.78M
  if (!hp) {
235
0
    delete desc_copy;
236
0
    delete word_copy;
237
0
    release_flags(aff, own_aff);
238
0
    return 1;
239
0
  }
240
241
2.78M
  char* hpw = hp->word;
242
2.78M
  memcpy(hpw, word->data(), word->size());
243
2.78M
  hpw[word->size()] = 0;
244
245
2.78M
  int i = hash(hpw, word->size());
246
247
2.78M
  hp->blen = (unsigned short)word->size();
248
2.78M
  hp->clen = (unsigned short)wcl;
249
2.78M
  hp->alen = (short)al;
250
2.78M
  hp->astr = aff;
251
2.78M
  hp->next = nullptr;
252
2.78M
  hp->next_homonym = nullptr;
253
2.78M
  hp->var = (captype == INITCAP) ? H_OPT_INITCAP : 0;
254
2.78M
  if (own_aff)
255
42.8k
    hp->var |= H_OPT_OWNFLAGS;
256
257
  // store the description string or its pointer
258
2.78M
  if (desc) {
259
760k
    hp->var |= H_OPT;
260
760k
    if (!aliasm.empty()) {
261
56.9k
      hp->var |= H_OPT_ALIASM;
262
56.9k
      store_pointer(hpw + word->size() + 1, get_aliasm(atoi(desc->c_str())));
263
703k
    } else {
264
703k
      strcpy(hpw + word->size() + 1, desc->c_str());
265
703k
    }
266
760k
    if (HENTRY_FIND(hp, MORPH_PHON)) {
267
74.1k
      hp->var |= H_OPT_PHON;
268
      // store ph: fields (pronounciation, misspellings, old orthography etc.)
269
      // of a morphological description in reptable to use in REP replacements.
270
74.1k
      size_t predicted = tableptr.size() / MORPH_PHON_RATIO;
271
74.1k
      if (reptable.capacity() < predicted)
272
7.14k
          reptable.reserve(predicted);
273
74.1k
      std::string fields = HENTRY_DATA(hp);
274
74.1k
      std::string::const_iterator iter = fields.begin(), start_piece = mystrsep(fields, iter);
275
204k
      while (start_piece != fields.end()) {
276
130k
        if (std::string(start_piece, iter).find(MORPH_PHON) == 0) {
277
72.9k
          std::string ph = std::string(start_piece, iter).substr(sizeof MORPH_PHON - 1);
278
72.9k
          if (!ph.empty()) {
279
66.2k
            std::vector<w_char> w;
280
66.2k
            size_t strippatt;
281
66.2k
            std::string wordpart;
282
            // dictionary based REP replacement, separated by "->"
283
            // for example "pretty ph:prity ph:priti->pretti" to handle
284
            // both prity -> pretty and pritier -> prettiest suggestions.
285
66.2k
            if (((strippatt = ph.find("->")) != std::string::npos) &&
286
5.84k
                    (strippatt > 0) && (strippatt < ph.size() - 2)) {
287
4.93k
                wordpart = ph.substr(strippatt + 2);
288
4.93k
                ph.erase(ph.begin() + strippatt, ph.end());
289
4.93k
            } else
290
61.3k
                wordpart = in_word;
291
            // when the ph: field ends with the character *,
292
            // strip last character of the pattern and the replacement
293
            // to match in REP suggestions also at character changes,
294
            // for example, "pretty ph:prity*" results "prit->prett"
295
            // REP replacement instead of "prity->pretty", to get
296
            // prity->pretty and pritiest->prettiest suggestions.
297
66.2k
            if (ph.at(ph.size()-1) == '*') {
298
9.77k
              strippatt = 1;
299
9.77k
              size_t stripword = 0;
300
9.77k
              if (utf8) {
301
11.2k
                while ((strippatt < ph.size()) &&
302
10.1k
                  is_utf8_cont(ph.at(ph.size()-strippatt-1)))
303
5.71k
                     ++strippatt;
304
8.28k
                while ((stripword < wordpart.size()) &&
305
7.14k
                  is_utf8_cont(wordpart.at(wordpart.size()-stripword-1)))
306
2.79k
                     ++stripword;
307
5.49k
              }
308
9.77k
              ++strippatt;
309
9.77k
              ++stripword;
310
9.77k
              if ((ph.size() > strippatt) && (wordpart.size() > stripword)) {
311
5.89k
                ph.erase(ph.size()-strippatt, strippatt);
312
5.89k
                wordpart.erase(wordpart.size()-stripword, stripword);
313
5.89k
              }
314
9.77k
            }
315
            // capitalize lowercase pattern for capitalized words to support
316
            // good suggestions also for capitalized misspellings, eg.
317
            // Wednesday ph:wendsay
318
            // results wendsay -> Wednesday and Wendsay -> Wednesday, too.
319
66.2k
            if (captype == INITCAP) {
320
22.8k
              std::string ph_capitalized;
321
22.8k
              if (utf8) {
322
7.86k
                u8_u16(w, ph);
323
7.86k
                if (get_captype_utf8(w, langnum) == NOCAP) {
324
6.45k
                  mkinitcap_utf(w, langnum);
325
6.45k
                  u16_u8(ph_capitalized, w);
326
6.45k
                }
327
15.0k
              } else if (get_captype(ph, csconv) == NOCAP)
328
9.57k
                  mkinitcap(ph_capitalized, csconv);
329
330
22.8k
              if (!ph_capitalized.empty()) {
331
                // add also lowercase word in the case of German or
332
                // Hungarian to support lowercase suggestions lowercased by
333
                // compound word generation or derivational suffixes
334
                // (for example by adjectival suffix "-i" of geographical
335
                // names in Hungarian:
336
                // Massachusetts ph:messzecsuzec
337
                // messzecsuzeci -> massachusettsi (adjective)
338
                // For lowercasing by conditional PFX rules, see
339
                // tests/germancompounding test example or the
340
                // Hungarian dictionary.)
341
6.45k
                if (langnum == LANG_de || langnum == LANG_hu) {
342
3.37k
                  std::string wordpart_lower(wordpart);
343
3.37k
                  if (utf8) {
344
3.37k
                    u8_u16(w, wordpart_lower);
345
3.37k
                    mkallsmall_utf(w, langnum);
346
3.37k
                    u16_u8(wordpart_lower, w);
347
3.37k
                  } else {
348
0
                    mkallsmall(wordpart_lower, csconv);
349
0
                  }
350
3.37k
                  reptable.emplace_back();
351
3.37k
                  reptable.back().pattern.assign(ph);
352
3.37k
                  reptable.back().outstrings[0].assign(wordpart_lower);
353
3.37k
                }
354
6.45k
                reptable.emplace_back();
355
6.45k
                reptable.back().pattern.assign(ph_capitalized);
356
6.45k
                reptable.back().outstrings[0].assign(wordpart);
357
6.45k
              }
358
22.8k
            }
359
66.2k
            reptable.emplace_back();
360
66.2k
            reptable.back().pattern.assign(ph);
361
66.2k
            reptable.back().outstrings[0].assign(wordpart);
362
66.2k
          }
363
72.9k
        }
364
130k
        start_piece = mystrsep(fields, iter);
365
130k
      }
366
74.1k
    }
367
760k
  }
368
369
2.78M
  struct hentry* dp = tableptr[i];
370
2.78M
  if (!dp) {
371
960k
    tableptr[i] = hp;
372
960k
    delete desc_copy;
373
960k
    delete word_copy;
374
960k
    return 0;
375
960k
  }
376
16.0M
  while (dp->next != nullptr) {
377
14.3M
    if ((!dp->next_homonym) && (strcmp(hp->word, dp->word) == 0)) {
378
      // remove hidden onlyupcase homonym
379
116k
      if (!onlyupcase) {
380
101k
        if ((dp->astr) && TESTAFF(dp->astr, ONLYUPCASEFLAG, dp->alen)) {
381
927
          release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
382
927
          dp->astr = hp->astr;
383
927
          dp->alen = hp->alen;
384
927
          dp->var &= ~H_OPT_OWNFLAGS;
385
927
          dp->var |= (hp->var & H_OPT_OWNFLAGS);
386
927
          arena_free(hp);
387
927
          delete desc_copy;
388
927
          delete word_copy;
389
927
          return 0;
390
100k
        } else if (!dp->astr && dp->alen == 0 &&
391
92.1k
                   !hp->astr && hp->alen == 0) {
392
          // word already exists with no flags, skip duplicate
393
88.8k
          release_flags(hp->astr, hp->var & H_OPT_OWNFLAGS);
394
88.8k
          arena_free(hp);
395
88.8k
          delete desc_copy;
396
88.8k
          delete word_copy;
397
88.8k
          return 0;
398
88.8k
        } else {
399
11.3k
          dp->next_homonym = hp;
400
11.3k
        }
401
101k
      } else {
402
15.6k
        upcasehomonym = true;
403
15.6k
      }
404
116k
    }
405
14.2M
    dp = dp->next;
406
14.2M
  }
407
1.73M
  if (strcmp(hp->word, dp->word) == 0) {
408
    // remove hidden onlyupcase homonym
409
1.62M
    if (!onlyupcase) {
410
1.47M
      if ((dp->astr) && TESTAFF(dp->astr, ONLYUPCASEFLAG, dp->alen)) {
411
4.13k
        release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
412
4.13k
        dp->astr = hp->astr;
413
4.13k
        dp->alen = hp->alen;
414
4.13k
        dp->var &= ~H_OPT_OWNFLAGS;
415
4.13k
        dp->var |= (hp->var & H_OPT_OWNFLAGS);
416
4.13k
        arena_free(hp);
417
4.13k
        delete desc_copy;
418
4.13k
        delete word_copy;
419
4.13k
        return 0;
420
1.47M
      } else if (!dp->astr && dp->alen == 0 &&
421
1.24M
                 !hp->astr && hp->alen == 0) {
422
        // word already exists with no flags, skip duplicate
423
1.18M
        release_flags(hp->astr, hp->var & H_OPT_OWNFLAGS);
424
1.18M
        arena_free(hp);
425
1.18M
        delete desc_copy;
426
1.18M
        delete word_copy;
427
1.18M
        return 0;
428
1.18M
      } else {
429
290k
        dp->next_homonym = hp;
430
290k
      }
431
1.47M
    } else {
432
145k
      upcasehomonym = true;
433
145k
    }
434
1.62M
  }
435
544k
  if (!upcasehomonym) {
436
383k
    dp->next = hp;
437
383k
  } else {
438
    // remove hidden onlyupcase homonym
439
161k
    release_flags(hp->astr, hp->var & H_OPT_OWNFLAGS);
440
161k
    arena_free(hp);
441
161k
  }
442
443
544k
  delete desc_copy;
444
544k
  delete word_copy;
445
544k
  return 0;
446
1.73M
}
447
448
int HashMgr::add_hidden_capitalized_word(const std::string& word,
449
                                         int wcl,
450
                                         unsigned short* flags,
451
                                         int flagslen,
452
                                         const std::string* dp,
453
2.34M
                                         int captype) {
454
2.34M
  if (flags == nullptr)
455
2.00M
    flagslen = 0;
456
457
  // add inner capitalized forms to handle the following allcap forms:
458
  // Mixed caps: OpenOffice.org -> OPENOFFICE.ORG
459
  // Allcaps with suffixes: CIA's -> CIA'S
460
2.34M
  if (((captype == HUHCAP) || (captype == HUHINITCAP) ||
461
1.96M
       ((captype == ALLCAP) && (flagslen != 0))) &&
462
434k
      !((flagslen != 0) && TESTAFF(flags, forbiddenword, flagslen))) {
463
430k
    auto flags2 = (unsigned short*)arena_alloc((flagslen + 1) * sizeof(unsigned short),
464
430k
                                                alignof(unsigned short));
465
430k
    flags2[flagslen] = ONLYUPCASEFLAG;
466
430k
    if (flagslen) {
467
100k
      memcpy(flags2, flags, flagslen * sizeof(unsigned short));
468
100k
      std::sort(flags2, flags2 + flagslen + 1);
469
100k
    }
470
430k
    if (utf8) {
471
42.0k
      std::string st;
472
42.0k
      std::vector<w_char> w;
473
42.0k
      u8_u16(w, word);
474
42.0k
      mkallsmall_utf(w, langnum);
475
42.0k
      mkinitcap_utf(w, langnum);
476
42.0k
      u16_u8(st, w);
477
42.0k
      return add_word(st, wcl, flags2, flagslen + 1, dp, true, INITCAP, false);
478
388k
    } else {
479
388k
      std::string new_word(word);
480
388k
      mkallsmall(new_word, csconv);
481
388k
      mkinitcap(new_word, csconv);
482
388k
      int ret = add_word(new_word, wcl, flags2, flagslen + 1, dp, true, INITCAP, false);
483
388k
      return ret;
484
388k
    }
485
430k
  }
486
1.91M
  return 0;
487
2.34M
}
488
489
// detect captype and modify word length for UTF-8 encoding
490
2.35M
int HashMgr::get_clen_and_captype(const std::string& word, int* captype, std::vector<w_char> &workbuf) {
491
2.35M
  int len;
492
2.35M
  if (utf8) {
493
307k
    len = u8_u16(workbuf, word);
494
307k
    *captype = get_captype_utf8(workbuf, langnum);
495
2.04M
  } else {
496
2.04M
    len = word.size();
497
2.04M
    *captype = get_captype(word, csconv);
498
2.04M
  }
499
2.35M
  return len;
500
2.35M
}
501
502
43.1k
int HashMgr::get_clen_and_captype(const std::string& word, int* captype) {
503
43.1k
  std::vector<w_char> workbuf;
504
43.1k
  return get_clen_and_captype(word, captype, workbuf);
505
43.1k
}
506
507
// remove word (personal dictionary function for standalone applications)
508
31.2k
int HashMgr::remove(const std::string& word) {
509
31.2k
  struct hentry* dp = lookup(word.c_str(), word.size());
510
51.7k
  while (dp) {
511
20.5k
    if (dp->alen == 0 || !TESTAFF(dp->astr, forbiddenword, dp->alen)) {
512
13.1k
      auto flags = new unsigned short[dp->alen + 1];
513
319k
      for (int i = 0; i < dp->alen; i++)
514
306k
        flags[i] = dp->astr[i];
515
13.1k
      flags[dp->alen] = forbiddenword;
516
13.1k
      release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
517
13.1k
      dp->astr = flags;
518
13.1k
      dp->alen++;
519
13.1k
      dp->var |= H_OPT_OWNFLAGS;
520
13.1k
      std::sort(flags, flags + dp->alen);
521
13.1k
    }
522
20.5k
    dp = dp->next_homonym;
523
20.5k
  }
524
31.2k
  return 0;
525
31.2k
}
526
527
/* remove forbidden flag to add a personal word to the hash */
528
75.4k
void HashMgr::remove_forbidden_flag(const std::string& word) {
529
75.4k
  struct hentry* dp = lookup(word.c_str(), word.size());
530
75.4k
  if (!dp)
531
44.3k
    return;
532
92.3k
  while (dp) {
533
61.2k
    if (dp->astr && TESTAFF(dp->astr, forbiddenword, dp->alen)) {
534
8.66k
      if (dp->alen == 1) {
535
5.84k
        release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
536
5.84k
        dp->astr = nullptr;
537
5.84k
        dp->alen = 0;
538
5.84k
        dp->var &= ~H_OPT_OWNFLAGS;
539
5.84k
      } else {
540
2.82k
        auto newflags = new unsigned short[dp->alen - 1];
541
2.82k
        int j = 0;
542
204k
        for (int i = 0; i < dp->alen; i++) {
543
202k
          if (dp->astr[i] != forbiddenword)
544
198k
            newflags[j++] = dp->astr[i];
545
202k
        }
546
2.82k
        release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
547
2.82k
        dp->astr = newflags;
548
2.82k
        dp->alen = (short)j;
549
2.82k
        dp->var |= H_OPT_OWNFLAGS;
550
2.82k
      }
551
8.66k
    }
552
61.2k
    dp = dp->next_homonym;
553
61.2k
  }
554
31.1k
}
555
556
// add a custom dic. word to the hash table (public)
557
38.4k
int HashMgr::add(const std::string& word) {
558
38.4k
  remove_forbidden_flag(word);
559
38.4k
  int captype, al = 0;
560
38.4k
  unsigned short* flags = nullptr;
561
38.4k
  int wcl = get_clen_and_captype(word, &captype);
562
38.4k
  if (add_word(word, wcl, flags, al, nullptr, false, captype, true))
563
202
    return 1;
564
38.2k
  return add_hidden_capitalized_word(word, wcl, flags, al, nullptr, captype);
565
38.4k
}
566
567
0
int HashMgr::add_with_flags(const std::string& word, const std::string& flags, const std::string& desc) {
568
0
  remove_forbidden_flag(word);
569
0
  int captype;
570
0
  unsigned short *df;
571
0
  int al = decode_flags(&df, flags, nullptr);
572
0
  int wcl = get_clen_and_captype(word, &captype);
573
0
  if (add_word(word, wcl, df, al, &desc, false, captype, true))
574
0
    return 1;
575
0
  return add_hidden_capitalized_word(word, wcl, df, al, &desc, captype);
576
0
}
577
578
36.9k
int HashMgr::add_with_affix(const std::string& word, const std::string& example) {
579
  // detect captype and modify word length for UTF-8 encoding
580
36.9k
  struct hentry* dp = lookup(example.c_str(), example.size());
581
36.9k
  remove_forbidden_flag(word);
582
36.9k
  if (dp && dp->astr) {
583
4.60k
    int captype;
584
4.60k
    int wcl = get_clen_and_captype(word, &captype);
585
4.60k
    auto flags = new unsigned short[dp->alen];
586
4.60k
    memcpy(flags, dp->astr, dp->alen * sizeof(unsigned short));
587
4.60k
    if (add_word(word, wcl, flags, dp->alen, nullptr, false, captype, true))
588
82
      return 1;
589
4.52k
    return add_hidden_capitalized_word(word, wcl, flags, dp->alen, nullptr, captype);
590
4.60k
  }
591
32.3k
  return 1;
592
36.9k
}
593
594
// walk the hash table entry by entry - null at end
595
// initialize: col=-1; hp = NULL; hp = walk_hashtable(&col, hp);
596
3.27M
struct hentry* HashMgr::walk_hashtable(int& col, struct hentry* hp) const {
597
3.27M
  if (hp && hp->next != nullptr)
598
380k
    return hp->next;
599
115M
  for (col++; col < (int)tableptr.size(); ++col) {
600
115M
    if (tableptr[col])
601
2.76M
      return tableptr[col];
602
115M
  }
603
  // null at end and reset to start
604
132k
  col = -1;
605
132k
  return nullptr;
606
2.89M
}
607
608
// load a munched word list and build a hash table on the fly
609
55.9k
int HashMgr::load_tables(const char* tpath, const char* key) {
610
  // open dictionary file
611
55.9k
  FileMgr* dict = new FileMgr(tpath, key);
612
613
  // first read the first line of file to get hash table size
614
55.9k
  std::string ts;
615
55.9k
  if (!dict->getline(ts)) {
616
533
    fprintf(stderr, "error: empty dic file %s\n", tpath);
617
533
    delete dict;
618
533
    return 2;
619
533
  }
620
55.4k
  mychomp(ts);
621
622
  /* remove byte order mark */
623
55.4k
  if (ts.compare(0, 3, "\xEF\xBB\xBF", 3) == 0) {
624
4
    ts.erase(0, 3);
625
4
  }
626
627
55.4k
  int tablesize = atoi(ts.c_str());
628
629
55.4k
  const int nExtra = 5 + USERWORD;
630
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
631
  // covers full-form uncompressed dictionaries (~7M for uk_UA expanded)
632
  const int max_allowed = 10000000;
633
#else
634
55.4k
  const int max_allowed = (10000 - 1 - nExtra) / int(sizeof(struct hentry*));
635
55.4k
#endif
636
637
55.4k
  if (tablesize <= 0 || tablesize >= max_allowed) {
638
30.9k
    fprintf(stderr,
639
30.9k
            "error: %s: line 1: missing or bad word count in the dic file\n",
640
30.9k
            tpath);
641
30.9k
    delete dict;
642
30.9k
    return 4;
643
30.9k
  }
644
24.4k
  tablesize += nExtra;
645
24.4k
  if ((tablesize & 1) == 0)
646
14.2k
    tablesize++;
647
648
  // allocate the hash table
649
24.4k
  tableptr.resize(tablesize, nullptr);
650
651
  // loop through all words on much list and add to hash
652
  // table and create word and affix strings
653
654
24.4k
  std::vector<w_char> workbuf;
655
656
24.4k
  int nLineCount(0);
657
2.33M
  while (dict->getline(ts)) {
658
2.30M
    ++nLineCount;
659
2.30M
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
660
    // limit words loaded to avoid O(n^2) hash chain walk timeout
661
2.30M
    if (nLineCount >= tablesize)
662
123
      break;
663
2.30M
#endif
664
2.30M
    mychomp(ts);
665
    // split each line into word and morphological description
666
2.30M
    size_t dp_pos = 0;
667
2.75M
    while ((dp_pos = ts.find(':', dp_pos)) != std::string::npos) {
668
507k
      if ((dp_pos > 3) && (ts[dp_pos - 3] == ' ' || ts[dp_pos - 3] == '\t')) {
669
91.6k
        for (dp_pos -= 3; dp_pos > 0 && (ts[dp_pos-1] == ' ' || ts[dp_pos-1] == '\t'); --dp_pos)
670
27.6k
          ;
671
63.9k
        if (dp_pos == 0) {  // missing word
672
949
          dp_pos = std::string::npos;
673
62.9k
        } else {
674
62.9k
          ++dp_pos;
675
62.9k
        }
676
63.9k
        break;
677
63.9k
      }
678
443k
      ++dp_pos;
679
443k
    }
680
681
    // tabulator is the old morphological field separator
682
2.30M
    size_t dp2_pos = ts.find('\t');
683
2.30M
    if (dp2_pos != std::string::npos && (dp_pos == std::string::npos || dp2_pos < dp_pos)) {
684
614k
      dp_pos = dp2_pos + 1;
685
614k
    }
686
687
2.30M
    std::string dp;
688
2.30M
    if (dp_pos != std::string::npos) {
689
632k
      dp.assign(ts.substr(dp_pos));
690
632k
      ts.resize(dp_pos - 1);
691
632k
    }
692
693
    // split each line into word and affix char strings
694
    // "\/" signs slash in words (not affix separator)
695
    // "/" at beginning of the line is word character (not affix separator)
696
2.30M
    size_t ap_pos = ts.find('/');
697
2.34M
    while (ap_pos != std::string::npos) {
698
400k
      if (ap_pos == 0) {
699
33.8k
        ++ap_pos;
700
33.8k
        continue;
701
366k
      } else if (ts[ap_pos - 1] != '\\')
702
363k
        break;
703
      // replace "\/" with "/"
704
3.09k
      ts.erase(ap_pos - 1, 1);
705
3.09k
      ap_pos = ts.find('/', ap_pos);
706
3.09k
    }
707
708
2.30M
    unsigned short* flags;
709
2.30M
    int al;
710
2.30M
    if (ap_pos != std::string::npos && ap_pos != ts.size()) {
711
359k
      std::string ap(ts.substr(ap_pos + 1));
712
359k
      ts.resize(ap_pos);
713
359k
      if (!aliasf.empty()) {
714
10.6k
        int index = atoi(ap.c_str());
715
10.6k
        al = get_aliasf(index, &flags, dict);
716
10.6k
        if (!al) {
717
6.51k
          HUNSPELL_WARNING(stderr, "error: line %d: bad flag vector alias\n",
718
6.51k
                           dict->getlinenum());
719
6.51k
        }
720
348k
      } else {
721
348k
        al = decode_flags(&flags, ap, dict, /* arena = */ true);
722
348k
        if (al == -1) {
723
0
          HUNSPELL_WARNING(stderr, "Can't allocate memory.\n");
724
0
          delete dict;
725
0
          return 6;
726
0
        }
727
348k
        std::sort(flags, flags + al);
728
348k
      }
729
1.94M
    } else {
730
1.94M
      al = 0;
731
1.94M
      flags = nullptr;
732
1.94M
    }
733
734
2.30M
    int captype;
735
2.30M
    int wcl = get_clen_and_captype(ts, &captype, workbuf);
736
2.30M
    const std::string* dp_str = dp.empty() ? nullptr : &dp;
737
    // add the word and its index plus its capitalized form optionally
738
    // flags are arena-allocated, so own_aff must be false
739
2.30M
    bool own = false;
740
2.30M
    if (add_word(ts, wcl, flags, al, dp_str, false, captype, own) ||
741
2.30M
        add_hidden_capitalized_word(ts, wcl, flags, al, dp_str, captype)) {
742
35
      delete dict;
743
35
      return 5;
744
35
    }
745
2.30M
  }
746
747
24.3k
  int ret(0);
748
749
  // reject ludicrous tablesizes
750
24.3k
  if (tablesize > 8192 + nExtra && tablesize > nLineCount * 10 + nExtra) {
751
0
    HUNSPELL_WARNING(stderr, ".dic initial approximate word count line value of %d is too large for %d lines\n", tablesize, nLineCount);
752
0
    ret = 3;
753
0
  }
754
755
24.3k
  delete dict;
756
24.3k
  return ret;
757
24.4k
}
758
759
// the hash function is a simple load and rotate
760
// algorithm borrowed
761
5.25G
int HashMgr::hash(const char* word, size_t len) const {
762
5.25G
  unsigned long hv = 0;
763
5.25G
  size_t i = 0;
764
25.7G
  while (i < 4 && i < len)
765
20.4G
    hv = (hv << 8) | word[i++];
766
301G
  while (i < len) {
767
295G
    ROTATE(hv, ROTATE_LEN);
768
295G
    hv ^= word[i++];
769
295G
  }
770
5.25G
  return (unsigned long)hv % tableptr.size();
771
5.25G
}
772
773
54.8k
int HashMgr::decode_flags(unsigned short** result, const std::string& flags, FileMgr* af) const {
774
54.8k
  return decode_flags(result, flags, af, /* arena = */ false);
775
54.8k
}
776
777
415k
int HashMgr::decode_flags(unsigned short** result, const std::string& flags, FileMgr* af, bool use_arena) const {
778
415k
  auto alloc = [&](int n) -> unsigned short* {
779
393k
    return use_arena ? (unsigned short*)this->arena_alloc(n * sizeof(unsigned short),
780
344k
                                                          alignof(unsigned short))
781
393k
                     : new unsigned short[n];
782
393k
  };
783
415k
  int len;
784
415k
  if (flags.empty()) {
785
22.0k
    *result = nullptr;
786
22.0k
    return 0;
787
22.0k
  }
788
393k
  switch (flag_mode) {
789
35.3k
    case FLAG_LONG: {  // two-character flags (1x2yZz -> 1x 2y Zz)
790
35.3k
      len = flags.size();
791
35.3k
      if ((len & 1) == 1 && af != nullptr)
792
21.2k
        HUNSPELL_WARNING(stderr, "error: line %d: bad flagvector\n",
793
21.2k
                         af->getlinenum());
794
35.3k
      len >>= 1;
795
35.3k
      *result = alloc(len);
796
590k
      for (int i = 0; i < len; i++) {
797
555k
        unsigned short flag = ((unsigned short)((unsigned char)flags[i << 1]) << 8) |
798
555k
                              ((unsigned short)((unsigned char)flags[(i << 1) | 1]));
799
800
555k
        (*result)[i] = flag;
801
555k
      }
802
35.3k
      break;
803
0
    }
804
10.3k
    case FLAG_NUM: {  // decimal numbers separated by comma (4521,23,233 -> 4521
805
                      // 23 233)
806
1.83M
      len = int(1 + std::count_if(flags.begin(), flags.end(), [](char c) { return c == ','; }));
807
10.3k
      *result = alloc(len);
808
10.3k
      unsigned short* dest = *result;
809
10.3k
      const char* src = flags.c_str();
810
1.84M
      for (size_t p = 0; p < flags.size(); ++p) {
811
1.83M
        if (flags[p] == ',') {
812
16.9k
          int i = atoi(src);
813
16.9k
          if ((i > std::numeric_limits<unsigned short>::max() || i < 0) && af != nullptr) {
814
4.20k
            HUNSPELL_WARNING(
815
4.20k
                stderr, "error: line %d: flag id %d is out of range\n",
816
4.20k
                af->getlinenum(), i);
817
4.20k
             i = 0;
818
4.20k
          }
819
16.9k
          *dest = (unsigned short)i;
820
16.9k
          if (*dest == 0 && af != nullptr)
821
10.7k
            HUNSPELL_WARNING(stderr, "error: line %d: 0 is wrong flag id\n",
822
10.7k
                             af->getlinenum());
823
16.9k
          src = flags.c_str() + p + 1;
824
16.9k
          dest++;
825
16.9k
        }
826
1.83M
      }
827
10.3k
      int i = atoi(src);
828
10.3k
      if ((i > std::numeric_limits<unsigned short>::max() || i < 0) && af) {
829
1.75k
        HUNSPELL_WARNING(stderr,
830
1.75k
                         "error: line %d: flag id %d is out of range\n",
831
1.75k
                         af->getlinenum(), i);
832
1.75k
        i = 0;
833
1.75k
      }
834
10.3k
      *dest = (unsigned short)i;
835
10.3k
      if (*dest == 0 && af)
836
6.36k
        HUNSPELL_WARNING(stderr, "error: line %d: 0 is wrong flag id\n",
837
6.36k
                         af->getlinenum());
838
10.3k
      break;
839
0
    }
840
37.0k
    case FLAG_UNI: {  // UTF-8 characters
841
37.0k
      std::vector<w_char> w;
842
37.0k
      u8_u16(w, flags);
843
37.0k
      len = w.size();
844
37.0k
      *result = alloc(len);
845
37.0k
#if defined(_WIN32) || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__))  || defined(__LITTLE_ENDIAN__)
846
37.0k
      memcpy(*result, w.data(), len * sizeof(unsigned short));
847
#else
848
      unsigned short* dest = *result;
849
      for (const w_char wc : w) {
850
        *dest = (unsigned short)wc;
851
        dest++;
852
      }
853
#endif
854
37.0k
      break;
855
0
    }
856
311k
    default: {  // Ispell's one-character flags (erfg -> e r f g)
857
311k
      len = flags.size();
858
311k
      *result = alloc(len);
859
311k
      unsigned short* dest = *result;
860
12.3M
      for (const char flag : flags) {
861
12.3M
        *dest = (unsigned char)flag;
862
12.3M
        dest++;
863
12.3M
      }
864
311k
    }
865
393k
  }
866
393k
  return len;
867
393k
}
868
869
1.37M
bool HashMgr::decode_flags(std::vector<unsigned short>& result, const std::string& flags, FileMgr* af) const {
870
1.37M
  if (flags.empty()) {
871
1.01k
    return false;
872
1.01k
  }
873
1.37M
  switch (flag_mode) {
874
75.0k
    case FLAG_LONG: {  // two-character flags (1x2yZz -> 1x 2y Zz)
875
75.0k
      size_t len = flags.size();
876
75.0k
      if ((len & 1) == 1)
877
73.4k
        HUNSPELL_WARNING(stderr, "error: line %d: bad flagvector\n",
878
73.4k
                         af->getlinenum());
879
75.0k
      len >>= 1;
880
75.0k
      size_t origsize = result.size();
881
75.0k
      result.resize(origsize + len);
882
246k
      for (size_t i = 0; i < len; ++i) {
883
171k
        result[origsize + i] = ((unsigned short)((unsigned char)flags[i << 1]) << 8) |
884
171k
                               ((unsigned short)((unsigned char)flags[(i << 1) | 1]));
885
171k
      }
886
75.0k
      break;
887
0
    }
888
64.4k
    case FLAG_NUM: {  // decimal numbers separated by comma (4521,23,233 -> 4521
889
                      // 23 233)
890
64.4k
      const char* src = flags.c_str();
891
260k
      for (const char* p = src; *p; p++) {
892
195k
        if (*p == ',') {
893
11.0k
          int i = atoi(src);
894
11.0k
          if (i > std::numeric_limits<unsigned short>::max() || i < 0) {
895
2.54k
            HUNSPELL_WARNING(
896
2.54k
                stderr, "error: line %d: flag id %d is out of range\n",
897
2.54k
                af->getlinenum(), i);
898
2.54k
            i = 0;
899
2.54k
          }
900
11.0k
          result.push_back((unsigned short)i);
901
11.0k
          if (result.back() == 0)
902
8.17k
            HUNSPELL_WARNING(stderr, "error: line %d: 0 is wrong flag id\n",
903
8.17k
                             af->getlinenum());
904
11.0k
          src = p + 1;
905
11.0k
        }
906
195k
      }
907
64.4k
      int i = atoi(src);
908
64.4k
      if (i > std::numeric_limits<unsigned short>::max() || i < 0) {
909
1.80k
        HUNSPELL_WARNING(stderr,
910
1.80k
                         "error: line %d: flag id %d is out of range\n",
911
1.80k
                         af->getlinenum(), i);
912
1.80k
        i = 0;
913
1.80k
      }
914
64.4k
      result.push_back((unsigned short)i);
915
64.4k
      if (result.back() == 0)
916
52.3k
        HUNSPELL_WARNING(stderr, "error: line %d: 0 is wrong flag id\n",
917
52.3k
                         af->getlinenum());
918
64.4k
      break;
919
0
    }
920
714k
    case FLAG_UNI: {  // UTF-8 characters
921
714k
      std::vector<w_char> w;
922
714k
      u8_u16(w, flags);
923
714k
      size_t len = w.size(), origsize = result.size();
924
714k
      result.resize(origsize + len);
925
714k
#if defined(_WIN32) || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__))  || defined(__LITTLE_ENDIAN__)
926
714k
      memcpy(result.data() + origsize, w.data(), len * sizeof(short));
927
#else
928
      for (size_t i = 0; i < len; ++i)
929
        result[origsize + i] = (unsigned short)w[i];
930
#endif
931
714k
      break;
932
0
    }
933
518k
    default: {  // Ispell's one-character flags (erfg -> e r f g)
934
518k
      result.reserve(flags.size());
935
3.98M
      for (const char flag : flags) {
936
3.98M
        result.push_back((unsigned char)flag);
937
3.98M
      }
938
518k
    }
939
1.37M
  }
940
1.37M
  return true;
941
1.37M
}
942
943
404k
unsigned short HashMgr::decode_flag(const std::string& f) const {
944
404k
  unsigned short s = 0;
945
404k
  int i;
946
404k
  switch (flag_mode) {
947
25.3k
    case FLAG_LONG:
948
25.3k
      if (f.size() >= 2)
949
12.4k
        s = ((unsigned short)((unsigned char)f[0]) << 8) | ((unsigned short)((unsigned char)f[1]));
950
25.3k
      break;
951
14.4k
    case FLAG_NUM:
952
14.4k
      i = atoi(f.c_str());
953
14.4k
      if (i > std::numeric_limits<unsigned short>::max() || i < 0) {
954
1.43k
        HUNSPELL_WARNING(stderr, "error: flag id %d is out of range\n", i);
955
1.43k
        i = 0;
956
1.43k
      }
957
14.4k
      s = (unsigned short)i;
958
14.4k
      break;
959
25.7k
    case FLAG_UNI: {
960
25.7k
      std::vector<w_char> w;
961
25.7k
      u8_u16(w, f);
962
25.7k
      if (!w.empty())
963
23.0k
        s = (unsigned short)w[0];
964
25.7k
      break;
965
0
    }
966
338k
    default:
967
338k
      if (!f.empty())
968
333k
        s = (unsigned char)f[0];
969
404k
  }
970
404k
  if (s == 0)
971
148k
    HUNSPELL_WARNING(stderr, "error: 0 is wrong flag id\n");
972
404k
  return s;
973
404k
}
974
975
55.4M
std::string HashMgr::encode_flag(unsigned short f) const {
976
55.4M
  if (f == 0)
977
51.7M
    return "(NULL)";
978
3.74M
  std::string ch;
979
3.74M
  if (flag_mode == FLAG_LONG) {
980
239
    ch.push_back((unsigned char)(f >> 8));
981
239
    ch.push_back((unsigned char)(f - ((f >> 8) << 8)));
982
3.74M
  } else if (flag_mode == FLAG_NUM) {
983
124
    ch = std::to_string(f);
984
3.74M
  } else if (flag_mode == FLAG_UNI) {
985
986
537k
#if defined(_WIN32) || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__))  || defined(__LITTLE_ENDIAN__)
987
988
#if (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) && defined __cpp_lib_bit_cast && __cpp_lib_bit_cast >= 201806L
989
    auto wc = std::bit_cast<w_char>(f);
990
#else
991
537k
    w_char wc;
992
537k
    memcpy(&wc, &f, sizeof(unsigned short));
993
537k
#endif
994
995
#else
996
    w_char wc;
997
    wc.h = (unsigned char)(f >> 8);
998
    wc.l = (unsigned char)(f & 0xff);
999
#endif
1000
537k
    const std::vector<w_char> w = { wc };
1001
537k
    u16_u8(ch, w);
1002
3.20M
  } else {
1003
3.20M
    ch.push_back((unsigned char)(f));
1004
3.20M
  }
1005
3.74M
  return ch;
1006
55.4M
}
1007
1008
// read in aff file and set flag mode
1009
55.9k
int HashMgr::load_config(const char* affpath, const char* key) {
1010
55.9k
  int firstline = 1;
1011
1012
  // open the affix file
1013
55.9k
  FileMgr* afflst = new FileMgr(affpath, key);
1014
1015
  // read in each line ignoring any that do not
1016
  // start with a known line type indicator
1017
1018
55.9k
  std::string line;
1019
1.80M
  while (afflst->getline(line)) {
1020
1.76M
    mychomp(line);
1021
1022
    /* remove byte order mark */
1023
1.76M
    if (firstline) {
1024
55.2k
      firstline = 0;
1025
55.2k
      if (line.compare(0, 3, "\xEF\xBB\xBF", 3) == 0) {
1026
99
        line.erase(0, 3);
1027
99
      }
1028
55.2k
    }
1029
1030
    /* parse in the try string */
1031
1.76M
    if ((line.compare(0, 4, "FLAG", 4) == 0) && line.size() > 4 && isspace(line[4])) {
1032
14.5k
      if (flag_mode != FLAG_CHAR) {
1033
4.99k
        HUNSPELL_WARNING(stderr,
1034
4.99k
                         "error: line %d: multiple definitions of the FLAG "
1035
4.99k
                         "affix file parameter\n",
1036
4.99k
                         afflst->getlinenum());
1037
4.99k
      }
1038
14.5k
      if (line.find("long") != std::string::npos)
1039
3.24k
        flag_mode = FLAG_LONG;
1040
14.5k
      if (line.find("num") != std::string::npos)
1041
2.65k
        flag_mode = FLAG_NUM;
1042
14.5k
      if (line.find("UTF-8") != std::string::npos)
1043
1.96k
        flag_mode = FLAG_UNI;
1044
14.5k
      if (flag_mode == FLAG_CHAR) {
1045
4.87k
        HUNSPELL_WARNING(
1046
4.87k
            stderr,
1047
4.87k
            "error: line %d: FLAG needs `num', `long' or `UTF-8' parameter\n",
1048
4.87k
            afflst->getlinenum());
1049
4.87k
      }
1050
14.5k
    }
1051
1052
1.76M
    if (line.compare(0, 13, "FORBIDDENWORD", 13) == 0) {
1053
6.10k
      std::string st;
1054
6.10k
      if (!parse_string(line, st, afflst->getlinenum())) {
1055
38
        delete afflst;
1056
38
        return 1;
1057
38
      }
1058
6.06k
      forbiddenword = decode_flag(st);
1059
6.06k
    }
1060
1061
1.76M
    if (line.compare(0, 3, "SET", 3) == 0) {
1062
12.8k
      if (!parse_string(line, enc, afflst->getlinenum())) {
1063
279
        delete afflst;
1064
279
        return 1;
1065
279
      }
1066
12.6k
      if (enc == "UTF-8") {
1067
11.1k
        utf8 = 1;
1068
11.1k
      } else
1069
1.48k
        csconv = get_current_cs(enc);
1070
12.6k
    }
1071
1072
1.76M
    if (line.compare(0, 4, "LANG", 4) == 0) {
1073
6.72k
      if (!parse_string(line, lang, afflst->getlinenum())) {
1074
213
        delete afflst;
1075
213
        return 1;
1076
213
      }
1077
6.51k
      langnum = get_lang_num(lang);
1078
6.51k
    }
1079
1080
    /* parse in the ignored characters (for example, Arabic optional diacritics
1081
     * characters */
1082
1.75M
    if (line.compare(0, 6, "IGNORE", 6) == 0) {
1083
6.53k
      if (!parse_array(line, ignorechars, ignorechars_utf16,
1084
6.53k
                       utf8, afflst->getlinenum())) {
1085
261
        delete afflst;
1086
261
        return 1;
1087
261
      }
1088
6.53k
    }
1089
1090
1.75M
    if ((line.compare(0, 2, "AF", 2) == 0) && line.size() > 2 && isspace(line[2])) {
1091
7.30k
      if (!parse_aliasf(line, afflst)) {
1092
6.84k
        delete afflst;
1093
6.84k
        return 1;
1094
6.84k
      }
1095
7.30k
    }
1096
1097
1.75M
    if ((line.compare(0, 2, "AM", 2) == 0) && line.size() > 2 && isspace(line[2])) {
1098
5.58k
      if (!parse_aliasm(line, afflst)) {
1099
4.13k
        delete afflst;
1100
4.13k
        return 1;
1101
4.13k
      }
1102
5.58k
    }
1103
1104
1.74M
    if (line.compare(0, 15, "COMPLEXPREFIXES", 15) == 0)
1105
8.48k
      complexprefixes = 1;
1106
1107
    /* parse in the typical fault correcting table */
1108
1.74M
    if (line.compare(0, 3, "REP", 3) == 0) {
1109
2.39k
      if (!parse_reptable(line, afflst)) {
1110
2.31k
        delete afflst;
1111
2.31k
        return 1;
1112
2.31k
      }
1113
2.39k
    }
1114
1115
    // don't check the full affix file, yet
1116
1.74M
    if (((line.compare(0, 3, "SFX", 3) == 0) ||
1117
1.69M
         (line.compare(0, 3, "PFX", 3) == 0)) &&
1118
98.6k
            line.size() > 3 && isspace(line[3]) &&
1119
46.6k
            !reptable.empty()) // (REP table is in the end of Afrikaans aff file)
1120
6
      break;
1121
1.74M
  }
1122
1123
41.8k
  delete afflst;
1124
41.8k
  return 0;
1125
55.9k
}
1126
1127
/* parse in the ALIAS table */
1128
7.30k
bool HashMgr::parse_aliasf(const std::string& line, FileMgr* af) {
1129
7.30k
  if (!aliasf.empty()) {
1130
187
    HUNSPELL_WARNING(stderr, "error: line %d: multiple table definitions\n",
1131
187
                     af->getlinenum());
1132
187
    return false;
1133
187
  }
1134
7.11k
  int i = 0, np = 0, numaliasf = 0;
1135
7.11k
  auto iter = line.begin(), start_piece = mystrsep(line, iter);
1136
21.1k
  while (start_piece != line.end()) {
1137
15.9k
    switch (i) {
1138
7.11k
      case 0: {
1139
7.11k
        np++;
1140
7.11k
        break;
1141
0
      }
1142
5.31k
      case 1: {
1143
5.31k
        numaliasf = atoi(std::string(start_piece, iter).c_str());
1144
5.31k
        if (numaliasf < 1) {
1145
1.92k
          aliasf.clear();
1146
1.92k
          aliasflen.clear();
1147
1.92k
          HUNSPELL_WARNING(stderr, "error: line %d: bad entry number\n",
1148
1.92k
                           af->getlinenum());
1149
1.92k
          return false;
1150
1.92k
        }
1151
3.39k
        aliasf.reserve(std::min(numaliasf, 16384));
1152
3.39k
        aliasflen.reserve(std::min(numaliasf, 16384));
1153
3.39k
        np++;
1154
3.39k
        break;
1155
5.31k
      }
1156
3.52k
      default:
1157
3.52k
        break;
1158
15.9k
    }
1159
14.0k
    ++i;
1160
14.0k
    start_piece = mystrsep(line, iter);
1161
14.0k
  }
1162
5.19k
  if (np != 2) {
1163
1.79k
    aliasf.clear();
1164
1.79k
    aliasflen.clear();
1165
1.79k
    HUNSPELL_WARNING(stderr, "error: line %d: missing data\n",
1166
1.79k
                     af->getlinenum());
1167
1.79k
    return false;
1168
1.79k
  }
1169
1170
  /* now parse the numaliasf lines to read in the remainder of the table */
1171
15.8k
  for (int j = 0; j < numaliasf; ++j) {
1172
15.3k
    std::string nl;
1173
15.3k
    unsigned short* alias = nullptr;
1174
15.3k
    unsigned aliaslen = 0;
1175
15.3k
    i = 0;
1176
15.3k
    if (af->getline(nl)) {
1177
14.6k
      mychomp(nl);
1178
14.6k
      iter = nl.begin();
1179
14.6k
      start_piece = mystrsep(nl, iter);
1180
14.6k
      bool errored = false;
1181
58.8k
      while (!errored && start_piece != nl.end()) {
1182
44.1k
        switch (i) {
1183
14.5k
          case 0: {
1184
14.5k
            if (nl.compare(start_piece - nl.begin(), 2, "AF", 2) != 0) {
1185
2.00k
              errored = true;
1186
2.00k
              break;
1187
2.00k
            }
1188
12.5k
            break;
1189
14.5k
          }
1190
12.5k
          case 1: {
1191
12.4k
            std::string piece(start_piece, iter);
1192
12.4k
            aliaslen =
1193
12.4k
                (unsigned short)decode_flags(&alias, piece, af, /* arena = */ true);
1194
12.4k
            std::sort(alias, alias + aliaslen);
1195
12.4k
            break;
1196
14.5k
          }
1197
17.1k
          default:
1198
17.1k
            break;
1199
44.1k
        }
1200
44.1k
        ++i;
1201
44.1k
        start_piece = mystrsep(nl, iter);
1202
44.1k
      }
1203
14.6k
    }
1204
15.3k
    if (!alias) {
1205
11.1k
      for (int k = 0; k < j; ++k) {
1206
8.21k
        arena_free(aliasf[k]);
1207
8.21k
      }
1208
2.93k
      aliasf.clear();
1209
2.93k
      aliasflen.clear();
1210
2.93k
      HUNSPELL_WARNING(stderr, "error: line %d: table is corrupt\n",
1211
2.93k
                       af->getlinenum());
1212
2.93k
      return false;
1213
2.93k
    }
1214
1215
12.4k
    aliasf.push_back(alias);
1216
12.4k
    aliasflen.push_back(aliaslen);
1217
12.4k
  }
1218
454
  return true;
1219
3.39k
}
1220
1221
176k
int HashMgr::is_aliasf() const {
1222
176k
  return !aliasf.empty();
1223
176k
}
1224
1225
15.5k
int HashMgr::get_aliasf(int index, unsigned short** fvec, FileMgr* af) const {
1226
15.5k
  if (index > 0 && static_cast<size_t>(index) <= aliasflen.size()) {
1227
4.76k
    *fvec = aliasf[index - 1];
1228
4.76k
    return aliasflen[index - 1];
1229
4.76k
  }
1230
10.8k
  HUNSPELL_WARNING(stderr, "error: line %d: bad flag alias index: %d\n",
1231
10.8k
                   af->getlinenum(), index);
1232
10.8k
  *fvec = nullptr;
1233
10.8k
  return 0;
1234
15.5k
}
1235
1236
/* parse morph alias definitions */
1237
5.58k
bool HashMgr::parse_aliasm(const std::string& line, FileMgr* af) {
1238
5.58k
  if (!aliasm.empty()) {
1239
614
    HUNSPELL_WARNING(stderr, "error: line %d: multiple table definitions\n",
1240
614
                     af->getlinenum());
1241
614
    return false;
1242
614
  }
1243
4.96k
  int i = 0, np = 0, numaliasm = 0;
1244
4.96k
  auto iter = line.begin(), start_piece = mystrsep(line, iter);
1245
25.7k
  while (start_piece != line.end()) {
1246
21.8k
    switch (i) {
1247
4.96k
      case 0: {
1248
4.96k
        np++;
1249
4.96k
        break;
1250
0
      }
1251
4.93k
      case 1: {
1252
4.93k
        numaliasm = atoi(std::string(start_piece, iter).c_str());
1253
4.93k
        if (numaliasm < 1) {
1254
1.11k
          HUNSPELL_WARNING(stderr, "error: line %d: bad entry number\n",
1255
1.11k
                           af->getlinenum());
1256
1.11k
          return false;
1257
1.11k
        }
1258
3.82k
        aliasm.reserve(std::min(numaliasm, 16384));
1259
3.82k
        np++;
1260
3.82k
        break;
1261
4.93k
      }
1262
11.9k
      default:
1263
11.9k
        break;
1264
21.8k
    }
1265
20.7k
    ++i;
1266
20.7k
    start_piece = mystrsep(line, iter);
1267
20.7k
  }
1268
3.85k
  if (np != 2) {
1269
34
    aliasm.clear();
1270
34
    HUNSPELL_WARNING(stderr, "error: line %d: missing data\n",
1271
34
                     af->getlinenum());
1272
34
    return false;
1273
34
  }
1274
1275
  /* now parse the numaliasm lines to read in the remainder of the table */
1276
12.1k
  for (int j = 0; j < numaliasm; ++j) {
1277
10.6k
    std::string nl;
1278
10.6k
    char* alias = nullptr;
1279
10.6k
    if (af->getline(nl)) {
1280
10.0k
      mychomp(nl);
1281
10.0k
      iter = nl.begin();
1282
10.0k
      i = 0;
1283
10.0k
      start_piece = mystrsep(nl, iter);
1284
10.0k
      bool errored = false;
1285
44.6k
      while (!errored && start_piece != nl.end()) {
1286
34.5k
        switch (i) {
1287
9.67k
          case 0: {
1288
9.67k
            if (nl.compare(start_piece - nl.begin(), 2, "AM", 2) != 0) {
1289
1.33k
              errored = true;
1290
1.33k
              break;
1291
1.33k
            }
1292
8.34k
            break;
1293
9.67k
          }
1294
8.34k
          case 1: {
1295
            // add the remaining of the line
1296
8.28k
            std::string::const_iterator end = nl.end();
1297
8.28k
            std::string chunk(start_piece, end);
1298
8.28k
            if (complexprefixes) {
1299
2.13k
              if (utf8)
1300
887
                reverseword_utf(chunk);
1301
1.25k
              else
1302
1.25k
                reverseword(chunk);
1303
2.13k
            }
1304
8.28k
            size_t sl = chunk.size() + 1;
1305
8.28k
            alias = (char*)arena_alloc(sl, alignof(char));
1306
8.28k
            if (alias) {
1307
8.28k
              memcpy(alias, chunk.c_str(), sl);
1308
8.28k
            }
1309
8.28k
            break;
1310
9.67k
          }
1311
16.5k
          default:
1312
16.5k
            break;
1313
34.5k
        }
1314
34.5k
        ++i;
1315
34.5k
        start_piece = mystrsep(nl, iter);
1316
34.5k
      }
1317
10.0k
    }
1318
10.6k
    if (!alias) {
1319
6.78k
      for (int k = 0; k < j; ++k) {
1320
4.40k
        arena_free(aliasm[k]);
1321
4.40k
      }
1322
2.38k
      aliasm.clear();
1323
2.38k
      HUNSPELL_WARNING(stderr, "error: line %d: table is corrupt\n",
1324
2.38k
                       af->getlinenum());
1325
2.38k
      return false;
1326
2.38k
    }
1327
8.28k
    aliasm.push_back(alias);
1328
8.28k
  }
1329
1.44k
  return true;
1330
3.82k
}
1331
1332
199k
int HashMgr::is_aliasm() const {
1333
199k
  return !aliasm.empty();
1334
199k
}
1335
1336
69.3k
char* HashMgr::get_aliasm(int index) const {
1337
69.3k
  if (index > 0 && static_cast<size_t>(index) <= aliasm.size())
1338
15.2k
    return aliasm[index - 1];
1339
54.1k
  HUNSPELL_WARNING(stderr, "error: bad morph. alias index: %d\n", index);
1340
54.1k
  return nullptr;
1341
69.3k
}
1342
1343
/* parse in the typical fault correcting table */
1344
2.39k
bool HashMgr::parse_reptable(const std::string& line, FileMgr* af) {
1345
2.39k
  if (!reptable.empty()) {
1346
9
    HUNSPELL_WARNING(stderr, "error: line %d: multiple table definitions\n",
1347
9
                     af->getlinenum());
1348
9
    return false;
1349
9
  }
1350
2.38k
  int numrep = -1, i = 0, np = 0;
1351
2.38k
  auto iter = line.begin(), start_piece = mystrsep(line, iter);
1352
12.0k
  while (start_piece != line.end()) {
1353
9.87k
    switch (i) {
1354
2.38k
      case 0: {
1355
2.38k
        np++;
1356
2.38k
        break;
1357
0
      }
1358
2.16k
      case 1: {
1359
2.16k
        numrep = atoi(std::string(start_piece, iter).c_str());
1360
2.16k
        if (numrep < 1) {
1361
209
          HUNSPELL_WARNING(stderr, "error: line %d: incorrect entry number\n",
1362
209
                           af->getlinenum());
1363
209
          return false;
1364
209
        }
1365
1.95k
        reptable.reserve(std::min(numrep, 16384));
1366
1.95k
        np++;
1367
1.95k
        break;
1368
2.16k
      }
1369
5.32k
      default:
1370
5.32k
        break;
1371
9.87k
    }
1372
9.66k
    ++i;
1373
9.66k
    start_piece = mystrsep(line, iter);
1374
9.66k
  }
1375
2.17k
  if (np != 2) {
1376
224
    HUNSPELL_WARNING(stderr, "error: line %d: missing data\n",
1377
224
                     af->getlinenum());
1378
224
    return false;
1379
224
  }
1380
1381
  /* now parse the numrep lines to read in the remainder of the table */
1382
8.11k
  for (int j = 0; j < numrep; ++j) {
1383
8.03k
    std::string nl;
1384
8.03k
    reptable.emplace_back();
1385
8.03k
    int type = 0;
1386
8.03k
    if (af->getline(nl)) {
1387
7.58k
      mychomp(nl);
1388
7.58k
      iter = nl.begin();
1389
7.58k
      i = 0;
1390
7.58k
      start_piece = mystrsep(nl, iter);
1391
7.58k
      bool errored = false;
1392
38.3k
      while (!errored && start_piece != nl.end()) {
1393
30.7k
        switch (i) {
1394
7.42k
          case 0: {
1395
7.42k
            if (nl.compare(start_piece - nl.begin(), 3, "REP", 3) != 0) {
1396
1.07k
              errored = true;
1397
1.07k
              break;
1398
1.07k
            }
1399
6.34k
            break;
1400
7.42k
          }
1401
6.34k
          case 1: {
1402
6.29k
            if (*start_piece == '^')
1403
531
              type = 1;
1404
6.29k
            reptable.back().pattern.assign(start_piece + type, iter);
1405
6.29k
            mystrrep(reptable.back().pattern, "_", " ");
1406
6.29k
            if (!reptable.back().pattern.empty() && reptable.back().pattern[reptable.back().pattern.size() - 1] == '$') {
1407
1.92k
              type += 2;
1408
1.92k
              reptable.back().pattern.resize(reptable.back().pattern.size() - 1);
1409
1.92k
            }
1410
6.29k
            break;
1411
7.42k
          }
1412
6.16k
          case 2: {
1413
6.16k
            reptable.back().outstrings[type].assign(start_piece, iter);
1414
6.16k
            mystrrep(reptable.back().outstrings[type], "_", " ");
1415
6.16k
            break;
1416
7.42k
          }
1417
10.9k
          default:
1418
10.9k
            break;
1419
30.7k
        }
1420
30.7k
        ++i;
1421
30.7k
        start_piece = mystrsep(nl, iter);
1422
30.7k
      }
1423
7.58k
    }
1424
8.03k
    if (reptable.back().pattern.empty() || reptable.back().outstrings[type].empty()) {
1425
1.86k
      HUNSPELL_WARNING(stderr, "error: line %d: table is corrupt\n",
1426
1.86k
                       af->getlinenum());
1427
1.86k
      reptable.clear();
1428
1.86k
      return false;
1429
1.86k
    }
1430
8.03k
  }
1431
85
  return true;
1432
1.95k
}
1433
1434
// return replacing table
1435
5.11M
const std::vector<replentry>& HashMgr::get_reptable() const {
1436
5.11M
  return reptable;
1437
5.11M
}
1438
1439
3.56M
void* HashMgr::arena_alloc(size_t num_bytes, size_t alignment) const {
1440
  // Fixed-size 64KB chunks: small enough to avoid significant waste on small
1441
  // dictionaries, large enough to amortize per-chunk malloc overhead on large
1442
  // ones. make_unique throws std::bad_alloc on OOM.
1443
3.56M
  static const size_t MIN_CHUNK_SIZE = 65536;
1444
3.56M
  static const size_t MAX_ALIGNMENT = alignof(max_align_t);
1445
  // Chunk sizes are rounded up to MAX_ALIGNMENT below; with this invariant,
1446
  // any alignment that divides MAX_ALIGNMENT keeps aligned_offset within bounds.
1447
3.56M
  assert(alignment > 0 && alignment <= MAX_ALIGNMENT);
1448
  // Pad the offset up to the requested alignment before placing this allocation.
1449
  // make_unique returns memory aligned for any scalar, so chunk-start is fine.
1450
3.56M
  size_t aligned_offset = (current_chunk_offset + alignment - 1) & ~(alignment - 1);
1451
3.56M
  if (arena.empty() || current_chunk_size - aligned_offset < num_bytes) {
1452
    // Round the new chunk's size up to a multiple of MAX_ALIGNMENT so that an
1453
    // oversized num_bytes (>= MIN_CHUNK_SIZE) cannot leave a non-aligned
1454
    // current_chunk_size that would later cause aligned_offset to overshoot.
1455
    // Allocate before mutating current_chunk_size so a throwing make_unique
1456
    // leaves the HashMgr in a consistent state.
1457
28.4k
    size_t new_size = std::max(MIN_CHUNK_SIZE, num_bytes);
1458
28.4k
    new_size = (new_size + MAX_ALIGNMENT - 1) & ~(MAX_ALIGNMENT - 1);
1459
28.4k
    arena.push_back(std::make_unique<uint8_t[]>(new_size));
1460
28.4k
    current_chunk_size = new_size;
1461
28.4k
    aligned_offset = 0;
1462
28.4k
  }
1463
1464
3.56M
  uint8_t* ptr = &arena.back()[aligned_offset];
1465
3.56M
  current_chunk_offset = aligned_offset + num_bytes;
1466
3.56M
  ++outstanding_arena_allocations;
1467
3.56M
  return ptr;
1468
3.56M
}
1469
1470
2.79M
void HashMgr::arena_free(void*) const {
1471
  // The arena vector owns all allocations and frees them in bulk at HashMgr
1472
  // destruction, so this is a no-op for the memory itself. The counter is a
1473
  // memory-safety check: more arena_free calls than arena_alloc calls would
1474
  // indicate a double-free or use-after-free in Hunspell. Abort hard rather
1475
  // than silently desynchronize tracking, even in release builds.
1476
2.79M
  if (outstanding_arena_allocations == 0) {
1477
0
    std::abort();
1478
0
  }
1479
2.79M
  --outstanding_arena_allocations;
1480
2.79M
}