1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/renderer/spellchecker/hunspell_engine.h"
6
7#include <algorithm>
8#include <iterator>
9
10#include "base/files/memory_mapped_file.h"
11#include "base/metrics/histogram.h"
12#include "base/time/time.h"
13#include "chrome/common/spellcheck_common.h"
14#include "chrome/common/spellcheck_messages.h"
15#include "content/public/renderer/render_thread.h"
16#include "third_party/hunspell/src/hunspell/hunspell.hxx"
17
18using base::TimeTicks;
19using content::RenderThread;
20
21namespace {
22  // Maximum length of words we actually check.
23  // 64 is the observed limits for OSX system checker.
24  const size_t kMaxCheckedLen = 64;
25
26  // Maximum length of words we provide suggestions for.
27  // 24 is the observed limits for OSX system checker.
28  const size_t kMaxSuggestLen = 24;
29
30  COMPILE_ASSERT(kMaxCheckedLen <= size_t(MAXWORDLEN), MaxCheckedLen_too_long);
31  COMPILE_ASSERT(kMaxSuggestLen <= kMaxCheckedLen, MaxSuggestLen_too_long);
32}
33
34#if !defined(OS_MACOSX)
35SpellingEngine* CreateNativeSpellingEngine() {
36  return new HunspellEngine();
37}
38#endif
39
40HunspellEngine::HunspellEngine()
41    : file_(base::kInvalidPlatformFileValue),
42      initialized_(false),
43      dictionary_requested_(false) {
44  // Wait till we check the first word before doing any initializing.
45}
46
47HunspellEngine::~HunspellEngine() {
48}
49
50void HunspellEngine::Init(base::PlatformFile file) {
51  initialized_ = true;
52  hunspell_.reset();
53  bdict_file_.reset();
54  file_ = file;
55  // Delay the actual initialization of hunspell until it is needed.
56}
57
58void HunspellEngine::InitializeHunspell() {
59  if (hunspell_.get())
60    return;
61
62  bdict_file_.reset(new base::MemoryMappedFile);
63
64  if (bdict_file_->Initialize(file_)) {
65    TimeTicks debug_start_time = base::Histogram::DebugNow();
66
67    hunspell_.reset(
68        new Hunspell(bdict_file_->data(), bdict_file_->length()));
69
70    DHISTOGRAM_TIMES("Spellcheck.InitTime",
71                     base::Histogram::DebugNow() - debug_start_time);
72  } else {
73    NOTREACHED() << "Could not mmap spellchecker dictionary.";
74  }
75}
76
77bool HunspellEngine::CheckSpelling(const base::string16& word_to_check,
78                                   int tag) {
79  // Assume all words that cannot be checked are valid. Since Chrome can't
80  // offer suggestions on them, either, there's no point in flagging them to
81  // the user.
82  bool word_correct = true;
83  std::string word_to_check_utf8(UTF16ToUTF8(word_to_check));
84
85  // Limit the size of checked words.
86  if (word_to_check_utf8.length() <= kMaxCheckedLen) {
87    // If |hunspell_| is NULL here, an error has occurred, but it's better
88    // to check rather than crash.
89    if (hunspell_.get()) {
90      // |hunspell_->spell| returns 0 if the word is misspelled.
91      word_correct = (hunspell_->spell(word_to_check_utf8.c_str()) != 0);
92    }
93  }
94
95  return word_correct;
96}
97
98void HunspellEngine::FillSuggestionList(
99    const base::string16& wrong_word,
100    std::vector<base::string16>* optional_suggestions) {
101  std::string wrong_word_utf8(UTF16ToUTF8(wrong_word));
102  if (wrong_word_utf8.length() > kMaxSuggestLen)
103    return;
104
105  // If |hunspell_| is NULL here, an error has occurred, but it's better
106  // to check rather than crash.
107  // TODO(groby): Technically, it's not. We should track down the issue.
108  if (!hunspell_.get())
109    return;
110
111  char** suggestions = NULL;
112  int number_of_suggestions =
113      hunspell_->suggest(&suggestions, wrong_word_utf8.c_str());
114
115  // Populate the vector of WideStrings.
116  for (int i = 0; i < number_of_suggestions; ++i) {
117    if (i < chrome::spellcheck_common::kMaxSuggestions)
118      optional_suggestions->push_back(UTF8ToUTF16(suggestions[i]));
119    free(suggestions[i]);
120  }
121  if (suggestions != NULL)
122    free(suggestions);
123}
124
125bool HunspellEngine::InitializeIfNeeded() {
126  if (!initialized_ && !dictionary_requested_) {
127    // RenderThread will not exist in test.
128    if (RenderThread::Get())
129      RenderThread::Get()->Send(new SpellCheckHostMsg_RequestDictionary);
130    dictionary_requested_ = true;
131    return true;
132  }
133
134  // Don't initialize if hunspell is disabled.
135  if (file_ != base::kInvalidPlatformFileValue)
136    InitializeHunspell();
137
138  return !initialized_;
139}
140
141bool HunspellEngine::IsEnabled() {
142  return file_ != base::kInvalidPlatformFileValue;
143}
144