CVE-2022-41917 (Java, published 2022-11-16T00:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 4.3)
Weakness: CWE-755 \u2014 Improper Handling of Exceptional Conditions
The product does not handle or incorrectly handles an exceptional condition.
Repository: https://github.com/opensearch-project/OpenSearch
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 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>

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());