001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.jexl3;
018
019import java.io.BufferedReader;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.Reader;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.math.MathContext;
027import java.nio.charset.Charset;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.function.Predicate;
035
036import org.apache.commons.jexl3.introspection.JexlPermissions;
037import org.apache.commons.jexl3.introspection.JexlUberspect;
038
039/**
040 * Loads a YAML configuration file and applies it to a {@link JexlBuilder}.
041 *
042 * <p>Quick start:</p>
043 * <pre>
044 * try (InputStream in = getClass().getResourceAsStream("/jexl.yaml")) {
045 *     JexlEngine engine = JexlConfigLoader.load(in).create();
046 * }
047 * </pre>
048 *
049 * <p>The loader understands a simple YAML subset: top-level scalars, one level of section nesting,
050 * and list items.  No external YAML library is required.  Inline comments ({@code #} after
051 * whitespace), empty lines, and single/double-quoted string values are supported.</p>
052 *
053 * <h2>Top-level scalar keys</h2>
054 * <p>These map directly to the matching {@link JexlBuilder} setter.  Boolean values accept
055 * {@code true/false}, {@code yes/no}, or {@code on/off}.  Example:</p>
056 * <pre>
057 * strict: true         # JexlBuilder.strict(true)
058 * silent: false        # JexlBuilder.silent(false)
059 * safe: true           # JexlBuilder.safe(true)   — enable safe-navigation ?.
060 * cancellable: false   # JexlBuilder.cancellable(false)
061 * antish: true         # JexlBuilder.antish(true) — resolve ant-style dotted names
062 * lexical: true        # JexlBuilder.lexical(true)
063 * lexicalShade: false  # JexlBuilder.lexicalShade(false)
064 * strictInterpolation: false
065 * booleanLogical: false
066 * debug: true          # include source location in exceptions
067 * cache: 512           # expression cache size (entries)
068 * cacheThreshold: 64   # max expression length cached
069 * stackOverflow: 512   # max recursion depth
070 * collectMode: 1       # variable collection mode (0=off, 1=on)
071 * charset: UTF-8       # source charset for script text
072 * strategy: JEXL_STRATEGY   # property-resolver order: JEXL_STRATEGY or MAP_STRATEGY
073 * </pre>
074 *
075 * <h2>{@code permissions:} section</h2>
076 * <p>Controls which classes and members are visible to scripts.</p>
077 * <pre>
078 * permissions:
079 *   base: SECURE           # NONE (default) | SECURE | RESTRICTED | UNRESTRICTED
080 *   rules:                 # list of JexlPermissions DSL strings composed on top of base
081 *     - "com.example.api +{}"           # allow whole package
082 *     - "com.example.api +{ Foo{} }"    # allow specific class
083 *     - "java.util +{ -Formatter { Formatter(); } }"  # deny one constructor
084 *   classes:               # explicit fully-qualified class names to allow (ClassPermissions)
085 *     - com.example.api.Foo
086 *     - com.example.api.Bar
087 *   logging: my.logger     # optional: wrap in LoggingPermissions; value = logger name
088 *                          # bare "logging:" with no value uses the default logger
089 * </pre>
090 * <p>See {@link JexlPermissions#parse(String...)} for the DSL syntax,
091 * {@link JexlPermissions#SECURE}, {@link JexlPermissions#RESTRICTED},
092 * {@link JexlPermissions#logging()} for the logging wrapper.</p>
093 *
094 * <h2>{@code features:} section</h2>
095 * <p>Controls which syntactic constructs are available at parse time.  Each key is the name of
096 * a {@link JexlFeatures} boolean setter method; unknown keys are silently ignored.</p>
097 * <pre>
098 * features:
099 *   # Script structure
100 *   script: true             # multi-statement scripts (vs. single-expression mode)
101 *   localVar: true           # local variable declarations (var x = ...)
102 *   lambda: true             # lambda / named-function definitions
103 *   loops: true              # for / while / do-while loops
104 *   newInstance: true        # new(...) constructor calls
105 *
106 *   # Side-effects
107 *   sideEffect: true         # any assignment or modification (=, +=, ...)
108 *   sideEffectGlobal: true   # assignment to context / global variables
109 *
110 *   # Literals and operators
111 *   structuredLiteral: true  # array [], map {}, set {} literals; ranges a..b
112 *   arrayReferenceExpr: true # non-constant array index expressions (x[f()])
113 *   methodCall: true         # method calls on objects (obj.method())
114 *   annotation: true         # @annotation statements
115 *   pragma: true             # #pragma directives
116 *   pragmaAnywhere: true     # allow #pragma anywhere (not just at the top)
117 *   namespacePragma: true    # #pragma jexl.namespace.ns ... syntax
118 *   namespaceIdentifier: true# ns:fun(...) compact namespace call syntax
119 *   importPragma: true       # #pragma jexl.import ... syntax
120 *
121 *   # Lambda arrow styles
122 *   thinArrow: true          # thin-arrow lambdas  x -&gt; x + 1
123 *   fatArrow: true           # fat-arrow lambdas   x =&gt; x + 1
124 *
125 *   # Variable capture semantics
126 *   constCapture: true       # captured variables are read-only (Java-style)
127 *   referenceCapture: false  # captured variables are pass-by-reference (ECMAScript-style)
128 *
129 *   # Lexical scoping (also settable at top level via 'lexical:' / 'lexicalShade:')
130 *   lexical: true
131 *   lexicalShade: false
132 *
133 *   # Misc
134 *   comparatorNames: true    # allow 'gt', 'lt', 'ge', 'le', 'eq', 'ne' as operator aliases
135 *   ambiguousStatement: true # allow statements that are syntactically ambiguous
136 *   ignoreTemplatePrefix: false
137 *
138 *   # Reserved names (list — cannot be used as local variable or parameter names)
139 *   reservedNames:
140 *     - try
141 *     - catch
142 *     - class
143 * </pre>
144 *
145 * <h2>{@code arithmetic:} section</h2>
146 * <p>Selects and configures the {@link JexlArithmetic} implementation.</p>
147 * <pre>
148 * arithmetic:
149 *   clazz: org.apache.commons.jexl3.JexlArithmetic  # fully-qualified class (default)
150 *   strict: true       # strict arithmetic (true = default); passed to the constructor
151 *   mathContext: DECIMAL64   # java.math.MathContext field: DECIMAL32 | DECIMAL64 | DECIMAL128 | UNLIMITED
152 *   mathScale: 10      # BigDecimal scale; requires mathContext; -1 = use context default
153 * </pre>
154 * <p>When {@code mathContext} is present the constructor {@code (boolean, MathContext, int)} is used;
155 * otherwise {@code (boolean)} is used.  The class must be on the classpath.</p>
156 *
157 * <h2>{@code namespaces:} section</h2>
158 * <p>Maps namespace prefixes to fully-qualified class names.  The class is loaded and passed to
159 * {@link JexlBuilder#namespaces(java.util.Map)}.  Its static (or instance) methods become
160 * callable as {@code prefix:methodName(args)} from scripts.</p>
161 * <pre>
162 * namespaces:
163 *   math: java.lang.Math          # math:abs(-1) etc.
164 *   str:  com.example.StringUtils # str:trim(x) etc.
165 * </pre>
166 *
167 * <h2>{@code imports:} section</h2>
168 * <p>A list of package or class names passed to {@link JexlBuilder#imports(java.util.Collection)}.
169 * Imported packages allow unqualified class names in {@code new(...)} and type references.</p>
170 * <pre>
171 * imports:
172 *   - java.lang
173 *   - java.util
174 *   - com.example.api
175 * </pre>
176 *
177 * <h2>{@code pragmas:} section</h2>
178 * <p>A list of allowed pragma key patterns, turned into the predicate passed to
179 * {@link JexlFeatures#pragmaTest(java.util.function.Predicate)}.  A pattern ending with {@code *}
180 * matches any key starting with what precedes it; any other pattern must match the key exactly.
181 * Scripts declaring a pragma whose key matches no pattern fail to parse with a
182 * {@link JexlException.Feature}.  The section is an allow-list: when present with no items, no pragma is
183 * allowed; when absent, all pragmas are allowed (subject to the {@code pragma}, {@code importPragma} and
184 * {@code namespacePragma} features).  The list applies to JEXL's own pragmas too: {@code jexl.options},
185 * {@code jexl.import}, {@code jexl.namespace.*} and {@code jexl.module.*} must be listed to be allowed.
186 * A single pattern may also be written inline
187 * ({@code pragmas: dlm.*}); entries that are not {@code - pattern} items, or flow-style lists, are
188 * rejected with an {@link IllegalArgumentException}.</p>
189 * <pre>
190 * pragmas:
191 *   - dlm.*              # any pragma key starting with 'dlm.'
192 *   - jexl.import        # exactly 'jexl.import'
193 *   - jexl.namespace.*
194 * </pre>
195 *
196 * <h2>Complete annotated example</h2>
197 * <p>Every flag is listed explicitly so the configuration does not depend on any library default
198 * (which may change between releases).  The {@code features:} block below reproduces the pre-3.7
199 * feature set ({@link JexlFeatures#createDefault()}).</p>
200 * <pre>
201 * # Production engine — explicit permissions + legacy feature set
202 * strict: true
203 * safe: false
204 * cache: 512
205 *
206 * permissions:
207 *   base: RESTRICTED
208 *   rules:
209 *     - "com.example.api +{}"
210 *   logging: com.example.jexl.permissions  # log allow/deny at INFO once per element
211 *
212 * features:
213 *   script: true
214 *   localVar: true
215 *   lambda: true
216 *   loops: true
217 *   newInstance: true
218 *   sideEffect: true
219 *   sideEffectGlobal: true
220 *   structuredLiteral: true
221 *   arrayReferenceExpr: true
222 *   methodCall: true
223 *   annotation: true
224 *   pragma: true
225 *   pragmaAnywhere: true
226 *   namespacePragma: true
227 *   importPragma: true
228 *   namespaceIdentifier: false
229 *   thinArrow: true
230 *   fatArrow: false
231 *   constCapture: false
232 *   referenceCapture: false
233 *   lexical: false
234 *   lexicalShade: false
235 *   comparatorNames: true
236 *   ambiguousStatement: false
237 *   ignoreTemplatePrefix: false
238 *
239 * namespaces:
240 *   math: java.lang.Math
241 *
242 * imports:
243 *   - java.lang
244 *   - java.util
245 * </pre>
246 *
247 * @since 3.7.0
248 */
249public final class JexlConfigLoader {
250
251    private JexlConfigLoader() { }
252
253    /**
254     * Loads configuration from a YAML {@link InputStream} (UTF-8) into a {@link JexlBuilder}.
255     *
256     * @param in YAML input; the caller is responsible for closing it
257     * @return A configured JexlBuilder
258     * @throws IOException Thrown if the stream cannot be read
259     */
260    public static JexlBuilder load(final InputStream in) throws IOException {
261        return load(new InputStreamReader(in, StandardCharsets.UTF_8));
262    }
263
264    /**
265     * Loads configuration from a YAML {@link Reader} into a {@link JexlBuilder}.
266     *
267     * @param reader YAML input; the caller is responsible for closing it
268     * @return A configured JexlBuilder
269     * @throws IOException Thrown if the reader cannot be read
270     */
271    public static JexlBuilder load(final Reader reader) throws IOException {
272        final JexlBuilder builder = new JexlBuilder();
273        final JexlFeatures features = new JexlFeatures();
274        parse(reader instanceof BufferedReader ? (BufferedReader) reader
275            : new BufferedReader(reader), builder, features);
276        return builder.features(features);
277    }
278
279    /**
280     * Convenience: loads YAML from {@code in} and creates the engine in one call.
281     *
282     * @param in YAML input; the caller is responsible for closing it
283     * @return A new JexlEngine configured from the YAML
284     * @throws IOException Thrown if the stream cannot be read
285     */
286    public static JexlEngine engine(final InputStream in) throws IOException {
287        return load(in).create();
288    }
289
290    // =========================================================================
291    //  Parser
292    // =========================================================================
293
294    /**
295     * Minimal line-by-line YAML parser that handles the subset needed for JEXL configuration:
296     * top-level key:value scalars, one-level section blocks, and list items within sections.
297     */
298    private static void parse(final BufferedReader reader,
299                              final JexlBuilder builder,
300                              final JexlFeatures features) throws IOException {
301        String section = null;
302        Map<String, Object> sectionData = null;
303        List<String> currentList = null;
304        String currentListKey = null;
305
306        for (String raw; (raw = reader.readLine()) != null; ) {
307            final int indent = leadingSpaces(raw);
308            final String stripped = ltrim(raw);
309            if (stripped.isEmpty() || stripped.charAt(0) == '#') {
310                continue;
311            }
312            final String line = stripComment(stripped);
313            if (line.isEmpty()) {
314                continue;
315            }
316
317            if (indent == 0) {
318                // back to top level — flush any open section
319                if (section != null) {
320                    if (currentList != null && currentListKey != null) {
321                        sectionData.put(currentListKey, currentList);
322                    }
323                    flushSection(section, sectionData, builder, features);
324                    section = null;
325                    sectionData = null;
326                    currentList = null;
327                    currentListKey = null;
328                }
329
330                final int colon = line.indexOf(':');
331                final String key = colon < 0 ? line : rtrim(line.substring(0, colon));
332                final String val = colon < 0 ? "" : ltrim(line.substring(colon + 1));
333
334                if (val.isEmpty()) {
335                    section = key;
336                    sectionData = new LinkedHashMap<>();
337                } else if ("pragmas".equals(key)) {
338                    // a single pattern on the same line
339                    features.pragmaTest(parsePragmaPatterns(Collections.singletonList(unquote(val))));
340                } else {
341                    applyTopLevel(key, val, builder);
342                }
343
344            } else {
345                // inside a section
346                if (section == null) {
347                    continue;
348                }
349
350                if (line.charAt(0) == '-') {
351                    // list item
352                    final String item = unquote(ltrim(line.substring(1)));
353                    if (currentList == null) {
354                        currentList = new ArrayList<>();
355                        currentListKey = section;
356                    }
357                    currentList.add(item);
358                } else {
359                    final int colon = line.indexOf(':');
360                    final String key = colon < 0 ? line : rtrim(line.substring(0, colon));
361                    final String val = colon < 0 ? "" : ltrim(line.substring(colon + 1));
362
363                    if (val.isEmpty()) {
364                        // nested list-key start (e.g. "rules:")
365                        if (currentList != null && currentListKey != null) {
366                            sectionData.put(currentListKey, currentList);
367                        }
368                        currentListKey = key;
369                        currentList = new ArrayList<>();
370                    } else {
371                        // scalar within section — close any pending list first
372                        if (currentList != null && currentListKey != null) {
373                            sectionData.put(currentListKey, currentList);
374                            currentList = null;
375                            currentListKey = null;
376                        }
377                        sectionData.put(key, val);
378                    }
379                }
380            }
381        }
382
383        // flush the last section
384        if (section != null) {
385            if (currentList != null && currentListKey != null) {
386                sectionData.put(currentListKey, currentList);
387            }
388            flushSection(section, sectionData, builder, features);
389        }
390    }
391
392    // =========================================================================
393    //  Section dispatchers
394    // =========================================================================
395
396    @SuppressWarnings("unchecked")
397    private static void flushSection(final String section,
398                                     final Map<String, Object> data,
399                                     final JexlBuilder builder,
400                                     final JexlFeatures features) {
401        switch (section) {
402            case "permissions":
403                applyPermissions(data, builder);
404                break;
405            case "namespaces":
406                applyNamespaces(data, builder);
407                break;
408            case "arithmetic":
409                applyArithmetic(data, builder);
410                break;
411            case "features":
412                applyFeatures(data, features);
413                break;
414            case "imports": {
415                final Object list = data.get(section);
416                if (list instanceof List) {
417                    builder.imports((List<String>) list);
418                }
419                break;
420            }
421            case "pragmas": {
422                // only list items are expected; anything else is a configuration mistake
423                for (final String key : data.keySet()) {
424                    if (!section.equals(key)) {
425                        throw new IllegalArgumentException(
426                            "pragmas: expects a list of '- pattern' items, found '" + key + "'");
427                    }
428                }
429                // an allow-list: a section with no items allows no pragma
430                final Object list = data.get(section);
431                features.pragmaTest(parsePragmaPatterns(list instanceof List
432                    ? (List<String>) list
433                    : Collections.emptyList()));
434                break;
435            }
436            default:
437                break;
438        }
439    }
440
441    private static void applyTopLevel(final String key,
442                                      final String value,
443                                      final JexlBuilder builder) {
444        switch (key) {
445            case "strict":              builder.strict(parseBool(value)); break;
446            case "silent":              builder.silent(parseBool(value)); break;
447            case "safe":                builder.safe(parseBool(value)); break;
448            case "cancellable":         builder.cancellable(parseBool(value)); break;
449            case "antish":              builder.antish(parseBool(value)); break;
450            case "lexical":             builder.lexical(parseBool(value)); break;
451            case "lexicalShade":        builder.lexicalShade(parseBool(value)); break;
452            case "strictInterpolation": builder.strictInterpolation(parseBool(value)); break;
453            case "booleanLogical":      builder.booleanLogical(parseBool(value)); break;
454            case "debug":               builder.debug(parseBool(value)); break;
455            case "cache":               builder.cache(parseInt(value)); break;
456            case "cacheThreshold":      builder.cacheThreshold(parseInt(value)); break;
457            case "stackOverflow":       builder.stackOverflow(parseInt(value)); break;
458            case "collectMode":         builder.collectMode(parseInt(value)); break;
459            case "charset":             builder.charset(Charset.forName(value)); break;
460            case "strategy":            builder.strategy(parseStrategy(value)); break;
461            default:
462                break;
463        }
464    }
465
466    @SuppressWarnings("unchecked")
467    private static void applyPermissions(final Map<String, Object> data,
468                                         final JexlBuilder builder) {
469        final Object baseVal = data.get("base");
470        // absent or "NONE" → deny-everything base (build from scratch with rules/classes)
471        JexlPermissions perms = "UNRESTRICTED".equals(baseVal) ? JexlPermissions.UNRESTRICTED
472            : "SECURE".equals(baseVal) ? JexlPermissions.SECURE
473            : "RESTRICTED".equals(baseVal) ? JexlPermissions.RESTRICTED
474            : JexlPermissions.NONE;
475        final Object rules = data.get("rules");
476        if (rules instanceof List && !((List<?>) rules).isEmpty()) {
477            final List<String> ruleList = (List<String>) rules;
478            perms = perms.compose(ruleList.toArray(new String[0]));
479        }
480        final Object classes = data.get("classes");
481        if (classes instanceof List && !((List<?>) classes).isEmpty()) {
482            perms = new JexlPermissions.ClassPermissions(perms, (List<String>) classes);
483        }
484        // optional logging wrapper, outermost so it reports the effective decisions;
485        // a String value names the logger, a bare "logging:" uses the default logger
486        final Object logging = data.get("logging");
487        if (logging != null) {
488            final String name = logging instanceof String ? (String) logging : "";
489            perms = name.isEmpty() ? perms.logging() : perms.logging(name);
490        }
491        builder.permissions(perms);
492    }
493
494    private static void applyNamespaces(final Map<String, Object> data,
495                                        final JexlBuilder builder) {
496        final Map<String, Object> ns = new LinkedHashMap<>(data.size());
497        for (final Map.Entry<String, Object> e : data.entrySet()) {
498            if (e.getValue() instanceof String) {
499                try {
500                    ns.put(e.getKey(), Class.forName((String) e.getValue()));
501                } catch (final ClassNotFoundException ex) {
502                    throw new IllegalArgumentException(
503                        "namespace class not found: " + e.getValue(), ex);
504                }
505            }
506        }
507        if (!ns.isEmpty()) {
508            builder.namespaces(ns);
509        }
510    }
511
512    private static void applyArithmetic(final Map<String, Object> data,
513                                        final JexlBuilder builder) {
514        final String className = data.containsKey("clazz")
515            ? (String) data.get("clazz")
516            : JexlArithmetic.class.getName();
517        final boolean astrict = !data.containsKey("strict") || parseBool((String) data.get("strict"));
518        final Class<?> clazz;
519        try {
520            clazz = Class.forName(className);
521        } catch (final ClassNotFoundException e) {
522            throw new IllegalArgumentException("arithmetic class not found: " + className, e);
523        }
524        try {
525            if (data.containsKey("mathContext")) {
526                final MathContext mc = (MathContext)
527                    MathContext.class.getField((String) data.get("mathContext")).get(null);
528                final int scale = data.containsKey("mathScale")
529                    ? parseInt((String) data.get("mathScale"))
530                    : -1;
531                builder.arithmetic((JexlArithmetic) clazz
532                    .getConstructor(boolean.class, MathContext.class, int.class)
533                    .newInstance(astrict, mc, scale));
534            } else {
535                builder.arithmetic((JexlArithmetic) clazz
536                    .getConstructor(boolean.class)
537                    .newInstance(astrict));
538            }
539        } catch (final ReflectiveOperationException e) {
540            throw new IllegalArgumentException(
541                "cannot instantiate arithmetic class " + className, e);
542        }
543    }
544
545    @SuppressWarnings("unchecked")
546    private static void applyFeatures(final Map<String, Object> data,
547                                      final JexlFeatures features) {
548        for (final Map.Entry<String, Object> e : data.entrySet()) {
549            final String key = e.getKey();
550            final Object val = e.getValue();
551            if ("reservedNames".equals(key) && val instanceof List) {
552                features.reservedNames((List<String>) val);
553            } else if (val instanceof String) {
554                try {
555                    final Method m = JexlFeatures.class.getMethod(key, boolean.class);
556                    m.invoke(features, parseBool((String) val));
557                } catch (final NoSuchMethodException ignored) {
558                    // unknown feature key — skip
559                } catch (final IllegalAccessException | InvocationTargetException ex) {
560                    throw new IllegalArgumentException("cannot set feature " + key, ex);
561                }
562            }
563        }
564    }
565
566    // =========================================================================
567    //  Parsing helpers
568    // =========================================================================
569
570    private static JexlUberspect.ResolverStrategy parseStrategy(final String name) {
571        return "MAP_STRATEGY".equals(name)
572            ? JexlUberspect.MAP_STRATEGY
573            : JexlUberspect.JEXL_STRATEGY;
574    }
575
576    /**
577     * Creates a pragma key predicate from a list of patterns.
578     *
579     * @see JexlFeatures#pragmaPatterns(java.util.Collection)
580     */
581    private static Predicate<String> parsePragmaPatterns(final List<String> patterns) {
582        for (final String pattern : patterns) {
583            final String trimmed = pattern.trim();
584            // flow-style lists ([a, b] or a, b) are not supported
585            if (trimmed.startsWith("[") || trimmed.indexOf(',') >= 0) {
586                throw new IllegalArgumentException(
587                    "pragmas: expects one pattern per '- pattern' item, found '" + trimmed + "'");
588            }
589        }
590        return JexlFeatures.pragmaPatterns(patterns);
591    }
592
593    private static boolean parseBool(final String s) {
594        return "true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s) || "on".equalsIgnoreCase(s);
595    }
596
597    private static int parseInt(final String s) {
598        return Integer.parseInt(s.trim());
599    }
600
601    private static int leadingSpaces(final String s) {
602        int i = 0;
603        while (i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t')) {
604            i++;
605        }
606        return i;
607    }
608
609    private static String ltrim(final String s) {
610        int i = 0;
611        while (i < s.length() && Character.isWhitespace(s.charAt(i))) {
612            i++;
613        }
614        return i == 0 ? s : s.substring(i);
615    }
616
617    private static String rtrim(final String s) {
618        int i = s.length();
619        while (i > 0 && Character.isWhitespace(s.charAt(i - 1))) {
620            i--;
621        }
622        return i == s.length() ? s : s.substring(0, i);
623    }
624
625    private static String stripComment(final String s) {
626        boolean inDouble = false;
627        boolean inSingle = false;
628        for (int i = 0; i < s.length(); i++) {
629            final char c = s.charAt(i);
630            if (c == '"' && !inSingle) {
631                inDouble = !inDouble;
632            } else if (c == '\'' && !inDouble) {
633                inSingle = !inSingle;
634            } else if (c == '#' && !inDouble && !inSingle
635                && i > 0 && Character.isWhitespace(s.charAt(i - 1))) {
636                return rtrim(s.substring(0, i));
637            }
638        }
639        return s;
640    }
641
642    private static String unquote(final String s) {
643        if (s.length() >= 2) {
644            final char first = s.charAt(0);
645            final char last = s.charAt(s.length() - 1);
646            if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
647                return s.substring(1, s.length() - 1);
648            }
649        }
650        return s;
651    }
652}