CVE-2022-41917: Improper Handling of Exceptional Conditions (CWE-755)
value: 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.
CWE description: The product does not handle or incorrectly handles an exceptional condition.
CVSS v3 base score: 4.3
Published: 2022-11-16T00:15Z
Repository: https://github.com/opensearch-project/OpenSearch
Language: Java
Files changed: CHANGELOG.md, modules/analysis-common/src/main/java/org/opensearch/analysis/common/HyphenationCompoundWordTokenFilterFactory.java, modules/analysis-common/src/main/java/org/opensearch/analysis/common/MappingCharFilterFactory.java, modules/analysis-common/src/main/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactory.java, modules/analysis-common/src/main/java/org/opensearch/analysis/common/SynonymTokenFilterFactory.java, modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java, modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterTokenFilterFactory.java, modules/analysis-common/src/test/java/org/opensearch/analysis/common/BaseWordDelimiterTokenFilterFactoryTestCase.java, modules/analysis-common/src/test/java/org/opensearch/analysis/common/MappingCharFilterFactoryTests.java, modules/analysis-common/src/test/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactoryTests.java, modules/analysis-common/src/test/java/org/opensearch/analysis/common/SynonymsAnalysisTests.java, plugins/analysis-icu/src/main/java/org/opensearch/index/analysis/IcuCollationTokenFilterFactory.java, plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiPartOfSpeechFilterFactory.java, plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiTokenizerFactory.java, plugins/analysis-kuromoji/src/test/java/org/opensearch/index/analysis/KuromojiAnalysisTests.java, plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriAnalyzerProvider.java, plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriPartOfSpeechStopFilterFactory.java, plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriTokenizerFactory.java, server/src/main/java/org/opensearch/index/analysis/Analysis.java, server/src/main/java/org/opensearch/index/analysis/CustomMappingRuleParser.java, server/src/main/java/org/opensearch/index/analysis/MappingRule.java, server/src/main/java/org/opensearch/indices/analysis/HunspellService.java, server/src/test/java/org/opensearch/index/analysis/AnalysisTests.java, server/src/test/java/org/opensearch/indices/analysis/AnalysisModuleTests.java, server/src/test/java/org/opensearch/indices/analyze/HunspellServiceTests.java
Commit date: 2022-11-07 16:52:52 -0800
Commit message: 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>
Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2870ccb28e5..dd5be81d422 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -210,10 +210,11 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Remove gradle-check dependency on precommit [#5027](https://github.com/opensearch-project/OpenSearch/pull/5027)
 - Fix version check for 2.x release for awareness attribute decommission([#5034](https://github.com/opensearch-project/OpenSearch/pull/5034))
 - 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))
 
 ## [2.x]
 ### Added
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/HyphenationCompoundWordTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/HyphenationCompoundWordTokenFilterFactory.java
index b46bef3e6c5..25bf5840992 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/HyphenationCompoundWordTokenFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/HyphenationCompoundWordTokenFilterFactory.java
@@ -30,16 +30,18 @@
  * GitHub history for details.
  */
 
 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;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -59,17 +61,19 @@ public class HyphenationCompoundWordTokenFilterFactory extends AbstractCompoundW
         String hyphenationPatternsPath = settings.get("hyphenation_patterns_path", null);
         if (hyphenationPatternsPath == null) {
             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.");
         }
     }
 
     @Override
     public TokenStream create(TokenStream tokenStream) {
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/MappingCharFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/MappingCharFilterFactory.java
index 7200b69135a..d6d9f8975f2 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/MappingCharFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/MappingCharFilterFactory.java
@@ -37,10 +37,11 @@ import org.apache.lucene.analysis.charfilter.NormalizeCharMap;
 import org.opensearch.common.settings.Settings;
 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;
 import java.util.List;
 import java.util.regex.Matcher;
@@ -51,17 +52,17 @@ public class MappingCharFilterFactory extends AbstractCharFilterFactory implemen
     private final NormalizeCharMap normMap;
 
     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();
     }
 
     @Override
     public Reader create(Reader tokenStream) {
@@ -69,22 +70,17 @@ 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];
 
     private String parseString(String s) {
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactory.java
index 89f07665422..bdd6e012614 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactory.java
@@ -38,56 +38,62 @@ import org.apache.lucene.analysis.miscellaneous.StemmerOverrideFilter.StemmerOve
 import org.opensearch.common.settings.Settings;
 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();
 
     }
 
     @Override
     public TokenStream create(TokenStream tokenStream) {
         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);
     }
