1 /* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
2  * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
3 
4 #include "dictdfiles.hh"
5 #include "btreeidx.hh"
6 #include "folding.hh"
7 #include "utf8.hh"
8 #include "dictzip.h"
9 #include "htmlescape.hh"
10 #include "fsencoding.hh"
11 #include "langcoder.hh"
12 #include <map>
13 #include <set>
14 #include <string>
15 #include <vector>
16 #include <list>
17 #include <wctype.h>
18 #include <stdlib.h>
19 #include "gddebug.hh"
20 #include "ftshelpers.hh"
21 #include <QUrl>
22 
23 #include <QDebug>
24 
25 #if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
26 #include <QRegularExpression>
27 #endif
28 
29 #ifdef _MSC_VER
30 #include <stub_msvc.h>
31 #endif
32 
33 namespace DictdFiles {
34 
35 using std::map;
36 using std::multimap;
37 using std::pair;
38 using std::set;
39 using std::string;
40 using gd::wstring;
41 using std::vector;
42 using std::list;
43 
44 using BtreeIndexing::WordArticleLink;
45 using BtreeIndexing::IndexedWords;
46 using BtreeIndexing::IndexInfo;
47 
48 namespace {
49 
50 DEF_EX_STR( exCantReadFile, "Can't read file", Dictionary::Ex )
51 DEF_EX( exFailedToReadLineFromIndex, "Failed to read line from index file", Dictionary::Ex )
52 DEF_EX( exMalformedIndexFileLine, "Malformed index file line encountered", Dictionary::Ex )
53 DEF_EX( exInvalidBase64, "Invalid base64 sequence encountered", Dictionary::Ex )
54 DEF_EX_STR( exDictzipError, "DICTZIP error", Dictionary::Ex )
55 
56 enum
57 {
58   Signature = 0x58444344, // DCDX on little-endian, XDCD on big-endian
59   CurrentFormatVersion = 5 + BtreeIndexing::FormatVersion + Folding::Version
60 };
61 
62 struct IdxHeader
63 {
64   uint32_t signature; // First comes the signature, DCDX
65   uint32_t formatVersion; // File format version (CurrentFormatVersion)
66   uint32_t wordCount; // Total number of words
67   uint32_t articleCount; // Total number of articles
68   uint32_t indexBtreeMaxElements; // Two fields from IndexInfo
69   uint32_t indexRootOffset;
70   uint32_t langFrom;  // Source language
71   uint32_t langTo;    // Target language
72 }
73 #ifndef _MSC_VER
74 __attribute__((packed))
75 #endif
76 ;
77 
indexIsOldOrBad(string const & indexFile)78 bool indexIsOldOrBad( string const & indexFile )
79 {
80   File::Class idx( indexFile, "rb" );
81 
82   IdxHeader header;
83 
84   return idx.readRecords( &header, sizeof( header ), 1 ) != 1 ||
85          header.signature != Signature ||
86          header.formatVersion != CurrentFormatVersion;
87 }
88 
89 class DictdDictionary: public BtreeIndexing::BtreeDictionary
90 {
91   Mutex idxMutex;
92   File::Class idx, indexFile; // The later is .index file
93   IdxHeader idxHeader;
94   dictData * dz;
95   string dictionaryName;
96   Mutex indexFileMutex, dzMutex;
97 
98 public:
99 
100   DictdDictionary( string const & id, string const & indexFile,
101                    vector< string > const & dictionaryFiles );
102 
103   ~DictdDictionary();
104 
getName()105   virtual string getName() throw()
106   { return dictionaryName; }
107 
getProperties()108   virtual map< Dictionary::Property, string > getProperties() throw()
109   { return map< Dictionary::Property, string >(); }
110 
getArticleCount()111   virtual unsigned long getArticleCount() throw()
112   { return idxHeader.articleCount; }
113 
getWordCount()114   virtual unsigned long getWordCount() throw()
115   { return idxHeader.wordCount; }
116 
117   virtual void loadIcon() throw();
118 
getLangFrom() const119   inline virtual quint32 getLangFrom() const
120   { return idxHeader.langFrom; }
121 
getLangTo() const122   inline virtual quint32 getLangTo() const
123   { return idxHeader.langTo; }
124 
125   virtual sptr< Dictionary::DataRequest > getArticle( wstring const &,
126                                                       vector< wstring > const & alts,
127                                                       wstring const &,
128                                                       bool ignoreDiacritics )
129     THROW_SPEC( std::exception );
130 
131   virtual QString const& getDescription();
132 
133   virtual sptr< Dictionary::DataRequest > getSearchResults( QString const & searchString,
134                                                             int searchMode, bool matchCase,
135                                                             int distanceBetweenWords,
136                                                             int maxResults,
137                                                             bool ignoreWordsOrder,
138                                                             bool ignoreDiacritics );
139   void getArticleText( uint32_t articleAddress, QString & headword, QString & text );
140 
141   virtual void makeFTSIndex(QAtomicInt & isCancelled, bool firstIteration );
142 
setFTSParameters(Config::FullTextSearch const & fts)143   virtual void setFTSParameters( Config::FullTextSearch const & fts )
144   {
145     can_FTS = fts.enabled
146               && !fts.disabledTypes.contains( "DICTD", Qt::CaseInsensitive )
147               && ( fts.maxDictionarySize == 0 || getArticleCount() <= fts.maxDictionarySize );
148   }
149 };
150 
DictdDictionary(string const & id,string const & indexFile,vector<string> const & dictionaryFiles)151 DictdDictionary::DictdDictionary( string const & id,
152                                   string const & indexFile,
153                                   vector< string > const & dictionaryFiles ):
154   BtreeDictionary( id, dictionaryFiles ),
155   idx( indexFile, "rb" ),
156   indexFile( dictionaryFiles[ 0 ], "rb" ),
157   idxHeader( idx.read< IdxHeader >() )
158 {
159 
160   // Read the dictionary name
161   idx.seek( sizeof( idxHeader ) );
162 
163   vector< char > dName( idx.read< uint32_t >() );
164   if( dName.size() > 0 )
165   {
166     idx.read( &dName.front(), dName.size() );
167     dictionaryName = string( &dName.front(), dName.size() );
168   }
169 
170   // Open the .dict file
171 
172   DZ_ERRORS error;
173   dz = dict_data_open( dictionaryFiles[ 1 ].c_str(), &error, 0 );
174 
175   if ( !dz )
176     throw exDictzipError( string( dz_error_str( error ) )
177                           + "(" + getDictionaryFilenames()[ 1 ] + ")" );
178 
179   // Initialize the index
180 
181   openIndex( IndexInfo( idxHeader.indexBtreeMaxElements,
182                         idxHeader.indexRootOffset ),
183              idx, idxMutex );
184 
185   // Full-text search parameters
186 
187   can_FTS = true;
188 
189   ftsIdxName = indexFile + "_FTS";
190 
191   if( !Dictionary::needToRebuildIndex( dictionaryFiles, ftsIdxName )
192       && !FtsHelpers::ftsIndexIsOldOrBad( ftsIdxName, this ) )
193     FTS_index_completed.ref();
194 }
195 
~DictdDictionary()196 DictdDictionary::~DictdDictionary()
197 {
198   if ( dz )
199     dict_data_close( dz );
200 }
201 
nameFromFileName(string const & indexFileName)202 string nameFromFileName( string const & indexFileName )
203 {
204   if ( indexFileName.empty() )
205     return string();
206 
207   char const * sep = strrchr( indexFileName.c_str(), FsEncoding::separator() );
208 
209   if ( !sep )
210     sep = indexFileName.c_str();
211 
212   char const * dot = strrchr( sep, '.' );
213 
214   if ( !dot )
215     dot = indexFileName.c_str() + indexFileName.size();
216 
217   return Utf8::encode( FsEncoding::decode( string( sep + 1, dot - sep - 1 ) ) );
218 }
219 
loadIcon()220 void DictdDictionary::loadIcon() throw()
221 {
222   if ( dictionaryIconLoaded )
223     return;
224 
225   QString fileName =
226     QDir::fromNativeSeparators( FsEncoding::decode( getDictionaryFilenames()[ 0 ].c_str() ) );
227 
228   // Remove the extension
229   fileName.chop( 5 );
230 
231   if( !loadIconFromFile( fileName ) )
232   {
233     // Load failed -- use default icons
234     dictionaryNativeIcon = dictionaryIcon = QIcon(":/icons/icon32_dictd.png");
235   }
236 
237   dictionaryIconLoaded = true;
238 }
239 
decodeBase64(string const & str)240 uint32_t decodeBase64( string const & str )
241 {
242   static char const digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
243 
244   uint32_t number = 0;
245 
246   for( char const * next = str.c_str(); *next; ++next )
247   {
248     char const * d = strchr( digits, *next );
249 
250     if ( !d )
251       throw exInvalidBase64();
252 
253     number = number * 64 + ( d - digits );
254   }
255 
256   return number;
257 }
258 
getArticle(wstring const & word,vector<wstring> const & alts,wstring const &,bool ignoreDiacritics)259 sptr< Dictionary::DataRequest > DictdDictionary::getArticle( wstring const & word,
260                                                              vector< wstring > const & alts,
261                                                              wstring const &,
262                                                              bool ignoreDiacritics )
263   THROW_SPEC( std::exception )
264 {
265   try
266   {
267     vector< WordArticleLink > chain = findArticles( word, ignoreDiacritics );
268 
269     for( unsigned x = 0; x < alts.size(); ++x )
270     {
271       /// Make an additional query for each alt
272 
273       vector< WordArticleLink > altChain = findArticles( alts[ x ], ignoreDiacritics );
274 
275       chain.insert( chain.end(), altChain.begin(), altChain.end() );
276     }
277 
278     multimap< wstring, string > mainArticles, alternateArticles;
279 
280     set< uint32_t > articlesIncluded; // Some synonyms make it that the articles
281                                       // appear several times. We combat this
282                                       // by only allowing them to appear once.
283 
284     wstring wordCaseFolded = Folding::applySimpleCaseOnly( word );
285     if( ignoreDiacritics )
286       wordCaseFolded = Folding::applyDiacriticsOnly( wordCaseFolded );
287 
288     char buf[ 16384 ];
289 
290     for( unsigned x = 0; x < chain.size(); ++x )
291     {
292       if ( articlesIncluded.find( chain[ x ].articleOffset ) != articlesIncluded.end() )
293         continue; // We already have this article in the body.
294 
295       // Now load that article
296 
297       {
298         Mutex::Lock _( indexFileMutex );
299         indexFile.seek( chain[ x ].articleOffset );
300 
301         if ( !indexFile.gets( buf, sizeof( buf ), true ) )
302           throw exFailedToReadLineFromIndex();
303       }
304 
305       char * tab1 = strchr( buf, '\t' );
306 
307       if ( !tab1 )
308         throw exMalformedIndexFileLine();
309 
310       char * tab2 = strchr( tab1 + 1, '\t' );
311 
312       if ( !tab2 )
313         throw exMalformedIndexFileLine();
314 
315       // After tab1 should be article offset, after tab2 -- article size
316 
317       uint32_t articleOffset = decodeBase64( string( tab1 + 1, tab2 - tab1 - 1 ) );
318 
319       char * tab3 = strchr( tab2 + 1, '\t');
320 
321       uint32_t articleSize;
322       if ( tab3 )
323       {
324          articleSize = decodeBase64( string( tab2 + 1, tab3 - tab2 - 1 ) );
325       }
326       else
327       {
328         articleSize = decodeBase64( tab2 + 1 );
329       }
330 
331       string articleText;
332 
333       char * articleBody;
334       {
335         Mutex::Lock _( dzMutex );
336         articleBody = dict_data_read_( dz, articleOffset, articleSize, 0, 0 );
337       }
338 
339       if ( !articleBody )
340       {
341         articleText = string( "<div class=\"dictd_article\">DICTZIP error: " )
342                       + dict_error_str( dz ) + "</div>";
343       }
344       else
345       {
346 #if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
347         static QRegularExpression phonetic( "\\\\([^\\\\]+)\\\\",
348                                             QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ...
349         static QRegularExpression refs( "\\{([^\\{\\}]+)\\}",
350                                         QRegularExpression::CaseInsensitiveOption );     // links: {stuff}
351         static QRegularExpression links( "<a href=\"gdlookup://localhost/([^\"]*)\">",
352                                          QRegularExpression::CaseInsensitiveOption );
353         static QRegularExpression tags( "<[^>]*>",
354                                         QRegularExpression::CaseInsensitiveOption );
355 #else
356         static QRegExp phonetic( "\\\\([^\\\\]+)\\\\", Qt::CaseInsensitive ); // phonetics: \stuff\ ...
357         static QRegExp refs( "\\{([^\\{\\}]+)\\}", Qt::CaseInsensitive );     // links: {stuff}
358         static QRegExp links( "<a href=\"gdlookup://localhost/([^\"]*)\">", Qt::CaseInsensitive );
359         static QRegExp tags( "<[^>]*>", Qt::CaseInsensitive );
360 #endif
361 
362         articleText = string( "<div class=\"dictd_article\"" );
363         if( isToLanguageRTL() )
364           articleText += " dir=\"rtl\"";
365         articleText += ">";
366 
367         string convertedText = Html::preformat( articleBody, isToLanguageRTL() );
368         free( articleBody );
369 
370         QString articleString = QString::fromUtf8( convertedText.c_str() )
371                                 .replace(phonetic, "<span class=\"dictd_phonetic\">\\1</span>")
372                                 .replace(refs, "<a href=\"gdlookup://localhost/\\1\">\\1</a>");
373         convertedText.erase();
374 
375         int pos = 0;
376 #if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
377         QString articleNewString;
378         QRegularExpressionMatchIterator it = links.globalMatch( articleString );
379         while( it.hasNext() )
380         {
381           QRegularExpressionMatch match = it.next();
382           articleNewString += articleString.midRef( pos, match.capturedStart() - pos );
383           pos = match.capturedEnd();
384 
385           QString link = match.captured( 1 );
386           link.replace( tags, " " );
387           link.replace( "&nbsp;", " " );
388 
389           QString newLink = match.captured();
390           newLink.replace( 30, match.capturedLength( 1 ),
391                            QString::fromUtf8( QUrl::toPercentEncoding( link.simplified() ) ) );
392           articleNewString += newLink;
393         }
394         if( pos )
395         {
396           articleNewString += articleString.midRef( pos );
397           articleString = articleNewString;
398           articleNewString.clear();
399         }
400 #else
401         for( ; ; )
402         {
403           pos = articleString.indexOf( links, pos );
404           if( pos < 0 )
405             break;
406 
407           QString link = links.cap( 1 );
408           link.replace( tags, " " );
409           link.replace( "&nbsp;", " " );
410           articleString.replace( pos + 30, links.cap( 1 ).length(),
411                                  QString::fromUtf8( QUrl::toPercentEncoding( link.simplified() ) ) );
412           pos += 30;
413         }
414 #endif
415 
416         articleString += "</div>";
417 
418         articleText += articleString.toUtf8().data();
419       }
420 
421       // Ok. Now, does it go to main articles, or to alternate ones? We list
422       // main ones first, and alternates after.
423 
424       // We do the case-folded comparison here.
425 
426       wstring headwordStripped =
427         Folding::applySimpleCaseOnly( Utf8::decode( chain[ x ].word ) );
428       if( ignoreDiacritics )
429         headwordStripped = Folding::applyDiacriticsOnly( headwordStripped );
430 
431       multimap< wstring, string > & mapToUse =
432         ( wordCaseFolded == headwordStripped ) ?
433           mainArticles : alternateArticles;
434 
435       mapToUse.insert( pair< wstring, string >(
436         Folding::applySimpleCaseOnly( Utf8::decode( chain[ x ].word ) ),
437         articleText ) );
438 
439       articlesIncluded.insert( chain[ x ].articleOffset );
440     }
441 
442     if ( mainArticles.empty() && alternateArticles.empty() )
443       return new Dictionary::DataRequestInstant( false );
444 
445     string result;
446 
447     multimap< wstring, string >::const_iterator i;
448 
449     for( i = mainArticles.begin(); i != mainArticles.end(); ++i )
450       result += i->second;
451 
452     for( i = alternateArticles.begin(); i != alternateArticles.end(); ++i )
453       result += i->second;
454 
455     sptr< Dictionary::DataRequestInstant > ret =
456       new Dictionary::DataRequestInstant( true );
457 
458     ret->getData().resize( result.size() );
459 
460     memcpy( &(ret->getData().front()), result.data(), result.size() );
461 
462     return ret;
463   }
464   catch( std::exception & e )
465   {
466     return new Dictionary::DataRequestInstant( QString( e.what() ) );
467   }
468 }
469 
getDescription()470 QString const& DictdDictionary::getDescription()
471 {
472     if( !dictionaryDescription.isEmpty() )
473         return dictionaryDescription;
474 
475     sptr< Dictionary::DataRequest > req = getArticle( GD_NATIVE_TO_WS( L"00databaseinfo" ),
476                                                       vector< wstring >(), wstring(), false );
477 
478     if( req->dataSize() > 0 )
479       dictionaryDescription = Html::unescape( QString::fromUtf8( req->getFullData().data(), req->getFullData().size() ), true );
480     else
481       dictionaryDescription = "NONE";
482 
483     return dictionaryDescription;
484 }
485 
makeFTSIndex(QAtomicInt & isCancelled,bool firstIteration)486 void DictdDictionary::makeFTSIndex( QAtomicInt & isCancelled, bool firstIteration )
487 {
488   if( !( Dictionary::needToRebuildIndex( getDictionaryFilenames(), ftsIdxName )
489          || FtsHelpers::ftsIndexIsOldOrBad( ftsIdxName, this ) ) )
490     FTS_index_completed.ref();
491 
492   if( haveFTSIndex() )
493     return;
494 
495   if( ensureInitDone().size() )
496     return;
497 
498   if( firstIteration && getArticleCount() > FTS::MaxDictionarySizeForFastSearch )
499     return;
500 
501   gdDebug( "DictD: Building the full-text index for dictionary: %s\n",
502            getName().c_str() );
503 
504   try
505   {
506     FtsHelpers::makeFTSIndex( this, isCancelled );
507     FTS_index_completed.ref();
508   }
509   catch( std::exception &ex )
510   {
511     gdWarning( "DictD: Failed building full-text search index for \"%s\", reason: %s\n", getName().c_str(), ex.what() );
512     QFile::remove( FsEncoding::decode( ftsIdxName.c_str() ) );
513   }
514 }
515 
getArticleText(uint32_t articleAddress,QString & headword,QString & text)516 void DictdDictionary::getArticleText( uint32_t articleAddress, QString & headword, QString & text )
517 {
518   try
519   {
520     char buf[ 16384 ];
521     {
522       Mutex::Lock _( indexFileMutex );
523       indexFile.seek( articleAddress );
524 
525       if ( !indexFile.gets( buf, sizeof( buf ), true ) )
526         throw exFailedToReadLineFromIndex();
527     }
528 
529     char * tab1 = strchr( buf, '\t' );
530 
531     if ( !tab1 )
532       throw exMalformedIndexFileLine();
533 
534     headword = QString::fromUtf8( buf, tab1 - buf );
535 
536     char * tab2 = strchr( tab1 + 1, '\t' );
537 
538     if ( !tab2 )
539       throw exMalformedIndexFileLine();
540 
541     // After tab1 should be article offset, after tab2 -- article size
542 
543     uint32_t articleOffset = decodeBase64( string( tab1 + 1, tab2 - tab1 - 1 ) );
544 
545     char * tab3 = strchr( tab2 + 1, '\t');
546 
547     uint32_t articleSize;
548     if ( tab3 )
549     {
550       articleSize = decodeBase64( string( tab2 + 1, tab3 - tab2 - 1 ) );
551     }
552     else
553     {
554       articleSize = decodeBase64( tab2 + 1 );
555     }
556 
557     string articleText;
558 
559     char * articleBody;
560     {
561       Mutex::Lock _( dzMutex );
562       articleBody = dict_data_read_( dz, articleOffset, articleSize, 0, 0 );
563     }
564 
565     if ( !articleBody )
566     {
567       articleText = dict_error_str( dz );
568     }
569     else
570     {
571 #if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
572       static QRegularExpression phonetic( "\\\\([^\\\\]+)\\\\",
573                                           QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ...
574       static QRegularExpression refs( "\\{([^\\{\\}]+)\\}",
575                                       QRegularExpression::CaseInsensitiveOption );     // links: {stuff}
576 #else
577       static QRegExp phonetic( "\\\\([^\\\\]+)\\\\", Qt::CaseInsensitive ); // phonetics: \stuff\ ...
578       static QRegExp refs( "\\{([^\\{\\}]+)\\}", Qt::CaseInsensitive );     // links: {stuff}
579 #endif
580 
581       string convertedText = Html::preformat( articleBody, isToLanguageRTL() );
582       free( articleBody );
583 
584       text = QString::fromUtf8( convertedText.data(), convertedText.size() )
585             .replace(phonetic, "<span class=\"dictd_phonetic\">\\1</span>")
586             .replace(refs, "<a href=\"gdlookup://localhost/\\1\">\\1</a>");
587 
588       text = Html::unescape( text );
589     }
590   }
591   catch( std::exception &ex )
592   {
593     gdWarning( "DictD: Failed retrieving article from \"%s\", reason: %s\n", getName().c_str(), ex.what() );
594   }
595 }
596 
getSearchResults(QString const & searchString,int searchMode,bool matchCase,int distanceBetweenWords,int maxResults,bool ignoreWordsOrder,bool ignoreDiacritics)597 sptr< Dictionary::DataRequest > DictdDictionary::getSearchResults( QString const & searchString,
598                                                                    int searchMode, bool matchCase,
599                                                                    int distanceBetweenWords,
600                                                                    int maxResults,
601                                                                    bool ignoreWordsOrder,
602                                                                    bool ignoreDiacritics )
603 {
604   return new FtsHelpers::FTSResultsRequest( *this, searchString,searchMode, matchCase, distanceBetweenWords, maxResults, ignoreWordsOrder, ignoreDiacritics );
605 }
606 
607 } // anonymous namespace
608 
makeDictionaries(vector<string> const & fileNames,string const & indicesDir,Dictionary::Initializing & initializing)609 vector< sptr< Dictionary::Class > > makeDictionaries(
610                                       vector< string > const & fileNames,
611                                       string const & indicesDir,
612                                       Dictionary::Initializing & initializing )
613   THROW_SPEC( std::exception )
614 {
615   vector< sptr< Dictionary::Class > > dictionaries;
616 
617   for( vector< string >::const_iterator i = fileNames.begin(); i != fileNames.end();
618        ++i )
619   {
620     // Only allow .index suffixes
621 
622     if ( i->size() < 6 ||
623          strcasecmp( i->c_str() + ( i->size() - 6 ), ".index" ) != 0 )
624       continue;
625 
626     try
627     {
628       vector< string > dictFiles( 1, *i );
629 
630       // Check if there is an 'abrv' file present
631       string baseName( *i, 0, i->size() - 5 );
632 
633       dictFiles.push_back( string() );
634 
635       if ( !File::tryPossibleName( baseName + "dict", dictFiles[ 1 ] ) &&
636            !File::tryPossibleName( baseName + "dict.dz", dictFiles[ 1 ] ) )
637       {
638         // No corresponding .dict file, skipping
639         continue;
640       }
641 
642       string dictId = Dictionary::makeDictionaryId( dictFiles );
643 
644       string indexFile = indicesDir + dictId;
645 
646       if ( Dictionary::needToRebuildIndex( dictFiles, indexFile ) ||
647            indexIsOldOrBad( indexFile ) )
648       {
649         // Building the index
650         string dictionaryName = nameFromFileName( dictFiles[ 0 ] );
651 
652         gdDebug( "DictD: Building the index for dictionary: %s\n", dictionaryName.c_str() );
653 
654         initializing.indexingDictionary( dictionaryName );
655 
656         File::Class idx( indexFile, "wb" );
657 
658         IdxHeader idxHeader;
659 
660         memset( &idxHeader, 0, sizeof( idxHeader ) );
661 
662         // We write a dummy header first. At the end of the process the header
663         // will be rewritten with the right values.
664 
665         idx.write( idxHeader );
666 
667         IndexedWords indexedWords;
668 
669         File::Class indexFile( dictFiles[ 0 ], "rb" );
670 
671         // Read words from index until none's left.
672 
673         char buf[ 16384 ];
674 
675         do
676         {
677           uint32_t curOffset = indexFile.tell();
678 
679           if ( !indexFile.gets( buf, sizeof( buf ), true ) )
680             break;
681 
682           // Check that there are exactly two or three tabs in the record.
683           char * tab1 = strchr( buf, '\t' );
684           if ( tab1 )
685           {
686             char * tab2 = strchr( tab1 + 1, '\t' );
687             if ( tab2 )
688             {
689               char * tab3 = strchr( tab2 + 1, '\t');
690               if ( tab3 )
691               {
692                 char * tab4 = strchr( tab3 + 1, '\t');
693                 if ( tab4 )
694                 {
695                   GD_DPRINTF( "Warning: too many tabs present, skipping: %s\n", buf );
696                   continue;
697                 }
698 
699                 // Handle the forth entry, if it exists. From dictfmt man:
700                 // When --index-keep-orig option is used fourth column is created
701                 // (if necessary) in .index file.
702                 indexedWords.addWord( Utf8::decode( string( tab3 + 1, strlen ( tab3 + 1 ) ) ), curOffset );
703                 ++idxHeader.wordCount;
704               }
705               indexedWords.addWord( Utf8::decode( string( buf, strchr( buf, '\t' ) - buf ) ), curOffset );
706               ++idxHeader.wordCount;
707               ++idxHeader.articleCount;
708 
709               // Check for proper dictionary name
710               if ( !strncmp( buf, "00databaseshort", 15 ) || !strncmp( buf, "00-database-short", 17 ) )
711               {
712                 // After tab1 should be article offset, after tab2 -- article size
713                 uint32_t articleOffset = decodeBase64( string( tab1 + 1, tab2 - tab1 - 1 ) );
714                 uint32_t articleSize = decodeBase64( tab2 + 1 );
715 
716                 DZ_ERRORS error;
717                 dictData * dz = dict_data_open( dictFiles[ 1 ].c_str(), &error, 0 );
718 
719                 if ( dz )
720                 {
721                   char * articleBody = dict_data_read_( dz, articleOffset, articleSize, 0, 0 );
722                   if ( articleBody )
723                   {
724                     char * eol;
725                     if ( !strncmp( articleBody, "00databaseshort", 15 ) || !strncmp( articleBody, "00-database-short", 17 ) )
726                       eol = strchr( articleBody, '\n'  ); // skip the first line (headword itself)
727                     else
728                       eol = articleBody; // No headword itself
729                     if ( eol )
730                     {
731                       while( *eol && Utf8::isspace( *eol ) ) ++eol; // skip spaces
732 
733                       // use only the single line for the dictionary title
734                       char * endEol = strchr( eol, '\n' );
735                       if ( endEol )
736                         *endEol = 0;
737 
738                       GD_DPRINTF( "DICT NAME: '%s'\n", eol );
739                       dictionaryName = eol;
740                     }
741                   }
742                   dict_data_close( dz );
743                 }
744                 else
745                   throw exDictzipError( string( dz_error_str( error ) )
746                                         + "(" + dictFiles[ 1 ] + ")" );
747               }
748             }
749             else
750             {
751               GD_DPRINTF( "Warning: only a single tab present, skipping: %s\n", buf );
752               continue;
753             }
754           }
755           else
756           {
757             GD_DPRINTF( "Warning: no tabs present, skipping: %s\n", buf );
758             continue;
759           }
760 
761 
762         } while( !indexFile.eof() );
763 
764 
765         // Write dictionary name
766 
767         idx.write( (uint32_t) dictionaryName.size() );
768         idx.write( dictionaryName.data(), dictionaryName.size() );
769 
770         // Build index
771 
772         IndexInfo idxInfo = BtreeIndexing::buildIndex( indexedWords, idx );
773 
774         idxHeader.indexBtreeMaxElements = idxInfo.btreeMaxElements;
775         idxHeader.indexRootOffset = idxInfo.rootOffset;
776 
777         // That concludes it. Update the header.
778 
779         idxHeader.signature = Signature;
780         idxHeader.formatVersion = CurrentFormatVersion;
781 
782         // read languages
783         QPair<quint32,quint32> langs =
784             LangCoder::findIdsForFilename( QString::fromStdString( dictFiles[ 0 ] ) );
785 
786         // if no languages found, try dictionary's name
787         if ( langs.first == 0 || langs.second == 0 )
788         {
789           langs =
790             LangCoder::findIdsForFilename( QString::fromStdString( nameFromFileName( dictFiles[ 0 ] ) ) );
791         }
792 
793         idxHeader.langFrom = langs.first;
794         idxHeader.langTo = langs.second;
795 
796         idx.rewind();
797 
798         idx.write( &idxHeader, sizeof( idxHeader ) );
799       }
800 
801       dictionaries.push_back( new DictdDictionary( dictId,
802                                                    indexFile,
803                                                    dictFiles ) );
804     }
805     catch( std::exception & e )
806     {
807       gdWarning( "Dictd dictionary \"%s\" reading failed, error: %s\n",
808                  i->c_str(), e.what() );
809     }
810   }
811 
812   return dictionaries;
813 }
814 
815 }
816