/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.firewall;

import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.PatternMatcher;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.regex.Pattern;

abstract class StringFilter implements Filter {
    private static final String ATTR_EQUALS = "equals";
    private static final String ATTR_STARTS_WITH = "startsWith";
    private static final String ATTR_CONTAINS = "contains";
    private static final String ATTR_PATTERN = "pattern";
    private static final String ATTR_REGEX = "regex";
    private static final String ATTR_IS_NULL = "isNull";

    private final ValueProvider mValueProvider;

    private StringFilter(ValueProvider valueProvider) {
        this.mValueProvider = valueProvider;
    }

    /**
     * Constructs a new StringFilter based on the string filter attribute on the current
     * element, and the given StringValueMatcher.
     *
     * The current node should contain exactly 1 string filter attribute. E.g. equals,
     * contains, etc. Otherwise, an XmlPullParserException will be thrown.
     *
     * @param parser      An XmlPullParser object positioned at an element that should
     *                    contain a string filter attribute
     * @return This StringFilter object
     */
    public static StringFilter readFromXml(ValueProvider valueProvider, XmlPullParser parser)
            throws IOException, XmlPullParserException {
        StringFilter filter = null;

        for (int i=0; i<parser.getAttributeCount(); i++) {
            StringFilter newFilter = getFilter(valueProvider, parser, i);
            if (newFilter != null) {
                if (filter != null) {
                    throw new XmlPullParserException("Multiple string filter attributes found");
                }
                filter = newFilter;
            }
        }

        if (filter == null) {
            // if there are no string filter attributes, we default to isNull="false" so that an
            // empty filter is equivalent to an existence check
            filter = new IsNullFilter(valueProvider, false);
        }

        return filter;
    }

    private static StringFilter getFilter(ValueProvider valueProvider, XmlPullParser parser,
            int attributeIndex) {
        String attributeName = parser.getAttributeName(attributeIndex);

        switch (attributeName.charAt(0)) {
            case 'e':
                if (!attributeName.equals(ATTR_EQUALS)) {
                    return null;
                }
                return new EqualsFilter(valueProvider, parser.getAttributeValue(attributeIndex));
            case 'i':
                if (!attributeName.equals(ATTR_IS_NULL)) {
                    return null;
                }
                return new IsNullFilter(valueProvider, parser.getAttributeValue(attributeIndex));
            case 's':
                if (!attributeName.equals(ATTR_STARTS_WITH)) {
                    return null;
                }
                return new StartsWithFilter(valueProvider,
                        parser.getAttributeValue(attributeIndex));
            case 'c':
                if (!attributeName.equals(ATTR_CONTAINS)) {
                    return null;
                }
                return new ContainsFilter(valueProvider, parser.getAttributeValue(attributeIndex));
            case 'p':
                if (!attributeName.equals(ATTR_PATTERN)) {
                    return null;
                }
                return new PatternStringFilter(valueProvider,
                        parser.getAttributeValue(attributeIndex));
            case 'r':
                if (!attributeName.equals(ATTR_REGEX)) {
                    return null;
                }
                return new RegexFilter(valueProvider, parser.getAttributeValue(attributeIndex));
        }
        return null;
    }

    protected abstract boolean matchesValue(String value);

    @Override
    public boolean matches(IntentFirewall ifw, Intent intent, ApplicationInfo callerApp,
            int callerUid, int callerPid, String resolvedType, ApplicationInfo resolvedApp) {
        String value = mValueProvider.getValue(intent, callerApp, resolvedType, resolvedApp);
        return matchesValue(value);
    }

    private static abstract class ValueProvider extends FilterFactory {
        protected ValueProvider(String tag) {
            super(tag);
        }

        public Filter newFilter(XmlPullParser parser)
                throws IOException, XmlPullParserException {
            return StringFilter.readFromXml(this, parser);
        }

        public abstract String getValue(Intent intent, ApplicationInfo callerApp,
                String resolvedType, ApplicationInfo resolvedApp);
    }