-
 }
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/SynonymTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/SynonymTokenFilterFactory.java
index dc6b5b2dd8b..01a65e87d74 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/SynonymTokenFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/SynonymTokenFilterFactory.java
@@ -30,10 +30,11 @@
  * GitHub history for details.
  */
 
 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;
 import org.apache.lucene.analysis.synonym.SynonymMap;
 import org.opensearch.common.logging.DeprecationLogger;
@@ -153,18 +154,19 @@ public class SynonymTokenFilterFactory extends AbstractTokenFilterFactory {
                 parser = new OpenSearchSolrSynonymParser(true, expand, lenient, analyzer);
                 ((OpenSearchSolrSynonymParser) parser).parse(rules);
             }
             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());
             }
             rulesReader = new StringReader(sb.toString());
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java
index 31d52d030cb..51ac3141fd4 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java
@@ -39,10 +39,11 @@ import org.apache.lucene.analysis.miscellaneous.WordDelimiterIterator;
 import org.opensearch.common.settings.Settings;
 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;
 import java.util.Set;
 
@@ -71,11 +72,16 @@ public class WordDelimiterGraphTokenFilterFactory extends AbstractTokenFilterFac
         // $ => DIGIT
         // % => DIGIT
         // . => 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 {
             this.charTypeTable = parseTypes(charTypeTableValues);
         }
diff --git a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterTokenFilterFactory.java
index d40acfa05dd..96e50206fb5 100644
--- a/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterTokenFilterFactory.java
+++ b/modules/analysis-common/src/main/java/org/opensearch/analysis/common/WordDelimiterTokenFilterFactory.java
@@ -39,10 +39,11 @@ import org.apache.lucene.analysis.miscellaneous.WordDelimiterIterator;
 import org.opensearch.common.settings.Settings;
 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;
 import java.util.List;
 import java.util.Map;
@@ -74,11 +75,16 @@ public class WordDelimiterTokenFilterFactory extends AbstractTokenFilterFactory
         // $ => DIGIT
         // % => DIGIT
         // . => 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 {
             this.charTypeTable = parseTypes(charTypeTableValues);
         }
@@ -125,23 +131,27 @@ 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
         byte types[] = new byte[Math.max(typeMap.lastKey() + 1, WordDelimiterIterator.DEFAULT_WORD_DELIM_TABLE.length)];
         for (int i = 0; i < types.length; i++)
diff --git a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/BaseWordDelimiterTokenFilterFactoryTestCase.java b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/BaseWordDelimiterTokenFilterFactoryTestCase.java
index 829ace512b5..94c7d63f2be 100644
--- a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/BaseWordDelimiterTokenFilterFactoryTestCase.java
+++ b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/BaseWordDelimiterTokenFilterFactoryTestCase.java
@@ -193,6 +193,26 @@ public abstract class BaseWordDelimiterTokenFilterFactoryTestCase extends OpenSe
         String[] expected = new String[] { "Power", "Shot", "500", "42", "wi", "fi", "wi", "fi", "4000", "j", "2", "se", "O", "Neil", "s" };
         Tokenizer tokenizer = new WhitespaceTokenizer();
         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());
+    }
 }
diff --git a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/MappingCharFilterFactoryTests.java b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/MappingCharFilterFactoryTests.java
new file mode 100644
index 00000000000..7d059ff9ce1
--- /dev/null
+++ b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/MappingCharFilterFactoryTests.java
@@ -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());
+    }
+}
diff --git a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactoryTests.java b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactoryTests.java
index e923e11ffc3..0fcb2fb8d51 100644
--- a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactoryTests.java
+++ b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/StemmerOverrideTokenFilterFactoryTests.java
@@ -44,11 +44,10 @@ import org.junit.Rule;
 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
     public ExpectedException expectedException = ExpectedException.none();
 
@@ -74,20 +73,22 @@ public class StemmerOverrideTokenFilterFactoryTests extends OpenSearchTokenStrea
             "a=>",     // no override
             "a=>b,c",  // multiple overrides
             "=>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());
         }
     }
 
     public void testRulesOk() throws IOException {
         TokenFilterFactory tokenFilterFactory = create("a => 1", "b,c => 2");
         Tokenizer tokenizer = new WhitespaceTokenizer();
         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());
