[GTK] WebKitWebProcess at 100% CPU loading hyphenation dictionaries
[WebKit-https.git] / Source / WebCore / platform / text / hyphen / HyphenationLibHyphen.cpp
1 /*
2  * Copyright (C) 2010 Apple Inc. All rights reserved.
3  * Copyright (C) 2015 Igalia S.L.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 #include "config.h"
28 #include "Hyphenation.h"
29
30 #if USE(LIBHYPHEN)
31
32 #include "FileSystem.h"
33 #include <hyphen.h>
34 #include <limits>
35 #include <stdlib.h>
36 #include <wtf/HashMap.h>
37 #include <wtf/NeverDestroyed.h>
38 #include <wtf/TinyLRUCache.h>
39 #include <wtf/text/AtomicStringHash.h>
40 #include <wtf/text/CString.h>
41 #include <wtf/text/StringView.h>
42
43 #if PLATFORM(GTK)
44 #include "GtkUtilities.h"
45 #include <wtf/glib/GUniquePtr.h>
46 #endif
47
48 namespace WebCore {
49
50 static const char* const gDictionaryDirectories[] = {
51     "/usr/share/hyphen",
52     "/usr/local/share/hyphen",
53 };
54
55 static String extractLocaleFromDictionaryFilePath(const String& filePath)
56 {
57     // Dictionary files always have the form "hyph_<locale name>.dic"
58     // so we strip everything except the locale.
59     String fileName = pathGetFileName(filePath);
60     static const int prefixLength = 5;
61     static const int suffixLength = 4;
62     return fileName.substring(prefixLength, fileName.length() - prefixLength - suffixLength);
63 }
64
65 static void scanDirectoryForDicionaries(const char* directoryPath, HashMap<AtomicString, Vector<String>>& availableLocales)
66 {
67     for (auto& filePath : listDirectory(directoryPath, "hyph_*.dic")) {
68         String locale = extractLocaleFromDictionaryFilePath(filePath).convertToASCIILowercase();
69
70         char normalizedPath[PATH_MAX];
71         if (!realpath(fileSystemRepresentation(filePath).data(), normalizedPath))
72             continue;
73
74         filePath = stringFromFileSystemRepresentation(normalizedPath);
75         availableLocales.add(locale, Vector<String>()).iterator->value.append(filePath);
76
77         String localeReplacingUnderscores = String(locale);
78         localeReplacingUnderscores.replace('_', '-');
79         if (locale != localeReplacingUnderscores)
80             availableLocales.add(localeReplacingUnderscores, Vector<String>()).iterator->value.append(filePath);
81
82         size_t dividerPosition = localeReplacingUnderscores.find('-');
83         if (dividerPosition != notFound) {
84             localeReplacingUnderscores.truncate(dividerPosition);
85             availableLocales.add(localeReplacingUnderscores, Vector<String>()).iterator->value.append(filePath);
86         }
87     }
88 }
89
90 #if ENABLE(DEVELOPER_MODE)
91 static void scanTestDictionariesDirectoryIfNecessary(HashMap<AtomicString, Vector<String>>& availableLocales)
92 {
93     // It's unfortunate that we need to look for the dictionaries this way, but
94     // libhyphen doesn't have the concept of installed dictionaries. Instead,
95     // we have this special case for WebKit tests.
96 #if PLATFORM(GTK)
97     CString buildDirectory = webkitBuildDirectory();
98     GUniquePtr<char> dictionariesPath(g_build_filename(buildDirectory.data(), "DependenciesGTK", "Root", "webkitgtk-test-dicts", nullptr));
99     if (g_file_test(dictionariesPath.get(), static_cast<GFileTest>(G_FILE_TEST_IS_DIR))) {
100         scanDirectoryForDicionaries(dictionariesPath.get(), availableLocales);
101         return;
102     }
103
104     // Try alternative dictionaries path for people not using JHBuild.
105     dictionariesPath.reset(g_build_filename(buildDirectory.data(), "webkitgtk-test-dicts", nullptr));
106     scanDirectoryForDicionaries(dictionariesPath.get(), availableLocales);
107 #elif defined(TEST_HYPHENATAION_PATH)
108     scanDirectoryForDicionaries(TEST_HYPHENATAION_PATH, availableLocales);
109 #else
110     UNUSED_PARAM(availableLocales);
111 #endif
112 }
113 #endif
114
115 static HashMap<AtomicString, Vector<String>>& availableLocales()
116 {
117     static bool scannedLocales = false;
118     static HashMap<AtomicString, Vector<String>> availableLocales;
119
120     if (!scannedLocales) {
121         for (size_t i = 0; i < WTF_ARRAY_LENGTH(gDictionaryDirectories); i++)
122             scanDirectoryForDicionaries(gDictionaryDirectories[i], availableLocales);
123
124 #if ENABLE(DEVELOPER_MODE)
125         scanTestDictionariesDirectoryIfNecessary(availableLocales);
126 #endif
127
128         scannedLocales = true;
129     }
130
131     return availableLocales;
132 }
133
134 bool canHyphenate(const AtomicString& localeIdentifier)
135 {
136     if (localeIdentifier.isNull())
137         return false;
138     if (availableLocales().contains(localeIdentifier))
139         return true;
140     return availableLocales().contains(AtomicString(localeIdentifier.string().convertToASCIILowercase()));
141 }
142
143 class HyphenationDictionary : public RefCounted<HyphenationDictionary> {
144     WTF_MAKE_NONCOPYABLE(HyphenationDictionary);
145     WTF_MAKE_FAST_ALLOCATED;
146 public:
147     typedef std::unique_ptr<HyphenDict, void(*)(HyphenDict*)> HyphenDictUniquePtr;
148
149     virtual ~HyphenationDictionary() { }
150     static RefPtr<HyphenationDictionary> createNull()
151     {
152         return adoptRef(new HyphenationDictionary());
153     }
154
155     static RefPtr<HyphenationDictionary> create(const CString& dictPath)
156     {
157         return adoptRef(new HyphenationDictionary(dictPath));
158     }
159
160     HyphenDict* libhyphenDictionary() const
161     {
162         return m_libhyphenDictionary.get();
163     }
164
165 private:
166     HyphenationDictionary(const CString& dictPath)
167         : m_libhyphenDictionary(HyphenDictUniquePtr(hnj_hyphen_load(dictPath.data()), hnj_hyphen_free))
168     {
169     }
170
171     HyphenationDictionary()
172         : m_libhyphenDictionary(HyphenDictUniquePtr(nullptr, hnj_hyphen_free))
173     {
174     }
175
176     HyphenDictUniquePtr m_libhyphenDictionary;
177 };
178
179 } // namespace WebCore
180
181 namespace WTF {
182
183 template<>
184 class TinyLRUCachePolicy<AtomicString, RefPtr<WebCore::HyphenationDictionary>>
185 {
186 public:
187     static TinyLRUCache<AtomicString, RefPtr<WebCore::HyphenationDictionary>, 32>& cache()
188     {
189         static NeverDestroyed<TinyLRUCache<AtomicString, RefPtr<WebCore::HyphenationDictionary>, 32>> cache;
190         return cache;
191     }
192
193     static bool isKeyNull(const AtomicString& localeIdentifier)
194     {
195         return localeIdentifier.isNull();
196     }
197
198     static RefPtr<WebCore::HyphenationDictionary> createValueForNullKey()
199     {
200         return WebCore::HyphenationDictionary::createNull();
201     }
202
203     static RefPtr<WebCore::HyphenationDictionary> createValueForKey(const AtomicString& dictionaryPath)
204     {
205         return WebCore::HyphenationDictionary::create(WebCore::fileSystemRepresentation(dictionaryPath.string()));
206     }
207 };
208
209 } // namespace WTF
210
211 namespace WebCore {
212
213 static void countLeadingSpaces(const CString& utf8String, int32_t& pointerOffset, int32_t& characterOffset)
214 {
215     pointerOffset = 0;
216     characterOffset = 0;
217     const char* stringData = utf8String.data();
218     UChar32 character = 0;
219     while (static_cast<unsigned>(pointerOffset) < utf8String.length()) {
220         int32_t nextPointerOffset = pointerOffset;
221         U8_NEXT(stringData, nextPointerOffset, static_cast<int32_t>(utf8String.length()), character);
222
223         if (character < 0 || !u_isUWhiteSpace(character))
224             return;
225
226         pointerOffset = nextPointerOffset;
227         characterOffset++;
228     }
229 }
230
231 size_t lastHyphenLocation(StringView string, size_t beforeIndex, const AtomicString& localeIdentifier)
232 {
233     // libhyphen accepts strings in UTF-8 format, but WebCore can only provide StringView
234     // which stores either UTF-16 or Latin1 data. This is unfortunate for performance
235     // reasons and we should consider switching to a more flexible hyphenation library
236     // if it is available.
237     CString utf8StringCopy = string.toStringWithoutCopying().utf8();
238
239     // WebCore often passes strings like " wordtohyphenate" to the platform layer. Since
240     // libhyphen isn't advanced enough to deal with leading spaces (presumably CoreFoundation
241     // can), we should find the appropriate indexes into the string to skip them.
242     int32_t leadingSpaceBytes;
243     int32_t leadingSpaceCharacters;
244     countLeadingSpaces(utf8StringCopy, leadingSpaceBytes, leadingSpaceCharacters);
245
246     // The libhyphen documentation specifies that this array should be 5 bytes longer than
247     // the byte length of the input string.
248     Vector<char> hyphenArray(utf8StringCopy.length() - leadingSpaceBytes + 5);
249     char* hyphenArrayData = hyphenArray.data();
250
251     String lowercaseLocaleIdentifier = AtomicString(localeIdentifier.string().convertToASCIILowercase());
252     ASSERT(availableLocales().contains(lowercaseLocaleIdentifier));
253     for (const auto& dictionaryPath : availableLocales().get(lowercaseLocaleIdentifier)) {
254         RefPtr<HyphenationDictionary> dictionary = WTF::TinyLRUCachePolicy<AtomicString, RefPtr<HyphenationDictionary>>::cache().get(AtomicString(dictionaryPath));
255
256         char** replacements = nullptr;
257         int* positions = nullptr;
258         int* removedCharacterCounts = nullptr;
259         hnj_hyphen_hyphenate2(dictionary->libhyphenDictionary(),
260             utf8StringCopy.data() + leadingSpaceBytes,
261             utf8StringCopy.length() - leadingSpaceBytes,
262             hyphenArrayData,
263             nullptr, /* output parameter for hyphenated word */
264             &replacements,
265             &positions,
266             &removedCharacterCounts);
267
268         if (replacements) {
269             for (unsigned i = 0; i < utf8StringCopy.length() - leadingSpaceBytes - 1; i++)
270                 free(replacements[i]);
271             free(replacements);
272         }
273
274         free(positions);
275         free(removedCharacterCounts);
276
277         for (int i = beforeIndex - leadingSpaceCharacters - 2; i >= 0; i--) {
278             // libhyphen will put an odd number in hyphenArrayData at all
279             // hyphenation points. A number & 1 will be true for odd numbers.
280             if (hyphenArrayData[i] & 1)
281                 return i + 1 + leadingSpaceCharacters;
282         }
283     }
284
285     return 0;
286 }
287
288 } // namespace WebCore
289
290 #endif // USE(LIBHYPHEN)