    private static class EqualsFilter extends StringFilter {
        private final String mFilterValue;

        public EqualsFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            mFilterValue = attrValue;
        }

        @Override
        public boolean matchesValue(String value) {
            return value != null && value.equals(mFilterValue);
        }
    }

    private static class ContainsFilter extends StringFilter {
        private final String mFilterValue;

        public ContainsFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            mFilterValue = attrValue;
        }

        @Override
        public boolean matchesValue(String value) {
            return value != null && value.contains(mFilterValue);
        }
    }

    private static class StartsWithFilter extends StringFilter {
        private final String mFilterValue;

        public StartsWithFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            mFilterValue = attrValue;
        }

        @Override
        public boolean matchesValue(String value) {
            return value != null && value.startsWith(mFilterValue);
        }
    }

    private static class PatternStringFilter extends StringFilter {
        private final PatternMatcher mPattern;

        public PatternStringFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            mPattern = new PatternMatcher(attrValue, PatternMatcher.PATTERN_SIMPLE_GLOB);
        }

        @Override
        public boolean matchesValue(String value) {
            return value != null && mPattern.match(value);
        }
    }

    private static class RegexFilter extends StringFilter {
        private final Pattern mPattern;

        public RegexFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            this.mPattern = Pattern.compile(attrValue);
        }

        @Override
        public boolean matchesValue(String value) {
            return value != null && mPattern.matcher(value).matches();
        }
    }

    private static class IsNullFilter extends StringFilter {
        private final boolean mIsNull;

        public IsNullFilter(ValueProvider valueProvider, String attrValue) {
            super(valueProvider);
            mIsNull = Boolean.parseBoolean(attrValue);
        }

        public IsNullFilter(ValueProvider valueProvider, boolean isNull) {
            super(valueProvider);
            mIsNull = isNull;
        }

        @Override
        public boolean matchesValue(String value) {
            return (value == null) == mIsNull;
        }
    }

    public static final ValueProvider COMPONENT = new ValueProvider("component") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            ComponentName cn = intent.getComponent();
            if (cn != null) {
                return cn.flattenToString();
            }
            return null;
        }
    };

    public static final ValueProvider COMPONENT_NAME = new ValueProvider("component-name") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            ComponentName cn = intent.getComponent();
            if (cn != null) {
                return cn.getClassName();
            }
            return null;
        }
    };

    public static final ValueProvider COMPONENT_PACKAGE = new ValueProvider("component-package") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            ComponentName cn = intent.getComponent();
            if (cn != null) {
                return cn.getPackageName();
            }
            return null;
        }
    };

    public static final FilterFactory ACTION = new ValueProvider("action") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            return intent.getAction();
        }
    };

    public static final ValueProvider DATA = new ValueProvider("data") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            Uri data = intent.getData();
            if (data != null) {
                return data.toString();
            }
            return null;
        }
    };

    public static final ValueProvider MIME_TYPE = new ValueProvider("mime-type") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            return resolvedType;
        }
    };

    public static final ValueProvider SCHEME = new ValueProvider("scheme") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            Uri data = intent.getData();
            if (data != null) {
                return data.getScheme();
            }
            return null;
        }
    };

    public static final ValueProvider SSP = new ValueProvider("scheme-specific-part") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            Uri data = intent.getData();
            if (data != null) {
                return data.getSchemeSpecificPart();
            }
            return null;
        }
    };

    public static final ValueProvider HOST = new ValueProvider("host") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            Uri data = intent.getData();
            if (data != null) {
                return data.getHost();
            }
            return null;
        }
    };

    public static final ValueProvider PATH = new ValueProvider("path") {
        @Override
        public String getValue(Intent intent, ApplicationInfo callerApp, String resolvedType,
                ApplicationInfo resolvedApp) {
            Uri data = intent.getData();
            if (data != null) {
                return data.getPath();
            }
            return null;
        }
    };
}
