CVE-2022-41917 (published 2022-11-16T00:15Z)

OpenSearch is a community-driven, open source fork of Elasticsearch and Kibana. OpenSearch allows users to specify a local file when defining text analyzers to process data for text analysis. An issue in the implementation of this feature allows certain specially crafted queries to return a response containing the first line of text from arbitrary files. The list of potentially impacted files is limited to text files with read permissions allowed in the Java Security Manager policy configuration. OpenSearch version 1.3.7 and 2.4.0 contain a fix for this issue. Users are advised to upgrade. There are no known workarounds for this issue.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-755
cwe_name: Improper Handling of Exceptional Conditions
description: The product does not handle or incorrectly handles an exceptional condition.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/opensearch-project/OpenSearch
commit_details: repo_url: https://github.com/opensearch-project/OpenSearch
author: Rabi Panda
author_date: 2022-11-07 16:52:52-08:00
msg: Merge pull request from GHSA-w3rx-m34v-wrqx

* Fix error handling while reading analyzer mapping rules

Add new parseWordList method that takes a parser as a parameter. It reads custom rules from settings or a file, parses and handles errors. Make error messages less verbose for rules files outside config directory.

Signed-off-by: Rabi Panda <adnapibar@gmail.com>

* Add CHANGELOG.md

Signed-off-by: Rabi Panda <adnapibar@gmail.com>