+    }
 }
diff --git a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/SynonymsAnalysisTests.java b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/SynonymsAnalysisTests.java
index a9d470e3fd5..2618c106d92 100644
--- a/modules/analysis-common/src/test/java/org/opensearch/analysis/common/SynonymsAnalysisTests.java
+++ b/modules/analysis-common/src/test/java/org/opensearch/analysis/common/SynonymsAnalysisTests.java
@@ -114,11 +114,11 @@ public class SynonymsAnalysisTests extends OpenSearchTestCase {
         try {
             indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisModulePlugin()).indexAnalyzers;
             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"));
         }
     }
 
     public void testExpandSynonymWordDeleteByAnalyzer() throws IOException {
         Settings settings = Settings.builder()
@@ -135,11 +135,11 @@ public class SynonymsAnalysisTests extends OpenSearchTestCase {
         try {
             indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisModulePlugin()).indexAnalyzers;
             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"));
         }
     }
 
     public void testSynonymsWrappedByMultiplexer() throws IOException {
         Settings settings = Settings.builder()
diff --git a/plugins/analysis-icu/src/main/java/org/opensearch/index/analysis/IcuCollationTokenFilterFactory.java b/plugins/analysis-icu/src/main/java/org/opensearch/index/analysis/IcuCollationTokenFilterFactory.java
index cd2898c9d64..ca29492addc 100644
--- a/plugins/analysis-icu/src/main/java/org/opensearch/index/analysis/IcuCollationTokenFilterFactory.java
+++ b/plugins/analysis-icu/src/main/java/org/opensearch/index/analysis/IcuCollationTokenFilterFactory.java
@@ -35,10 +35,11 @@ package org.opensearch.index.analysis;
 import java.io.IOException;
 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;
 import org.opensearch.env.Environment;
 import org.opensearch.index.IndexSettings;
@@ -78,13 +79,16 @@ public class IcuCollationTokenFilterFactory extends AbstractTokenFilterFactory {
             }
             try {
                 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 {
             String language = settings.get("language");
             if (language != null) {
diff --git a/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiPartOfSpeechFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiPartOfSpeechFilterFactory.java
index fef8d06c466..8e9c209ae42 100644
--- a/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiPartOfSpeechFilterFactory.java
+++ b/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiPartOfSpeechFilterFactory.java
@@ -47,11 +47,11 @@ public class KuromojiPartOfSpeechFilterFactory extends AbstractTokenFilterFactor
 
     private final Set<String> stopTags = new HashSet<>();
 
     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 {
             stopTags.addAll(JapaneseAnalyzer.getDefaultStopTags());
         }
diff --git a/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiTokenizerFactory.java b/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiTokenizerFactory.java
index b5e718eaa6f..ac4def8ac9a 100644
--- a/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiTokenizerFactory.java
+++ b/plugins/analysis-kuromoji/src/main/java/org/opensearch/index/analysis/KuromojiTokenizerFactory.java
@@ -30,10 +30,12 @@
  * GitHub history for details.
  */
 
 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;
 import org.apache.lucene.analysis.ja.dict.UserDictionary;
 import org.apache.lucene.analysis.ja.util.CSVUtil;
@@ -48,10 +50,11 @@ import java.util.HashSet;
 import java.util.List;
 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";
     private static final String NBEST_EXAMPLES = "nbest_examples";
     private static final String DISCARD_COMPOUND_TOKEN = "discard_compound_token";
@@ -72,42 +75,45 @@ public class KuromojiTokenizerFactory extends AbstractTokenizerFactory {
         nBestCost = settings.getAsInt(NBEST_COST, -1);
         nBestExamples = settings.get(NBEST_EXAMPLES);
         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(
                 "It is not allowed to use [" + USER_DICT_PATH_OPTION + "] in conjunction" + " with [" + USER_DICT_RULES_OPTION + "]"
             );
         }
         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");
         }
     }
 
     public static JapaneseTokenizer.Mode getMode(Settings settings) {
         JapaneseTokenizer.Mode mode = JapaneseTokenizer.DEFAULT_MODE;
diff --git a/plugins/analysis-kuromoji/src/test/java/org/opensearch/index/analysis/KuromojiAnalysisTests.java b/plugins/analysis-kuromoji/src/test/java/org/opensearch/index/analysis/KuromojiAnalysisTests.java
index e17658d83a0..23e6ef9fea0 100644
--- a/plugins/analysis-kuromoji/src/test/java/org/opensearch/index/analysis/KuromojiAnalysisTests.java
+++ b/plugins/analysis-kuromoji/src/test/java/org/opensearch/index/analysis/KuromojiAnalysisTests.java
@@ -388,12 +388,12 @@ public class KuromojiAnalysisTests extends OpenSearchTestCase {
                 "#comment",
                 "制限スピード,制限スピード,セイゲンスピード,テスト名詞",
                 "制限スピード,制限スピード,セイゲンスピード,テスト名詞"
             )
             .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 {
         TestAnalysis analysis = createTestAnalysis();
         TokenizerFactory tokenizerFactory = analysis.tokenizer.get("kuromoji_discard_compound_token");
diff --git a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriAnalyzerProvider.java b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriAnalyzerProvider.java
index 3dee6061854..e3b1cef6aee 100644
--- a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriAnalyzerProvider.java
+++ b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriAnalyzerProvider.java
@@ -50,11 +50,11 @@ public class NoriAnalyzerProvider extends AbstractIndexAnalyzerProvider<KoreanAn
 
     public NoriAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings) {
         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);
     }
 
     @Override
diff --git a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriPartOfSpeechStopFilterFactory.java b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriPartOfSpeechStopFilterFactory.java
index 18cbc3c7c15..5023db50422 100644
--- a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriPartOfSpeechStopFilterFactory.java
+++ b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriPartOfSpeechStopFilterFactory.java
@@ -46,11 +46,11 @@ import java.util.Set;
 public class NoriPartOfSpeechStopFilterFactory extends AbstractTokenFilterFactory {
     private final Set<POS.Tag> stopTags;
 
     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;
     }
 
     @Override
     public TokenStream create(TokenStream tokenStream) {
diff --git a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriTokenizerFactory.java b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriTokenizerFactory.java
index 5136277611e..9f3183194cd 100644
--- a/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriTokenizerFactory.java
+++ b/plugins/analysis-nori/src/main/java/org/opensearch/index/analysis/NoriTokenizerFactory.java
@@ -30,10 +30,12 @@
  * GitHub history for details.
  */
 
 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;
 import org.opensearch.OpenSearchException;
 import org.opensearch.common.settings.Settings;
@@ -45,10 +47,11 @@ import java.io.Reader;
 import java.io.StringReader;
 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";
 
     private final UserDictionary userDictionary;
     private final KoreanTokenizer.DecompoundMode decompoundMode;
@@ -65,22 +68,23 @@ public class NoriTokenizerFactory extends AbstractTokenizerFactory {
         if (settings.get(USER_DICT_PATH_OPTION) != null && settings.get(USER_DICT_RULES_OPTION) != null) {
             throw new IllegalArgumentException(
                 "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;
         }
         for (String line : ruleList) {
             sb.append(line).append(System.lineSeparator());
         }
         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");
         }
     }
 
     public static KoreanTokenizer.DecompoundMode getMode(Settings settings) {
         KoreanTokenizer.DecompoundMode mode = KoreanTokenizer.DEFAULT_DECOMPOUND;
diff --git a/server/src/main/java/org/opensearch/index/analysis/Analysis.java b/server/src/main/java/org/opensearch/index/analysis/Analysis.java
index 325c3b4f10d..e9a5896a0c4 100644
--- a/server/src/main/java/org/opensearch/index/analysis/Analysis.java
+++ b/server/src/main/java/org/opensearch/index/analysis/Analysis.java
@@ -30,10 +30,12 @@
  * GitHub history for details.
  */
 
 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;
 import org.apache.lucene.analysis.bn.BengaliAnalyzer;
 import org.apache.lucene.analysis.br.BrazilianAnalyzer;
@@ -92,10 +94,11 @@ import static java.util.Collections.unmodifiableMap;
  * Core analysis class
  *
  * @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");
         if ("_none_".equals(value)) {
             return CharArraySet.EMPTY_SET;
@@ -164,11 +167,11 @@ public class Analysis {
                 return CharArraySet.EMPTY_SET;
             } else {
                 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);
         }
         return defaultWords;
     }
@@ -205,83 +208,104 @@ public class Analysis {
         }
         return setWords;
     }
 
     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;
         }
         boolean ignoreCase = settings.getAsBoolean(settingsPrefix + "_case", false);
         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;
     }
 
     /**
      * Fetches a list of words from the specified settings file. The list should either be available at the key
      * specified by <code>settingList</code> or in a file specified by <code>settingPath</code>.
      *
      * @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;
             while ((word = br.readLine()) != null) {
                 if (Strings.hasText(word) == false) {
                     continue;
                 }
-                if (removeComments == false || word.startsWith("#") == false) {
-                    result.add(word.trim());
-                }
+                result.add(word.trim());
             }
         }
         return result;
     }
 
@@ -294,23 +318,36 @@ public class Analysis {
         String filePath = settings.get(settingPrefix, null);
 
         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;
+        }
+    }
 }
diff --git a/server/src/main/java/org/opensearch/index/analysis/CustomMappingRuleParser.java b/server/src/main/java/org/opensearch/index/analysis/CustomMappingRuleParser.java
new file mode 100644
index 00000000000..075c6ca1b58
--- /dev/null
+++ b/server/src/main/java/org/opensearch/index/analysis/CustomMappingRuleParser.java
@@ -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> {
+
+}
diff --git a/server/src/main/java/org/opensearch/index/analysis/MappingRule.java b/server/src/main/java/org/opensearch/index/analysis/MappingRule.java
new file mode 100644
index 00000000000..92c9d2a17dc
--- /dev/null
+++ b/server/src/main/java/org/opensearch/index/analysis/MappingRule.java
@@ -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;
+    }
+}
diff --git a/server/src/main/java/org/opensearch/indices/analysis/HunspellService.java b/server/src/main/java/org/opensearch/indices/analysis/HunspellService.java
index b4ab80edec4..1832de5a6f6 100644
--- a/server/src/main/java/org/opensearch/indices/analysis/HunspellService.java
+++ b/server/src/main/java/org/opensearch/indices/analysis/HunspellService.java
@@ -121,11 +121,12 @@ public class HunspellService {
         this.defaultIgnoreCase = HUNSPELL_IGNORE_CASE.get(settings);
         this.loadingFunction = (locale) -> {
             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)) {
             scanAndLoadDictionaries();
         }
diff --git a/server/src/test/java/org/opensearch/index/analysis/AnalysisTests.java b/server/src/test/java/org/opensearch/index/analysis/AnalysisTests.java
index d6f0cb194f2..01281ea323e 100644
--- a/server/src/test/java/org/opensearch/index/analysis/AnalysisTests.java
+++ b/server/src/test/java/org/opensearch/index/analysis/AnalysisTests.java
@@ -37,18 +37,14 @@ import org.opensearch.common.settings.Settings;
 import org.opensearch.env.Environment;
 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;
 
 import static org.hamcrest.Matchers.is;
@@ -77,17 +73,14 @@ public class AnalysisTests extends OpenSearchTestCase {
             .put(Environment.PATH_HOME_SETTING.getKey(), tempDir)
             .build();
         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 {
         Path tempDir = createTempDir();
         Path dict = tempDir.resolve("foo.dict");
@@ -97,22 +90,14 @@ public class AnalysisTests extends OpenSearchTestCase {
             writer.write('\n');
         }
         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 {
         Path tempDir = createTempDir();
         Path dict = tempDir.resolve("foo.dict");
@@ -122,10 +107,44 @@ public class AnalysisTests extends OpenSearchTestCase {
             writer.write('\n');
             writer.write("world");
             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());
     }
 }
diff --git a/server/src/test/java/org/opensearch/indices/analysis/AnalysisModuleTests.java b/server/src/test/java/org/opensearch/indices/analysis/AnalysisModuleTests.java
index 483e9401bb0..38398a20449 100644
--- a/server/src/test/java/org/opensearch/indices/analysis/AnalysisModuleTests.java
+++ b/server/src/test/java/org/opensearch/indices/analysis/AnalysisModuleTests.java
@@ -165,27 +165,30 @@ public class AnalysisModuleTests extends OpenSearchTestCase {
         CustomAnalyzer custom4 = (CustomAnalyzer) analyzer;
         assertThat(custom4.tokenFilters()[0], instanceOf(MyFilterTokenFilterFactory.class));
     }
 
     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();
 
         Set<?> wordList = Analysis.getWordSet(env, settings, "index.word_list");
         MatcherAssert.assertThat(wordList.size(), equalTo(6));
         // MatcherAssert.assertThat(wordList, hasItems(words));
         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);
                 writer.write('\n');
             }
diff --git a/server/src/test/java/org/opensearch/indices/analyze/HunspellServiceTests.java b/server/src/test/java/org/opensearch/indices/analyze/HunspellServiceTests.java
index 28732b099ea..f66045898f4 100644
--- a/server/src/test/java/org/opensearch/indices/analyze/HunspellServiceTests.java
+++ b/server/src/test/java/org/opensearch/indices/analyze/HunspellServiceTests.java
@@ -40,12 +40,10 @@ import org.opensearch.test.OpenSearchTestCase;
 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 {
     public void testLocaleDirectoryWithNodeLevelConfig() throws Exception {
         Settings settings = Settings.builder()
@@ -89,23 +87,23 @@ public class HunspellServiceTests extends OpenSearchTestCase {
 
         IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
             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())
             .build();
 
         IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
             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());
     }
 }

Vulnerable code:         Path hyphenationPatternsFile = env.configDir().resolve(hyphenationPatternsPath);
            throw new IllegalArgumentException("Exception while reading hyphenation_patterns_path.", e);
        List<String> rules = Analysis.getWordList(env, settings, "mappings");
        parseRules(rules, normMapBuilder);
    /**
     * 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);
        }
        List<String> rules = Analysis.getWordList(env, settings, "rules");
        parseRules(rules, builder, "=>");
    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);
            }
            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);

            throw new IllegalArgumentException("failed to build synonyms", e);
            List<String> rulesList = Analysis.getWordList(env, settings, "synonyms");
        List<String> charTypeTableValues = Analysis.getWordList(env, settings, "type_table");
        List<String> charTypeTableValues = Analysis.getWordList(env, settings, "type_table");
    static byte[] parseTypes(Collection<String> rules) {
        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);
import java.util.Locale;
            expectThrows(
                RuntimeException.class,
                String.format(Locale.ROOT, "Should fail for invalid rule: '%s'", rule),
                () -> create(rule)
            );
            assertThat(e.getMessage(), startsWith("failed to build synonyms"));
            assertThat(e.getMessage(), startsWith("failed to build synonyms"));
                    throw new IllegalArgumentException("Failed to resolve collation rules location", failureToResolve);
                    throw new IllegalArgumentException("Failed to parse collation rules", e);
        List<String> wordList = Analysis.getWordList(env, settings, "stoptags");
            List<String> ruleList = Analysis.getWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, false);
            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;
            }
            throw new OpenSearchException("failed to load kuromoji user dictionary", e);
        IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> createTestAnalysis(settings));
        assertThat(exc.getMessage(), containsString("[制限スピード] in user dictionary at line [3]"));
        final List<String> tagList = Analysis.getWordList(env, settings, "stoptags");
        List<String> tagList = Analysis.getWordList(env, settings, "stoptags");
        List<String> ruleList = Analysis.getWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, true);
            throw new OpenSearchException("failed to load nori user dictionary", e);
        List<String> pathLoadedWords = getWordList(env, settings, name);
        List<String> wordList = getWordList(env, settings, settingsPrefix);
     * 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.
    public static List<String> getWordList(Environment env, Settings settings, String settingPrefix) {
        return getWordList(env, settings, settingPrefix + "_path", settingPrefix, true);
    public static List<String> getWordList(
        Environment env,
        Settings settings,
        String settingPath,
        String settingList,
        boolean removeComments
    ) {
            List<String> explicitWordList = settings.getAsList(settingList, null);
            if (explicitWordList == null) {
                return null;
            } else {
                return explicitWordList;
            }
        final Path path = env.configDir().resolve(wordListPath);
            return loadWordList(path, removeComments);
                "Unsupported character encoding detected while reading %s: %s - files must be UTF-8 encoded",
                settingPath,
                path.toString()
            throw new IllegalArgumentException(message, ex);
            String message = String.format(Locale.ROOT, "IOException while reading %s: %s", settingPath, path.toString());
            throw new IllegalArgumentException(message, ioe);
    private static List<String> loadWordList(Path path, boolean removeComments) throws IOException {
                if (removeComments == false || word.startsWith("#") == false) {
                    result.add(word.trim());
                }
        final Path path = env.configDir().resolve(filePath);
                "Unsupported character encoding detected while reading %s_path: %s files must be UTF-8 encoded",
                settingPrefix,
                path.toString()
            throw new IllegalArgumentException(message, ex);
            String message = String.format(Locale.ROOT, "IOException while reading %s_path: %s", settingPrefix, path.toString());
            throw new IllegalArgumentException(message, ioe);
                throw new IllegalStateException("failed to load hunspell dictionary for locale: " + locale, e);
import java.io.FileNotFoundException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.MalformedInputException;
import java.nio.file.NoSuchFileException;
            () -> 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.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
        List<String> wordList = Analysis.getWordList(env, nodeSettings, "foo.bar");
        Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build();
        Path wordListFile = generateWordList(words);
    private Path generateWordList(String[] words) throws Exception {
        Path wordListFile = createTempDir().resolve("wordlist.txt");
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString;
        assertEquals("failed to load hunspell dictionary for locale: en_US", e.getMessage());
        assertThat(e.getCause(), hasToString(containsString("Missing affix file")));
    public void testDicWithTwoAffs() throws Exception {
        assertEquals("failed to load hunspell dictionary for locale: en_US", e.getMessage());
        assertThat(e.getCause(), hasToString(containsString("Too many affix files")));
Fixed code: - Fix error handling while reading analyzer mapping rules
import org.apache.logging.log4j.LogManager;
import org.opensearch.index.analysis.Analysis;
        Path hyphenationPatternsFile = Analysis.resolveAnalyzerPath(env, hyphenationPatternsPath);
            LogManager.getLogger(HyphenationCompoundWordTokenFilterFactory.class)
                .error("Exception while reading hyphenation_patterns_path ", e);
            throw new IllegalArgumentException("Exception while reading hyphenation_patterns_path.");
import org.opensearch.index.analysis.MappingRule;
        List<MappingRule<String, String>> rules = Analysis.parseWordList(env, settings, "mappings", this::parse);
        rules.forEach(rule -> normMapBuilder.add(rule.getLeft(), rule.getRight()));
    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);
import org.opensearch.index.analysis.MappingRule;
import java.util.ArrayList;
    private static final String MAPPING_SEPARATOR = "=>";
        List<MappingRule<List<String>, String>> rules = Analysis.parseWordList(env, settings, "rules", this::parse);
        for (MappingRule<List<String>, String> rule : rules) {
            for (String key : rule.getLeft()) {
                builder.add(key, rule.getRight());
            }
        }
    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);
        }
        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);
import org.apache.logging.log4j.LogManager;
            LogManager.getLogger(SynonymTokenFilterFactory.class).error("Failed to build synonyms: ", e);
            throw new IllegalArgumentException("Failed to build synonyms");
            List<String> rulesList = Analysis.parseWordList(env, settings, "synonyms", s -> s);
import org.opensearch.index.analysis.MappingRule;
        List<MappingRule<Character, Byte>> charTypeTableValues = Analysis.parseWordList(
            env,
            settings,
            "type_table",
            WordDelimiterTokenFilterFactory::parse
        );
import org.opensearch.index.analysis.MappingRule;
        List<MappingRule<Character, Byte>> charTypeTableValues = Analysis.parseWordList(
            env,
            settings,
            "type_table",
            WordDelimiterTokenFilterFactory::parse
        );
    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);
    }

    static byte[] parseTypes(Collection<MappingRule<Character, Byte>> rules) {
        for (MappingRule<Character, Byte> rule : rules) {
            typeMap.put(rule.getLeft(), rule.getRight());

    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());
    }
/*
 * 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());
    }
}
            RuntimeException ex = expectThrows(RuntimeException.class, () -> create(rule));
            assertEquals("Line [1]: Invalid keyword override rule: " + rule, ex.getMessage());

    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());
    }
            assertThat(e.getMessage(), startsWith("Failed to build synonyms"));
            assertThat(e.getMessage(), startsWith("Failed to build synonyms"));
import org.apache.logging.log4j.LogManager;
                    LogManager.getLogger(IcuCollationTokenFilterFactory.class)
                        .error("Failed to resolve collation rules location", failureToResolve);
                    throw new IllegalArgumentException("Failed to resolve collation rules location");
                    LogManager.getLogger(IcuCollationTokenFilterFactory.class).error("Failed to parse collation rules", e);
                    throw new IllegalArgumentException("Failed to parse collation rules");
        List<String> wordList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
    private static final Logger LOGGER = LogManager.getLogger(KuromojiTokenizerFactory.class);
    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;
    }

            Set<String> dup = new HashSet<>();
            List<String> ruleList = Analysis.parseWordList(
                env,
                settings,
                USER_DICT_PATH_OPTION,
                USER_DICT_RULES_OPTION,
                s -> parse(s, dup)
            );

            LOGGER.error("Failed to load kuromoji user dictionary", e);
            throw new OpenSearchException("Failed to load kuromoji user dictionary");
        RuntimeException exc = expectThrows(RuntimeException.class, () -> createTestAnalysis(settings));
        assertThat(exc.getMessage(), equalTo("Line [4]: Found duplicate term [制限スピード] in user dictionary."));
        final List<String> tagList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
        List<String> tagList = Analysis.parseWordList(env, settings, "stoptags", s -> s);
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
    private static final Logger LOGGER = LogManager.getLogger(NoriTokenizerFactory.class);
        List<String> ruleList = Analysis.parseWordList(env, settings, USER_DICT_PATH_OPTION, USER_DICT_RULES_OPTION, s -> s);
            LOGGER.error("Failed to load nori user dictionary", e);
            throw new OpenSearchException("Failed to load nori user dictionary");
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
    private static final Logger LOGGER = LogManager.getLogger(Analysis.class);
        List<String> pathLoadedWords = parseWordList(env, settings, name, s -> s);
        List<String> wordList = parseWordList(env, settings, settingsPrefix, s -> s);
    public static <T> List<T> parseWordList(Environment env, Settings settings, String settingPrefix, CustomMappingRuleParser<T> parser) {
        return parseWordList(env, settings, settingPrefix + "_path", settingPrefix, parser);
    }

     * Parses a list of words from the specified settings or from a file, with the given parser.
     * @throws RuntimeException
     *          If there is error parsing the words
    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;
    private static List<String> getWordList(Environment env, Settings settings, String settingPath, String settingList) {
            return settings.getAsList(settingList, null);
        final Path path = resolveAnalyzerPath(env, wordListPath);
            return loadWordList(path);
                "Unsupported character encoding detected while reading %s: files must be UTF-8 encoded",
                settingPath
            LOGGER.error("{}: from file: {}, exception is: {}", message, path.toString(), ex);
            throw new IllegalArgumentException(message);
            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) throws IOException {
                result.add(word.trim());
        final Path path = resolveAnalyzerPath(env, filePath);
                "Unsupported character encoding detected while reading %s_path: files must be UTF-8 encoded",
                settingPrefix
            LOGGER.error("{}: from file: {}, exception is: {}", message, path.toString(), ex);
            throw new IllegalArgumentException(message);
            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;
        }
    }
/*
 * 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> {

}
/*
 * 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;
    }
}
                logger.error("Failed to load hunspell dictionary for locale: " + locale, e);
                throw new IllegalStateException("Failed to load hunspell dictionary for locale: " + locale);
            () -> Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> s)
        assertEquals("IOException while reading foo.bar_path: file not readable", ex.getMessage());
        assertNull(ex.getCause());
            () -> 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());
        List<String> wordList = Analysis.parseWordList(env, nodeSettings, "foo.bar", s -> s);
    }
    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());
        Path home = createTempDir();
        Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home.toString()).build();
        Path wordListFile = generateWordList(home, words);
    private Path generateWordList(Path home, String[] words) throws Exception {
        Path config = home.resolve("config");
        Files.createDirectory(config);
        Path wordListFile = config.resolve("wordlist.txt");
        assertEquals("Failed to load hunspell dictionary for locale: en_US", e.getMessage());
        assertNull(e.getCause());
    public void testDicWithTwoAffs() {
        assertEquals("Failed to load hunspell dictionary for locale: en_US", e.getMessage());
        assertNull(e.getCause());