Signed-off-by: Rabi Panda <adnapibar@gmail.com>
repository: repo_url: https://github.com/opensearch-project/OpenSearch
repo_name: opensearch-project/OpenSearch
description: 🔎 Open source distributed and RESTful search engine.
repo_language: Java
owner: opensearch-project
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -212,6 +212,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Fix flaky test ResourceAwareTasksTests on Windows ([#5077](https://github.com/opensearch-project/OpenSearch/pull/5077))
 - Length calculation for block based fetching ([#5055](https://github.com/opensearch-project/OpenSearch/pull/5055))
 - [Segment Replication] Fix for AlreadyClosedException for engine ([#4743](https://github.com/opensearch-project/OpenSearch/pull/4743))
+- Fix error handling while reading analyzer mapping rules
 ### Security
 - CVE-2022-25857 org.yaml:snakeyaml DOS vulnerability ([#4341](https://github.com/opensearch-project/OpenSearch/pull/4341))
 

programming_language: Markdown
filename: HyphenationCompoundWordTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -32,12 +32,14 @@
 
 package org.opensearch.analysis.common;
 
+import org.apache.logging.log4j.LogManager;
 import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.compound.HyphenationCompoundWordTokenFilter;
 import org.apache.lucene.analysis.compound.hyphenation.HyphenationTree;
 import org.opensearch.common.settings.Settings;
 import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
+import org.opensearch.index.analysis.Analysis;
 import org.xml.sax.InputSource;
 
 import java.io.InputStream;
@@ -61,13 +63,15 @@ public class HyphenationCompoundWordTokenFilterFactory extends AbstractCompoundW
             throw new IllegalArgumentException("hyphenation_patterns_path is a required setting.");
         }
 
-        Path hyphenationPatternsFile = env.configDir().resolve(hyphenationPatternsPath);
+        Path hyphenationPatternsFile = Analysis.resolveAnalyzerPath(env, hyphenationPatternsPath);
 
         try {
             InputStream in = Files.newInputStream(hyphenationPatternsFile);
             hyphenationTree = HyphenationCompoundWordTokenFilter.getHyphenationTree(new InputSource(in));
         } catch (Exception e) {
-            throw new IllegalArgumentException("Exception while reading hyphenation_patterns_path.", e);
+            LogManager.getLogger(HyphenationCompoundWordTokenFilterFactory.class)
+                .error("Exception while reading hyphenation_patterns_path ", e);
+            throw new IllegalArgumentException("Exception while reading hyphenation_patterns_path.");
         }
     }
 

programming_language: Java
filename: MappingCharFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -39,6 +39,7 @@ import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.analysis.AbstractCharFilterFactory;
 import org.opensearch.index.analysis.Analysis;
+import org.opensearch.index.analysis.MappingRule;
 import org.opensearch.index.analysis.NormalizingCharFilterFactory;
 
 import java.io.Reader;
@@ -53,13 +54,13 @@ public class MappingCharFilterFactory extends AbstractCharFilterFactory implemen
     MappingCharFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) {
         super(indexSettings, name);
 
-        List<String> rules = Analysis.getWordList(env, settings, "mappings");
+        List<MappingRule<String, String>> rules = Analysis.parseWordList(env, settings, "mappings", this::parse);
         if (rules == null) {
             throw new IllegalArgumentException("mapping requires either `mappings` or `mappings_path` to be configured");
         }
 
         NormalizeCharMap.Builder normMapBuilder = new NormalizeCharMap.Builder();
-        parseRules(rules, normMapBuilder);
+        rules.forEach(rule -> normMapBuilder.add(rule.getLeft(), rule.getRight()));
         normMap = normMapBuilder.build();
     }
 
@@ -71,18 +72,13 @@ public class MappingCharFilterFactory extends AbstractCharFilterFactory implemen
     // source => target
     private static Pattern rulePattern = Pattern.compile("(.*)\\s*=>\\s*(.*)\\s*$");
 
-    /**
-     * parses a list of MappingCharFilter style rules into a normalize char map
-     */
-    private void parseRules(List<String> rules, NormalizeCharMap.Builder map) {
-        for (String rule : rules) {
-            Matcher m = rulePattern.matcher(rule);
-            if (!m.find()) throw new RuntimeException("Invalid Mapping Rule : [" + rule + "]");
-            String lhs = parseString(m.group(1).trim());
-            String rhs = parseString(m.group(2).trim());
-            if (lhs == null || rhs == null) throw new RuntimeException("Invalid Mapping Rule : [" + rule + "]. Illegal mapping.");
-            map.add(lhs, rhs);
-        }
+    private MappingRule<String, String> parse(String rule) {
+        Matcher m = rulePattern.matcher(rule);
+        if (!m.find()) throw new RuntimeException("Invalid mapping rule : [" + rule + "]");
+        String lhs = parseString(m.group(1).trim());
+        String rhs = parseString(m.group(2).trim());
+        if (lhs == null || rhs == null) throw new RuntimeException("Invalid mapping rule: [" + rule + "]. Illegal mapping.");
+        return new MappingRule<>(lhs, rhs);
     }
 
     char[] out = new char[256];

programming_language: Java
filename: StemmerOverrideTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -40,24 +40,31 @@ import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.analysis.AbstractTokenFilterFactory;
 import org.opensearch.index.analysis.Analysis;
+import org.opensearch.index.analysis.MappingRule;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 public class StemmerOverrideTokenFilterFactory extends AbstractTokenFilterFactory {
 
+    private static final String MAPPING_SEPARATOR = "=>";
     private final StemmerOverrideMap overrideMap;
 
     StemmerOverrideTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) throws IOException {
         super(indexSettings, name, settings);
 
-        List<String> rules = Analysis.getWordList(env, settings, "rules");
+        List<MappingRule<List<String>, String>> rules = Analysis.parseWordList(env, settings, "rules", this::parse);
         if (rules == null) {
             throw new IllegalArgumentException("stemmer override filter requires either `rules` or `rules_path` to be configured");
         }
 
         StemmerOverrideFilter.Builder builder = new StemmerOverrideFilter.Builder(false);
-        parseRules(rules, builder, "=>");
+        for (MappingRule<List<String>, String> rule : rules) {
+            for (String key : rule.getLeft()) {
+                builder.add(key, rule.getRight());
+            }
+        }
         overrideMap = builder.build();
 
     }
@@ -67,27 +74,26 @@ public class StemmerOverrideTokenFilterFactory extends AbstractTokenFilterFactor
         return new StemmerOverrideFilter(tokenStream, overrideMap);
     }
 
-    static void parseRules(List<String> rules, StemmerOverrideFilter.Builder builder, String mappingSep) {
-        for (String rule : rules) {
-            String[] sides = rule.split(mappingSep, -1);
-            if (sides.length != 2) {
-                throw new RuntimeException("Invalid Keyword override Rule:" + rule);
-            }
+    private MappingRule<List<String>, String> parse(String rule) {
+        String[] sides = rule.split(MAPPING_SEPARATOR, -1);
+        if (sides.length != 2) {
+            throw new RuntimeException("Invalid keyword override rule: " + rule);
+        }
 
-            String[] keys = sides[0].split(",", -1);
-            String override = sides[1].trim();
-            if (override.isEmpty() || override.indexOf(',') != -1) {
-                throw new RuntimeException("Invalid Keyword override Rule:" + rule);
-            }
+        String[] keys = sides[0].split(",", -1);
+        String override = sides[1].trim();
+        if (override.isEmpty() || override.indexOf(',') != -1) {
+            throw new RuntimeException("Invalid keyword override rule: " + rule);
+        }
 
-            for (String key : keys) {
-                String trimmedKey = key.trim();
-                if (trimmedKey.isEmpty()) {
-                    throw new RuntimeException("Invalid Keyword override Rule:" + rule);
-                }
-                builder.add(trimmedKey, override);
+        List<String> trimmedKeys = new ArrayList<>();
+        for (String key : keys) {
+            String trimmedKey = key.trim();
+            if (trimmedKey.isEmpty()) {
+                throw new RuntimeException("Invalid keyword override rule: " + rule);
             }
+            trimmedKeys.add(trimmedKey);
         }
+        return new MappingRule<>(trimmedKeys, override);
     }
-
 }

programming_language: Java
filename: SynonymTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,7 @@
 
 package org.opensearch.analysis.common;
 
+import org.apache.logging.log4j.LogManager;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.synonym.SynonymFilter;
@@ -155,14 +156,15 @@ public class SynonymTokenFilterFactory extends AbstractTokenFilterFactory {
             }
             return parser.build();
         } catch (Exception e) {
-            throw new IllegalArgumentException("failed to build synonyms", e);
+            LogManager.getLogger(SynonymTokenFilterFactory.class).error("Failed to build synonyms: ", e);
+            throw new IllegalArgumentException("Failed to build synonyms");
         }
     }
 
     Reader getRulesFromSettings(Environment env) {
         Reader rulesReader;
         if (settings.getAsList("synonyms", null) != null) {
-            List<String> rulesList = Analysis.getWordList(env, settings, "synonyms");
+            List<String> rulesList = Analysis.parseWordList(env, settings, "synonyms", s -> s);
             StringBuilder sb = new StringBuilder();
             for (String line : rulesList) {
                 sb.append(line).append(System.lineSeparator());

programming_language: Java
filename: WordDelimiterGraphTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -41,6 +41,7 @@ import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.analysis.AbstractTokenFilterFactory;
 import org.opensearch.index.analysis.Analysis;
+import org.opensearch.index.analysis.MappingRule;
 import org.opensearch.index.analysis.TokenFilterFactory;
 
 import java.util.List;
@@ -73,7 +74,12 @@ public class WordDelimiterGraphTokenFilterFactory extends AbstractTokenFilterFac
         // . => DIGIT
         // \u002C => DIGIT
         // \u200D => ALPHANUM
-        List<String> charTypeTableValues = Analysis.getWordList(env, settings, "type_table");
+        List<MappingRule<Character, Byte>> charTypeTableValues = Analysis.parseWordList(
+            env,
+            settings,
+            "type_table",
+            WordDelimiterTokenFilterFactory::parse
+        );
         if (charTypeTableValues == null) {
             this.charTypeTable = WordDelimiterIterator.DEFAULT_WORD_DELIM_TABLE;
         } else {

programming_language: Java
filename: WordDelimiterTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -41,6 +41,7 @@ import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.analysis.AbstractTokenFilterFactory;
 import org.opensearch.index.analysis.Analysis;
+import org.opensearch.index.analysis.MappingRule;
 import org.opensearch.index.analysis.TokenFilterFactory;
 
 import java.util.Collection;
@@ -76,7 +77,12 @@ public class WordDelimiterTokenFilterFactory extends AbstractTokenFilterFactory
         // . => DIGIT
         // \u002C => DIGIT
         // \u200D => ALPHANUM
-        List<String> charTypeTableValues = Analysis.getWordList(env, settings, "type_table");
+        List<MappingRule<Character, Byte>> charTypeTableValues = Analysis.parseWordList(
+            env,
+            settings,
+            "type_table",
+            WordDelimiterTokenFilterFactory::parse
+        );
         if (charTypeTableValues == null) {
             this.charTypeTable = WordDelimiterIterator.DEFAULT_WORD_DELIM_TABLE;
         } else {
@@ -127,19 +133,23 @@ public class WordDelimiterTokenFilterFactory extends AbstractTokenFilterFactory
     // source => type
     private static Pattern typePattern = Pattern.compile("(.*)\\s*=>\\s*(.*)\\s*$");
 
+    static MappingRule<Character, Byte> parse(String rule) {
+        Matcher m = typePattern.matcher(rule);
+        if (!m.find()) throw new RuntimeException("Invalid mapping rule: [" + rule + "]");
+        String lhs = parseString(m.group(1).trim());
+        Byte rhs = parseType(m.group(2).trim());
+        if (lhs.length() != 1) throw new RuntimeException("Invalid mapping rule: [" + rule + "]. Only a single character is allowed.");
+        if (rhs == null) throw new RuntimeException("Invalid mapping rule: [" + rule + "]. Illegal type.");
+        return new MappingRule<>(lhs.charAt(0), rhs);
+    }
+
     /**
      * parses a list of MappingCharFilter style rules into a custom byte[] type table
      */
-    static byte[] parseTypes(Collection<String> rules) {
+    static byte[] parseTypes(Collection<MappingRule<Character, Byte>> rules) {
         SortedMap<Character, Byte> typeMap = new TreeMap<>();
-        for (String rule : rules) {
-            Matcher m = typePattern.matcher(rule);
-            if (!m.find()) throw new RuntimeException("Invalid Mapping Rule : [" + rule + "]");
-            String lhs = parseString(m.group(1).trim());
-            Byte rhs = parseType(m.group(2).trim());
-            if (lhs.length() != 1) throw new RuntimeException("Invalid Mapping Rule : [" + rule + "]. Only a single character is allowed.");
-            if (rhs == null) throw new RuntimeException("Invalid Mapping Rule : [" + rule + "]. Illegal type.");
-            typeMap.put(lhs.charAt(0), rhs);
+        for (MappingRule<Character, Byte> rule : rules) {
+            typeMap.put(rule.getLeft(), rule.getRight());
         }
 
         // ensure the table is always at least as big as DEFAULT_WORD_DELIM_TABLE for performance

programming_language: Java
filename: BaseWordDelimiterTokenFilterFactoryTestCase.java
change_type: ModificationType.MODIFY
diff: @@ -195,4 +195,24 @@ public abstract class BaseWordDelimiterTokenFilterFactoryTestCase extends OpenSe
         tokenizer.setReader(new StringReader(source));
         assertTokenStreamContents(tokenFilter.create(tokenizer), expected);
     }
+
+    private void createTokenFilterFactoryWithTypeTable(String[] rules) throws IOException {
+        OpenSearchTestCase.TestAnalysis analysis = AnalysisTestsHelper.createTestAnalysisFromSettings(
+            Settings.builder()
+                .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString())
+                .put("index.analysis.filter.my_word_delimiter.type", type)
+                .putList("index.analysis.filter.my_word_delimiter.type_table", rules)
+                .put("index.analysis.filter.my_word_delimiter.catenate_words", "true")
+                .put("index.analysis.filter.my_word_delimiter.generate_word_parts", "true")
+                .build(),
+            new CommonAnalysisModulePlugin()
+        );
+        analysis.tokenFilter.get("my_word_delimiter");
+    }
+
+    public void testTypeTableParsingError() {
+        String[] rules = { "# This is a comment", "$ => DIGIT", "\\u200D => ALPHANUM", "abc => ALPHA" };
+        RuntimeException ex = expectThrows(RuntimeException.class, () -> createTokenFilterFactoryWithTypeTable(rules));
+        assertEquals("Line [4]: Invalid mapping rule: [abc => ALPHA]. Only a single character is allowed.", ex.getMessage());
+    }
 }

programming_language: Java
filename: MappingCharFilterFactoryTests.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,70 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.analysis.common;
+
+import org.apache.lucene.analysis.CharFilter;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.env.Environment;
+import org.opensearch.index.analysis.AnalysisTestsHelper;
+import org.opensearch.index.analysis.CharFilterFactory;
+import org.opensearch.test.OpenSearchTestCase;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+
+public class MappingCharFilterFactoryTests extends OpenSearchTestCase {
+    public static CharFilterFactory create(String... rules) throws IOException {
+        OpenSearchTestCase.TestAnalysis analysis = AnalysisTestsHelper.createTestAnalysisFromSettings(
+            Settings.builder()
+                .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString())
+                .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard")
+                .put("index.analysis.analyzer.my_analyzer.char_filter", "my_mappings_char_filter")
+                .put("index.analysis.char_filter.my_mappings_char_filter.type", "mapping")
+                .putList("index.analysis.char_filter.my_mappings_char_filter.mappings", rules)
+                .build(),
+            new CommonAnalysisModulePlugin()
+        );
+
+        return analysis.charFilter.get("my_mappings_char_filter");
+    }
+
+    public void testRulesOk() throws IOException {
+        MappingCharFilterFactory mappingCharFilterFactory = (MappingCharFilterFactory) create(
+            "# This is a comment",
+            ":) => _happy_",
+            ":( => _sad_"
+        );
+        CharFilter inputReader = (CharFilter) mappingCharFilterFactory.create(new StringReader("I'm so :)"));
+        char[] tempBuff = new char[14];
+        StringBuilder output = new StringBuilder();
+        while (true) {
+            int length = inputReader.read(tempBuff);
+            if (length == -1) break;
+            output.append(tempBuff, 0, length);
+        }
+        assertEquals("I'm so _happy_", output.toString());
+    }
+
+    public void testRuleError() {
+        for (String rule : Arrays.asList(
+            "",        // empty
+            "a",       // no arrow
+            "a:>b"     // invalid delimiter
+        )) {
+            RuntimeException ex = expectThrows(RuntimeException.class, () -> create(rule));
+            assertEquals("Line [1]: Invalid mapping rule : [" + rule + "]", ex.getMessage());
+        }
+    }
+
+    public void testRulePartError() {
+        RuntimeException ex = expectThrows(RuntimeException.class, () -> create("# This is a comment", ":) => _happy_", "a:b"));
+        assertEquals("Line [3]: Invalid mapping rule : [a:b]", ex.getMessage());
+    }
+}

programming_language: Java
filename: StemmerOverrideTokenFilterFactoryTests.java
change_type: ModificationType.MODIFY
diff: @@ -46,7 +46,6 @@ import org.junit.rules.ExpectedException;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.Arrays;
-import java.util.Locale;
 
 public class StemmerOverrideTokenFilterFactoryTests extends OpenSearchTokenStreamTestCase {
     @Rule
@@ -76,11 +75,8 @@ public class StemmerOverrideTokenFilterFactoryTests extends OpenSearchTokenStrea
             "=>a",     // no keys
             "a,=>b"    // empty key
         )) {
-            expectThrows(
-                RuntimeException.class,
-                String.format(Locale.ROOT, "Should fail for invalid rule: '%s'", rule),
-                () -> create(rule)
-            );
+            RuntimeException ex = expectThrows(RuntimeException.class, () -> create(rule));
+            assertEquals("Line [1]: Invalid keyword override rule: " + rule, ex.getMessage());
         }
     }
 
@@ -90,4 +86,9 @@ public class StemmerOverrideTokenFilterFactoryTests extends OpenSearchTokenStrea
         tokenizer.setReader(new StringReader("a b c"));
         assertTokenStreamContents(tokenFilterFactory.create(tokenizer), new String[] { "1", "2", "2" });
     }
+
+    public void testRulePartError() {
+        RuntimeException ex = expectThrows(RuntimeException.class, () -> create("a => 1", "b,c => 2", "# This is a comment", "=>a=>b"));
+        assertEquals("Line [4]: Invalid keyword override rule: =>a=>b", ex.getMessage());
+    }
 }

programming_language: Java
filename: SynonymsAnalysisTests.java
change_type: ModificationType.MODIFY
diff: @@ -116,7 +116,7 @@ public class SynonymsAnalysisTests extends OpenSearchTestCase {
             fail("fail! due to synonym word deleted by analyzer");
         } catch (Exception e) {
             assertThat(e, instanceOf(IllegalArgumentException.class));
-            assertThat(e.getMessage(), startsWith("failed to build synonyms"));
+            assertThat(e.getMessage(), startsWith("Failed to build synonyms"));
         }
     }
 
@@ -137,7 +137,7 @@ public class SynonymsAnalysisTests extends OpenSearchTestCase {
             fail("fail! due to synonym word deleted by analyzer");
         } catch (Exception e) {
             assertThat(e, instanceOf(IllegalArgumentException.class));
-            assertThat(e.getMessage(), startsWith("failed to build synonyms"));
+            assertThat(e.getMessage(), startsWith("Failed to build synonyms"));
         }
     }
 

programming_language: TypeScript
filename: IcuCollationTokenFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 
+import org.apache.logging.log4j.LogManager;
 import org.apache.lucene.analysis.TokenStream;
 import org.opensearch.common.io.Streams;
 import org.opensearch.common.settings.Settings;
@@ -80,9 +81,12 @@ public class IcuCollationTokenFilterFactory extends AbstractTokenFilterFactory {
                 collator = new RuleBasedCollator(rules);
             } catch (Exception e) {
                 if (failureToResolve != null) {
-                    throw new IllegalArgumentException("Failed to resolve collation rules location", failureToResolve);
+                    LogManager.getLogger(IcuCollationTokenFilterFactory.class)
+                        .error("Failed to resolve collation rules location", failureToResolve);
+                    throw new IllegalArgumentException("Failed to resolve collation rules location");
                 } else {
-                    throw new IllegalArgumentException("Failed to parse collation rules", e);
+                    LogManager.getLogger(IcuCollationTokenFilterFactory.class).error("Failed to parse collation rules", e);
+                    throw new IllegalArgumentException("Failed to parse collation rules");
                 }
             }
         } else {

programming_language: Java
filename: KuromojiPartOfSpeechFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,7 @@ public class KuromojiPartOfSpeechFilterFactory extends AbstractTokenFilterFactor
 
     public KuromojiPartOfSpeechFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) {
         super(indexSettings, name, settings);
-        List<String> wordList = Analysis.getWordList(env, settings, "stoptags");
+        List<String> wordList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
         if (wordList != null) {
             stopTags.addAll(wordList);
         } else {

programming_language: Java
filename: KuromojiTokenizerFactory.java
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,8 @@
 
 package org.opensearch.index.analysis;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.apache.lucene.analysis.Tokenizer;
 import org.apache.lucene.analysis.ja.JapaneseTokenizer;
 import org.apache.lucene.analysis.ja.JapaneseTokenizer.Mode;
@@ -50,6 +52,7 @@ import java.util.Set;
 
 public class KuromojiTokenizerFactory extends AbstractTokenizerFactory {
 
+    private static final Logger LOGGER = LogManager.getLogger(KuromojiTokenizerFactory.class);
     private static final String USER_DICT_PATH_OPTION = "user_dictionary";
     private static final String USER_DICT_RULES_OPTION = "user_dictionary_rules";
     private static final String NBEST_COST = "nbest_cost";
@@ -74,6 +77,14 @@ public class KuromojiTokenizerFactory extends AbstractTokenizerFactory {
         discardCompoundToken = settings.getAsBoolean(DISCARD_COMPOUND_TOKEN, false);
     }
 
+    private static String parse(String rule, Set<String> dup) {
+        String[] values = CSVUtil.parse(rule);
+        if (dup.add(values[0]) == false) {
+            throw new IllegalArgumentException("Found duplicate term [" + values[0] + "] in user dictionary.");
+        }
+        return rule;
+    }
+
     public static UserDictionary getUserDictionary(Environment env, Settings settings) {
         if (settings.get(USER_DICT_PATH_OPTION) != null && settings.get(USER_DICT_RULES_OPTION) != null) {
             throw new IllegalArgumentException(
@@ -81,31 +92,26 @@ public class KuromojiTokenizerFactory extends AbstractTokenizerFactory {
             );
         }
         try {
-            List<String> ruleList = Analysis.getWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, false);
+            Set<String> dup = new HashSet<>();
+            List<String> ruleList = Analysis.parseWordList(
+                env,
+                settings,
+                USER_DICT_PATH_OPTION,
+                USER_DICT_RULES_OPTION,
+                s -> parse(s, dup)
+            );
             if (ruleList == null || ruleList.isEmpty()) {
                 return null;
             }
-            Set<String> dup = new HashSet<>();
-            int lineNum = 0;
-            for (String line : ruleList) {
-                // ignore comments
-                if (line.startsWith("#") == false) {
-                    String[] values = CSVUtil.parse(line);
-                    if (dup.add(values[0]) == false) {
-                        throw new IllegalArgumentException(
-                            "Found duplicate term [" + values[0] + "] in user dictionary " + "at line [" + lineNum + "]"
-                        );
-                    }
-                }
-                ++lineNum;
-            }
+
             StringBuilder sb = new StringBuilder();
             for (String line : ruleList) {
                 sb.append(line).append(System.lineSeparator());
             }
             return UserDictionary.open(new StringReader(sb.toString()));
         } catch (IOException e) {
-            throw new OpenSearchException("failed to load kuromoji user dictionary", e);
+            LOGGER.error("Failed to load kuromoji user dictionary", e);
+            throw new OpenSearchException("Failed to load kuromoji user dictionary");
         }
     }
 

programming_language: Java
filename: KuromojiAnalysisTests.java
change_type: ModificationType.MODIFY
diff: @@ -390,8 +390,8 @@ public class KuromojiAnalysisTests extends OpenSearchTestCase {
                 "制限スピード,制限スピード,セイゲンスピード,テスト名詞"
             )
             .build();
-        IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> createTestAnalysis(settings));
-        assertThat(exc.getMessage(), containsString("[制限スピード] in user dictionary at line [3]"));
+        RuntimeException exc = expectThrows(RuntimeException.class, () -> createTestAnalysis(settings));
+        assertThat(exc.getMessage(), equalTo("Line [4]: Found duplicate term [制限スピード] in user dictionary."));
     }
 
     public void testDiscardCompoundToken() throws Exception {

programming_language: Java
filename: NoriAnalyzerProvider.java
change_type: ModificationType.MODIFY
diff: @@ -52,7 +52,7 @@ public class NoriAnalyzerProvider extends AbstractIndexAnalyzerProvider<KoreanAn
         super(indexSettings, name, settings);
         final KoreanTokenizer.DecompoundMode mode = NoriTokenizerFactory.getMode(settings);
         final UserDictionary userDictionary = NoriTokenizerFactory.getUserDictionary(env, settings);
-        final List<String> tagList = Analysis.getWordList(env, settings, "stoptags");
+        final List<String> tagList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
         final Set<POS.Tag> stopTags = tagList != null ? resolvePOSList(tagList) : KoreanPartOfSpeechStopFilter.DEFAULT_STOP_TAGS;
         analyzer = new KoreanAnalyzer(userDictionary, mode, stopTags, false);
     }

programming_language: Java
filename: NoriPartOfSpeechStopFilterFactory.java
change_type: ModificationType.MODIFY
diff: @@ -48,7 +48,7 @@ public class NoriPartOfSpeechStopFilterFactory extends AbstractTokenFilterFactor
 
     public NoriPartOfSpeechStopFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) {
         super(indexSettings, name, settings);
-        List<String> tagList = Analysis.getWordList(env, settings, "stoptags");
+        List<String> tagList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
         this.stopTags = tagList != null ? resolvePOSList(tagList) : KoreanPartOfSpeechStopFilter.DEFAULT_STOP_TAGS;
     }
 

programming_language: Java
filename: NoriTokenizerFactory.java
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,8 @@
 
 package org.opensearch.index.analysis;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.apache.lucene.analysis.Tokenizer;
 import org.apache.lucene.analysis.ko.KoreanTokenizer;
 import org.apache.lucene.analysis.ko.dict.UserDictionary;
@@ -47,6 +49,7 @@ import java.util.List;
 import java.util.Locale;
 
 public class NoriTokenizerFactory extends AbstractTokenizerFactory {
+    private static final Logger LOGGER = LogManager.getLogger(NoriTokenizerFactory.class);
     private static final String USER_DICT_PATH_OPTION = "user_dictionary";
     private static final String USER_DICT_RULES_OPTION = "user_dictionary_rules";
 
@@ -67,7 +70,7 @@ public class NoriTokenizerFactory extends AbstractTokenizerFactory {
                 "It is not allowed to use [" + USER_DICT_PATH_OPTION + "] in conjunction" + " with [" + USER_DICT_RULES_OPTION + "]"
             );
         }
-        List<String> ruleList = Analysis.getWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, true);
+        List<String> ruleList = Analysis.parseWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, s -> s);
         StringBuilder sb = new StringBuilder();
         if (ruleList == null || ruleList.isEmpty()) {
             return null;
@@ -78,7 +81,8 @@ public class NoriTokenizerFactory extends AbstractTokenizerFactory {
         try (Reader rulesReader = new StringReader(sb.toString())) {
             return UserDictionary.open(rulesReader);
         } catch (IOException e) {
-            throw new OpenSearchException("failed to load nori user dictionary", e);
+            LOGGER.error("Failed to load nori user dictionary", e);
+            throw new OpenSearchException("Failed to load nori user dictionary");
         }
     }
 

programming_language: Java
filename: Analysis.java
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,8 @@
 
 package org.opensearch.index.analysis;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.apache.lucene.analysis.CharArraySet;
 import org.apache.lucene.analysis.ar.ArabicAnalyzer;
 import org.apache.lucene.analysis.bg.BulgarianAnalyzer;
@@ -94,6 +96,7 @@ import static java.util.Collections.unmodifiableMap;
  * @opensearch.internal
  */
 public class Analysis {
+    private static final Logger LOGGER = LogManager.getLogger(Analysis.class);
 
     public static CharArraySet parseStemExclusion(Settings settings, CharArraySet defaultStemExclusion) {
         String value = settings.get("stem_exclusion");
@@ -166,7 +169,7 @@ public class Analysis {
                 return resolveNamedWords(settings.getAsList(name), namedWords, ignoreCase);
             }
         }
-        List<String> pathLoadedWords = getWordList(env, settings, name);
+        List<String> pathLoadedWords = parseWordList(env, settings, name, s -> s);
         if (pathLoadedWords != null) {
             return resolveNamedWords(pathLoadedWords, namedWords, ignoreCase);
         }
@@ -207,7 +210,7 @@ public class Analysis {
     }
 
     public static CharArraySet getWordSet(Environment env, Settings settings, String settingsPrefix) {
-        List<String> wordList = getWordList(env, settings, settingsPrefix);
+        List<String> wordList = parseWordList(env, settings, settingsPrefix, s -> s);
         if (wordList == null) {
             return null;
         }
@@ -215,15 +218,48 @@ public class Analysis {
         return new CharArraySet(wordList, ignoreCase);
     }
 
+    public static <T> List<T> parseWordList(Environment env, Settings settings, String settingPrefix, CustomMappingRuleParser<T> parser) {
+        return parseWordList(env, settings, settingPrefix + "_path", settingPrefix, parser);
+    }
+
     /**
-     * Fetches a list of words from the specified settings file. The list should either be available at the key
-     * specified by settingsPrefix or in a file specified by settingsPrefix + _path.
+     * Parses a list of words from the specified settings or from a file, with the given parser.
      *
      * @throws IllegalArgumentException
      *          If the word list cannot be found at either key.
+     * @throws RuntimeException
+     *          If there is error parsing the words
      */
-    public static List<String> getWordList(Environment env, Settings settings, String settingPrefix) {
-        return getWordList(env, settings, settingPrefix + "_path", settingPrefix, true);
+    public static <T> List<T> parseWordList(
+        Environment env,
+        Settings settings,
+        String settingPath,
+        String settingList,
+        CustomMappingRuleParser<T> parser
+    ) {
+        List<String> words = getWordList(env, settings, settingPath, settingList);
+        if (words == null) {
+            return null;
+        }
+        List<T> rules = new ArrayList<>();
+        int lineNum = 0;
+        for (String word : words) {
+            lineNum++;
+            if (word.startsWith("#") == false) {
+                try {
+                    rules.add(parser.apply(word));
+                } catch (RuntimeException ex) {
+                    String wordListPath = settings.get(settingPath, null);
+                    if (wordListPath == null || isUnderConfig(env, wordListPath)) {
+                        throw new RuntimeException("Line [" + lineNum + "]: " + ex.getMessage());
+                    } else {
+                        LOGGER.error("Line [{}]: {}", lineNum, ex);
+                        throw new RuntimeException("Line [" + lineNum + "]: " + "Invalid rule");
+                    }
+                }
+            }
+        }
+        return rules;
     }
 
     /**
@@ -233,43 +269,33 @@ public class Analysis {
      * @throws IllegalArgumentException
      *          If the word list cannot be found at either key.
      */
-    public static List<String> getWordList(
-        Environment env,
-        Settings settings,
-        String settingPath,
-        String settingList,
-        boolean removeComments
-    ) {
+    private static List<String> getWordList(Environment env, Settings settings, String settingPath, String settingList) {
         String wordListPath = settings.get(settingPath, null);
 
         if (wordListPath == null) {
-            List<String> explicitWordList = settings.getAsList(settingList, null);
-            if (explicitWordList == null) {
-                return null;
-            } else {
-                return explicitWordList;
-            }
+            return settings.getAsList(settingList, null);
         }
 
-        final Path path = env.configDir().resolve(wordListPath);
+        final Path path = resolveAnalyzerPath(env, wordListPath);
 
         try {
-            return loadWordList(path, removeComments);
+            return loadWordList(path);
         } catch (CharacterCodingException ex) {
             String message = String.format(
                 Locale.ROOT,
-                "Unsupported character encoding detected while reading %s: %s - files must be UTF-8 encoded",
-                settingPath,
-                path.toString()
+                "Unsupported character encoding detected while reading %s: files must be UTF-8 encoded",
+                settingPath
             );
-            throw new IllegalArgumentException(message, ex);
+            LOGGER.error("{}: from file: {}, exception is: {}", message, path.toString(), ex);
+            throw new IllegalArgumentException(message);
         } catch (IOException ioe) {
-            String message = String.format(Locale.ROOT, "IOException while reading %s: %s", settingPath, path.toString());
-            throw new IllegalArgumentException(message, ioe);
+            String message = String.format(Locale.ROOT, "IOException while reading %s: file not readable", settingPath);
+            LOGGER.error("{}, from file: {}, exception is: {}", message, path.toString(), ioe);
+            throw new IllegalArgumentException(message);
         }
     }
 
-    private static List<String> loadWordList(Path path, boolean removeComments) throws IOException {
+    private static List<String> loadWordList(Path path) throws IOException {
         final List<String> result = new ArrayList<>();
         try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
             String word;
@@ -277,9 +303,7 @@ public class Analysis {
                 if (Strings.hasText(word) == false) {
                     continue;
                 }
-                if (removeComments == false || word.startsWith("#") == false) {
-                    result.add(word.trim());
-                }
+                result.add(word.trim());
             }
         }
         return result;
@@ -296,21 +320,34 @@ public class Analysis {
         if (filePath == null) {
             return null;
         }
-        final Path path = env.configDir().resolve(filePath);
+        final Path path = resolveAnalyzerPath(env, filePath);
         try {
             return Files.newBufferedReader(path, StandardCharsets.UTF_8);
         } catch (CharacterCodingException ex) {
             String message = String.format(
                 Locale.ROOT,
-                "Unsupported character encoding detected while reading %s_path: %s files must be UTF-8 encoded",
-                settingPrefix,
-                path.toString()
+                "Unsupported character encoding detected while reading %s_path: files must be UTF-8 encoded",
+                settingPrefix
             );
-            throw new IllegalArgumentException(message, ex);
+            LOGGER.error("{}: from file: {}, exception is: {}", message, path.toString(), ex);
+            throw new IllegalArgumentException(message);
         } catch (IOException ioe) {
-            String message = String.format(Locale.ROOT, "IOException while reading %s_path: %s", settingPrefix, path.toString());
-            throw new IllegalArgumentException(message, ioe);
+            String message = String.format(Locale.ROOT, "IOException while reading %s_path: file not readable", settingPrefix);
+            LOGGER.error("{}, from file: {}, exception is: {}", message, path.toString(), ioe);
+            throw new IllegalArgumentException(message);
         }
     }
 
+    public static Path resolveAnalyzerPath(Environment env, String wordListPath) {
+        return env.configDir().resolve(wordListPath).normalize();
+    }
+
+    private static boolean isUnderConfig(Environment env, String wordListPath) {
+        try {
+            final Path path = env.configDir().resolve(wordListPath).normalize();
+            return path.startsWith(env.configDir().toAbsolutePath());
+        } catch (Exception ex) {
+            return false;
+        }
+    }
 }

programming_language: Java
filename: CustomMappingRuleParser.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,21 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.index.analysis;
+
+import java.util.function.Function;
+
+/**
+ * A parser that takes a raw string and returns the parsed data of type T.
+ *
+ * @param <T> type of parsed data
+ */
+@FunctionalInterface
+public interface CustomMappingRuleParser<T> extends Function<String, T> {
+
+}

programming_language: Java
filename: MappingRule.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,30 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.index.analysis;
+
+/**
+ * Represents a mapping between two objects.
+ */
+public class MappingRule<L, R> {
+    private final L left;
+    private final R right;
+
+    public MappingRule(L left, R right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    public L getLeft() {
+        return left;
+    }
+
+    public R getRight() {
+        return right;
+    }
+}

programming_language: Java
filename: HunspellService.java
change_type: ModificationType.MODIFY
diff: @@ -123,7 +123,8 @@ public class HunspellService {
             try {
                 return loadDictionary(locale, settings, env);
             } catch (Exception e) {
-                throw new IllegalStateException("failed to load hunspell dictionary for locale: " + locale, e);
+                logger.error("Failed to load hunspell dictionary for locale: " + locale, e);
+                throw new IllegalStateException("Failed to load hunspell dictionary for locale: " + locale);
             }
         };
         if (!HUNSPELL_LAZY_LOAD.get(settings)) {

programming_language: Java
filename: AnalysisTests.java
change_type: ModificationType.MODIFY
diff: @@ -39,14 +39,10 @@ import org.opensearch.env.TestEnvironment;
 import org.opensearch.test.OpenSearchTestCase;
 
 import java.io.BufferedWriter;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.MalformedInputException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.List;
@@ -79,13 +75,10 @@ public class AnalysisTests extends OpenSearchTestCase {
         Environment env = TestEnvironment.newEnvironment(nodeSettings);
         IllegalArgumentException ex = expectThrows(
             IllegalArgumentException.class,
-            () -> Analysis.getWordList(env, nodeSettings, "foo.bar")
-        );
-        assertEquals("IOException while reading foo.bar_path: " + tempDir.resolve("foo.dict").toString(), ex.getMessage());
-        assertTrue(
-            ex.getCause().toString(),
-            ex.getCause() instanceof FileNotFoundException || ex.getCause() instanceof NoSuchFileException
+            () -> Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> s)
         );
+        assertEquals("IOException while reading foo.bar_path: file not readable", ex.getMessage());
+        assertNull(ex.getCause());
     }
 
     public void testParseFalseEncodedFile() throws IOException {
@@ -99,18 +92,10 @@ public class AnalysisTests extends OpenSearchTestCase {
         Environment env = TestEnvironment.newEnvironment(nodeSettings);
         IllegalArgumentException ex = expectThrows(
             IllegalArgumentException.class,
-            () -> Analysis.getWordList(env, nodeSettings, "foo.bar")
-        );
-        assertEquals(
-            "Unsupported character encoding detected while reading foo.bar_path: "
-                + tempDir.resolve("foo.dict").toString()
-                + " - files must be UTF-8 encoded",
-            ex.getMessage()
-        );
-        assertTrue(
-            ex.getCause().toString(),
-            ex.getCause() instanceof MalformedInputException || ex.getCause() instanceof CharacterCodingException
+            () -> Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> s)
         );
+        assertEquals("Unsupported character encoding detected while reading foo.bar_path: files must be UTF-8 encoded", ex.getMessage());
+        assertNull(ex.getCause());
     }
 
     public void testParseWordList() throws IOException {
@@ -124,8 +109,42 @@ public class AnalysisTests extends OpenSearchTestCase {
             writer.write('\n');
         }
         Environment env = TestEnvironment.newEnvironment(nodeSettings);
-        List<String> wordList = Analysis.getWordList(env, nodeSettings, "foo.bar");
+        List<String> wordList = Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> s);
         assertEquals(Arrays.asList("hello", "world"), wordList);
+    }
 
+    public void testParseWordListError() throws IOException {
+        Path home = createTempDir();
+        Path config = home.resolve("config");
+        Files.createDirectory(config);
+        Path dict = config.resolve("foo.dict");
+        Settings nodeSettings = Settings.builder().put("foo.bar_path", dict).put(Environment.PATH_HOME_SETTING.getKey(), home).build();
+        try (BufferedWriter writer = Files.newBufferedWriter(dict, StandardCharsets.UTF_8)) {
+            writer.write("abcd");
+            writer.write('\n');
+        }
+        Environment env = TestEnvironment.newEnvironment(nodeSettings);
+        RuntimeException ex = expectThrows(
+            RuntimeException.class,
+            () -> Analysis.parseWordList(
+                env,
+                nodeSettings,
+                "foo.bar",
+                s -> { throw new RuntimeException("Error while parsing rule = " + s); }
+            )
+        );
+        assertEquals("Line [1]: Error while parsing rule = abcd", ex.getMessage());
+    }
+
+    public void testParseWordListOutsideConfigDirError() {
+        Path home = createTempDir();
+        Path dict = home.resolve("/etc/os-release");
+        Settings nodeSettings = Settings.builder().put("foo.bar_path", dict).put(Environment.PATH_HOME_SETTING.getKey(), home).build();
+        Environment env = TestEnvironment.newEnvironment(nodeSettings);
+        RuntimeException ex = expectThrows(
+            RuntimeException.class,
+            () -> Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> { throw new RuntimeException("Error while parsing"); })
+        );
+        assertEquals("Line [1]: Invalid rule", ex.getMessage());
     }
 }

programming_language: Java
filename: AnalysisModuleTests.java
change_type: ModificationType.MODIFY
diff: @@ -167,11 +167,12 @@ public class AnalysisModuleTests extends OpenSearchTestCase {
     }
 
     public void testWordListPath() throws Exception {
-        Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build();
+        Path home = createTempDir();
+        Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home.toString()).build();
         Environment env = TestEnvironment.newEnvironment(settings);
         String[] words = new String[] { "donau", "dampf", "schiff", "spargel", "creme", "suppe" };
 
-        Path wordListFile = generateWordList(words);
+        Path wordListFile = generateWordList(home, words);
         settings = Settings.builder()
             .loadFromSource("index: \n  word_list_path: " + wordListFile.toAbsolutePath(), XContentType.YAML)
             .build();
@@ -182,8 +183,10 @@ public class AnalysisModuleTests extends OpenSearchTestCase {
         Files.delete(wordListFile);
     }
 
-    private Path generateWordList(String[] words) throws Exception {
-        Path wordListFile = createTempDir().resolve("wordlist.txt");
+    private Path generateWordList(Path home, String[] words) throws Exception {
+        Path config = home.resolve("config");
+        Files.createDirectory(config);
+        Path wordListFile = config.resolve("wordlist.txt");
         try (BufferedWriter writer = Files.newBufferedWriter(wordListFile, StandardCharsets.UTF_8)) {
             for (String word : words) {
                 writer.write(word);

programming_language: Java
filename: HunspellServiceTests.java
change_type: ModificationType.MODIFY
diff: @@ -42,8 +42,6 @@ import java.nio.file.Path;
 import static java.util.Collections.emptyMap;
 import static org.opensearch.indices.analysis.HunspellService.HUNSPELL_IGNORE_CASE;
 import static org.opensearch.indices.analysis.HunspellService.HUNSPELL_LAZY_LOAD;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.hasToString;
 import static org.hamcrest.Matchers.notNullValue;
 
 public class HunspellServiceTests extends OpenSearchTestCase {
@@ -91,11 +89,11 @@ public class HunspellServiceTests extends OpenSearchTestCase {
             final Environment environment = new Environment(settings, getDataPath("/indices/analyze/no_aff_conf_dir"));
             new HunspellService(settings, environment, emptyMap()).getDictionary("en_US");
         });
-        assertEquals("failed to load hunspell dictionary for locale: en_US", e.getMessage());
-        assertThat(e.getCause(), hasToString(containsString("Missing affix file")));
+        assertEquals("Failed to load hunspell dictionary for locale: en_US", e.getMessage());
+        assertNull(e.getCause());
     }
 
-    public void testDicWithTwoAffs() throws Exception {
+    public void testDicWithTwoAffs() {
         Settings settings = Settings.builder()
             .put(HUNSPELL_LAZY_LOAD.getKey(), randomBoolean())
             .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir())
@@ -105,7 +103,7 @@ public class HunspellServiceTests extends OpenSearchTestCase {
             final Environment environment = new Environment(settings, getDataPath("/indices/analyze/two_aff_conf_dir"));
             new HunspellService(settings, environment, emptyMap()).getDictionary("en_US");
         });
-        assertEquals("failed to load hunspell dictionary for locale: en_US", e.getMessage());
-        assertThat(e.getCause(), hasToString(containsString("Too many affix files")));
+        assertEquals("Failed to load hunspell dictionary for locale: en_US", e.getMessage());
+        assertNull(e.getCause());
     }
 }

programming_language: Java