Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5a10db6a authored by Winson's avatar Winson
Browse files

Add DomainVerificationPersistence

Contains the XML serialization/parsing code for system state related
to domain verification.

Includes a change to Pair to support Kotlin deconstructing declarations.

Exempt-From-Owner-Approval: Already approved by owners on main branch

Bug: 163565712

Test: atest DomainVerificationPersistenceTest

Change-Id: I4a3e03e9dfc33b4157e0505900c00c37be823ecd
parent 018a0099
Loading
Loading
Loading
Loading
+404 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.pm;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Stack;

/**
 * A very specialized serialization/parsing wrapper around {@link TypedXmlSerializer} and {@link
 * TypedXmlPullParser} intended for use with PackageManager related settings files.
 * Assumptions/chosen behaviors:
 * <ul>
 *     <li>No namespace support</li>
 *     <li>Data for a parent object is stored as attributes</li>
 *     <li>All attribute read methods return a default false, -1, or null</li>
 *     <li>Default values will not be written</li>
 *     <li>Children are sub-elements</li>
 *     <li>Collections are repeated sub-elements, no attribute support for collections</li>
 * </ul>
 */
public class SettingsXml {

    private static final String TAG = "SettingsXml";

    private static final boolean DEBUG_THROW_EXCEPTIONS = false;

    private static final String FEATURE_INDENT =
            "http://xmlpull.org/v1/doc/features.html#indent-output";

    private static final int DEFAULT_NUMBER = -1;

    public static Serializer serializer(TypedXmlSerializer serializer) {
        return new Serializer(serializer);
    }

    public static ReadSection parser(TypedXmlPullParser parser)
            throws IOException, XmlPullParserException {
        return new ReadSectionImpl(parser);
    }

    public static class Serializer implements AutoCloseable {

        @NonNull
        private final TypedXmlSerializer mXmlSerializer;

        private final WriteSectionImpl mWriteSection;

        private Serializer(TypedXmlSerializer serializer) {
            mXmlSerializer = serializer;
            mWriteSection = new WriteSectionImpl(mXmlSerializer);
        }

        public WriteSection startSection(@NonNull String sectionName) throws IOException {
            return mWriteSection.startSection(sectionName);
        }

        @Override
        public void close() throws IOException {
            mWriteSection.closeCompletely();
            mXmlSerializer.endDocument();
        }
    }

    public interface ReadSection extends AutoCloseable {

        @NonNull
        String getName();

        @NonNull
        String getDescription();

        boolean has(String attrName);

        @Nullable
        String getString(String attrName);

        /**
         * @return value as String or {@param defaultValue} if doesn't exist
         */
        @NonNull
        String getString(String attrName, @NonNull String defaultValue);

        /**
         * @return value as boolean or false if doesn't exist
         */
        boolean getBoolean(String attrName);

        /**
         * @return value as boolean or {@param defaultValue} if doesn't exist
         */
        boolean getBoolean(String attrName, boolean defaultValue);

        /**
         * @return value as int or {@link #DEFAULT_NUMBER} if doesn't exist
         */
        int getInt(String attrName);

        /**
         * @return value as int or {@param defaultValue} if doesn't exist
         */
        int getInt(String attrName, int defaultValue);

        /**
         * @return value as long or {@link #DEFAULT_NUMBER} if doesn't exist
         */
        long getLong(String attrName);

        /**
         * @return value as long or {@param defaultValue} if doesn't exist
         */
        long getLong(String attrName, int defaultValue);

        ChildSection children();
    }

    /**
     * <pre><code>
     * ChildSection child = parentSection.children();
     * while (child.moveToNext(TAG_CHILD)) {
     *     String readValue = child.getString(...);
     *     ...
     * }
     * </code></pre>
     */
    public interface ChildSection extends ReadSection {
        boolean moveToNext();

        boolean moveToNext(@NonNull String expectedChildTagName);
    }

    public static class ReadSectionImpl implements ChildSection {

        @Nullable
        private final InputStream mInput;

        @NonNull
        private final TypedXmlPullParser mParser;

        @NonNull
        private final Stack<Integer> mDepthStack = new Stack<>();

        public ReadSectionImpl(@NonNull InputStream input)
                throws IOException, XmlPullParserException {
            mInput = input;
            mParser = Xml.newFastPullParser();
            mParser.setInput(mInput, StandardCharsets.UTF_8.name());
            moveToFirstTag();
        }

        public ReadSectionImpl(@NonNull TypedXmlPullParser parser)
                throws IOException, XmlPullParserException {
            mInput = null;
            mParser = parser;
            moveToFirstTag();
        }

        private void moveToFirstTag() throws IOException, XmlPullParserException {
            // Move to first tag
            int type;
            //noinspection StatementWithEmptyBody
            while ((type = mParser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
            }
        }

        @NonNull
        @Override
        public String getName() {
            return mParser.getName();
        }

        @NonNull
        @Override
        public String getDescription() {
            return mParser.getPositionDescription();
        }

        @Override
        public boolean has(String attrName) {
            return mParser.getAttributeValue(null, attrName) != null;
        }

        @Nullable
        @Override
        public String getString(String attrName) {
            return mParser.getAttributeValue(null, attrName);
        }

        @NonNull
        @Override
        public String getString(String attrName, @NonNull String defaultValue) {
            String value = mParser.getAttributeValue(null, attrName);
            if (value == null) {
                return defaultValue;
            }
            return value;
        }

        @Override
        public boolean getBoolean(String attrName) {
            return getBoolean(attrName, false);
        }

        @Override
        public boolean getBoolean(String attrName, boolean defaultValue) {
            return mParser.getAttributeBoolean(null, attrName, defaultValue);
        }

        @Override
        public int getInt(String attrName) {
            return getInt(attrName, DEFAULT_NUMBER);
        }

        @Override
        public int getInt(String attrName, int defaultValue) {
            return mParser.getAttributeInt(null, attrName, defaultValue);
        }

        @Override
        public long getLong(String attrName) {
            return getLong(attrName, DEFAULT_NUMBER);
        }

        @Override
        public long getLong(String attrName, int defaultValue) {
            return mParser.getAttributeLong(null, attrName, defaultValue);
        }

        @Override
        public ChildSection children() {
            mDepthStack.push(mParser.getDepth());
            return this;
        }

        @Override
        public boolean moveToNext() {
            return moveToNextInternal(null);
        }

        @Override
        public boolean moveToNext(@NonNull String expectedChildTagName) {
            return moveToNextInternal(expectedChildTagName);
        }

        private boolean moveToNextInternal(@Nullable String expectedChildTagName) {
            try {
                int depth = mDepthStack.peek();
                boolean hasTag = false;
                int type;
                while (!hasTag
                        && (type = mParser.next()) != XmlPullParser.END_DOCUMENT
                        && (type != XmlPullParser.END_TAG || mParser.getDepth() > depth)) {
                    if (type != XmlPullParser.START_TAG) {
                        continue;
                    }

                    if (expectedChildTagName != null
                            && !expectedChildTagName.equals(mParser.getName())) {
                        continue;
                    }

                    hasTag = true;
                }

                if (!hasTag) {
                    mDepthStack.pop();
                }

                return hasTag;
            } catch (Exception ignored) {
                return false;
            }
        }

        @Override
        public void close() throws Exception {
            if (mDepthStack.isEmpty()) {
                Slog.wtf(TAG, "Children depth stack was not empty, data may have been lost",
                        new Exception());
            }
            if (mInput != null) {
                mInput.close();
            }
        }
    }

    public interface WriteSection extends AutoCloseable {

        WriteSection startSection(@NonNull String sectionName) throws IOException;

        WriteSection attribute(String attrName, @Nullable String value) throws IOException;

        WriteSection attribute(String attrName, int value) throws IOException;

        WriteSection attribute(String attrName, long value) throws IOException;

        WriteSection attribute(String attrName, boolean value) throws IOException;

        @Override
        void close() throws IOException;

        void finish() throws IOException;
    }

    private static class WriteSectionImpl implements WriteSection {

        @NonNull
        private final TypedXmlSerializer mXmlSerializer;

        @NonNull
        private final Stack<String> mTagStack = new Stack<>();

        private WriteSectionImpl(@NonNull TypedXmlSerializer xmlSerializer) {
            mXmlSerializer = xmlSerializer;
        }

        @Override
        public WriteSection startSection(@NonNull String sectionName) throws IOException {
            // Try to start the tag first before we push it to the stack
            mXmlSerializer.startTag(null, sectionName);
            mTagStack.push(sectionName);
            return this;
        }

        @Override
        public WriteSection attribute(String attrName, String value) throws IOException {
            if (value != null) {
                mXmlSerializer.attribute(null, attrName, value);
            }
            return this;
        }

        @Override
        public WriteSection attribute(String attrName, int value) throws IOException {
            if (value != DEFAULT_NUMBER) {
                mXmlSerializer.attributeInt(null, attrName, value);
            }
            return this;
        }

        @Override
        public WriteSection attribute(String attrName, long value) throws IOException {
            if (value != DEFAULT_NUMBER) {
                mXmlSerializer.attributeLong(null, attrName, value);
            }
            return this;
        }

        @Override
        public WriteSection attribute(String attrName, boolean value) throws IOException {
            if (value) {
                mXmlSerializer.attributeBoolean(null, attrName, value);
            }
            return this;
        }

        @Override
        public void finish() throws IOException {
            close();
        }

        @Override
        public void close() throws IOException {
            mXmlSerializer.endTag(null, mTagStack.pop());
        }

        private void closeCompletely() throws IOException {
            if (DEBUG_THROW_EXCEPTIONS && mTagStack != null && !mTagStack.isEmpty()) {
                throw new IllegalStateException(
                        "tag stack is not empty when closing, contains " + mTagStack);
            } else if (mTagStack != null) {
                while (!mTagStack.isEmpty()) {
                    close();
                }
            }
        }
    }
}
+313 −0

File added.

Preview size limit exceeded, changes collapsed.

+7 −0
Original line number Diff line number Diff line
@@ -19,6 +19,10 @@ package com.android.server.pm.test.domain.verify
import android.content.pm.domain.verify.DomainVerificationRequest
import android.content.pm.domain.verify.DomainVerificationSet
import android.content.pm.domain.verify.DomainVerificationUserSelection
import com.android.server.pm.domain.verify.DomainVerificationPersistence

operator fun <F> android.util.Pair<F, *>.component1() = first
operator fun <S> android.util.Pair<*, S>.component2() = second

operator fun DomainVerificationRequest.component1() = packageNames

@@ -31,3 +35,6 @@ operator fun DomainVerificationUserSelection.component2() = packageName
operator fun DomainVerificationUserSelection.component3() = user
operator fun DomainVerificationUserSelection.component4() = isLinkHandlingAllowed
operator fun DomainVerificationUserSelection.component5() = hostToUserSelectionMap

operator fun DomainVerificationPersistence.ReadResult.component1() = active
operator fun DomainVerificationPersistence.ReadResult.component2() = restored
+212 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.pm.test.domain.verify

import android.content.pm.domain.verify.DomainVerificationManager
import android.util.ArrayMap
import android.util.TypedXmlSerializer
import android.util.Xml
import com.android.server.pm.domain.verify.DomainVerificationPersistence
import com.android.server.pm.domain.verify.models.DomainVerificationPkgState
import com.android.server.pm.domain.verify.models.DomainVerificationStateMap
import com.android.server.pm.domain.verify.models.DomainVerificationUserState
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.util.UUID

class DomainVerificationPersistenceTest {

    companion object {
        private val PKG_PREFIX = DomainVerificationPersistenceTest::class.java.`package`!!.name
    }

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Test
    fun writeAndReadBackNormal() {
        val attached = DomainVerificationStateMap<DomainVerificationPkgState>().apply {
            mockPkgState(0).let { put(it.packageName, it.id, it) }
            mockPkgState(1).let { put(it.packageName, it.id, it) }
        }
        val pending = ArrayMap<String, DomainVerificationPkgState>().apply {
            mockPkgState(2).let { put(it.packageName, it) }
            mockPkgState(3).let { put(it.packageName, it) }
        }
        val restored = ArrayMap<String, DomainVerificationPkgState>().apply {
            mockPkgState(4).let { put(it.packageName, it) }
            mockPkgState(5).let { put(it.packageName, it) }
        }

        val file = writeXml {
            DomainVerificationPersistence.writeToXml(it, attached, pending, restored)
        }

        val xml = file.readText()

        val (readActive, readRestored) = file.inputStream()
                .use { DomainVerificationPersistence.readFromXml(Xml.resolvePullParser(it)) }

        assertWithMessage(xml).that(readActive.values)
                .containsExactlyElementsIn(attached.values() + pending.values)
        assertWithMessage(xml).that(readRestored.values).containsExactlyElementsIn(restored.values)
    }

    @Test
    fun readMalformed() {
        val stateZero = mockEmptyPkgState(0).apply {
            stateMap["example.com"] = DomainVerificationManager.STATE_SUCCESS
            stateMap["example.org"] = DomainVerificationManager.STATE_FIRST_VERIFIER_DEFINED

            // A domain without a written state falls back to default
            stateMap["missing-state.com"] = DomainVerificationManager.STATE_NO_RESPONSE

            userSelectionStates[1] = DomainVerificationUserState(1).apply {
                addHosts(setOf("example-user1.com", "example-user1.org"))
                isDisallowLinkHandling = false
            }
        }
        val stateOne = mockEmptyPkgState(1).apply {
            // It's valid to have a user selection without any autoVerify domains
            userSelectionStates[1] = DomainVerificationUserState(1).apply {
                addHosts(setOf("example-user1.com", "example-user1.org"))
                isDisallowLinkHandling = true
            }
        }

        // Also valid to have neither autoVerify domains nor any active user states
        val stateTwo = mockEmptyPkgState(2, hasAutoVerifyDomains = false)

        // language=XML
        val xml = """
            <?xml?>
            <domain-verifications>
                <active>
                    <package-state
                        packageName="${stateZero.packageName}"
                        id="${stateZero.id}"
                        >
                        <state>
                            <domain name="duplicate-takes-last.com" state="1"/>
                        </state>
                    </package-state>
                    <package-state
                        packageName="${stateZero.packageName}"
                        id="${stateZero.id}"
                        hasAutoVerifyDomains="true"
                        >
                        <state>
                            <domain name="example.com" state="${DomainVerificationManager.STATE_SUCCESS}"/>
                            <domain name="example.org" state="${DomainVerificationManager.STATE_FIRST_VERIFIER_DEFINED}"/>
                            <not-domain name="not-domain.com" state="1"/>
                            <domain name="missing-state.com"/>
                        </state>
                        <user-states>
                            <user-state userId="1" disallowLinkHandling="false">
                                <enabled-hosts>
                                    <host name="example-user1.com"/>
                                    <not-host name="not-host.com"/>
                                    <host/>
                                </enabled-hosts>
                                <enabled-hosts>
                                    <host name="example-user1.org"/>
                                </enabled-hosts>
                                <enabled-hosts/>
                            </user-state>
                            <user-state>
                                <enabled-hosts>
                                    <host name="no-user-id.com"/>
                                </enabled-hosts>
                            </user-state>
                        </user-states>
                    </package-state>
                </active>
                <not-active/>
                <restored>
                    <package-state
                        packageName="${stateOne.packageName}"
                        id="${stateOne.id}"
                        hasAutoVerifyDomains="true"
                        >
                        <state/>
                        <user-states>
                            <user-state userId="1" disallowLinkHandling="true">
                                <enabled-hosts>
                                    <host name="example-user1.com"/>
                                    <host name="example-user1.org"/>
                                </enabled-hosts>
                            </user-state>
                        </user-states>
                    </package-state>
                    <package-state packageName="${stateTwo.packageName}"/>
                    <package-state id="${stateTwo.id}"/>
                    <package-state
                        packageName="${stateTwo.packageName}"
                        id="${stateTwo.id}"
                        hasAutoVerifyDomains="false"
                        >
                        <state/>
                        <user-states/>
                    </package-state>
                </restore>
                <not-restored/>
            </domain-verifications>
        """.trimIndent()

        val (active, restored) = DomainVerificationPersistence
                .readFromXml(Xml.resolvePullParser(xml.byteInputStream()))

        assertThat(active.values).containsExactly(stateZero)
        assertThat(restored.values).containsExactly(stateOne, stateTwo)
    }

    private fun writeXml(block: (TypedXmlSerializer) -> Unit) = tempFolder.newFile()
            .apply {
                outputStream().use {
                    Xml.resolveSerializer(it)
                            .apply {
                                startDocument(null, true)
                                setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
                            }
                            .apply(block)
                            .endDocument()
                }
            }

    private fun mockEmptyPkgState(
        id: Int,
        hasAutoVerifyDomains: Boolean = true
    ): DomainVerificationPkgState {
        val pkgName = pkgName(id)
        val domainSetId = UUID(0L, id.toLong())
        return DomainVerificationPkgState(pkgName, domainSetId, hasAutoVerifyDomains)
    }

    private fun mockPkgState(id: Int) = mockEmptyPkgState(id).apply {
        stateMap["$packageName.com"] = id
        userSelectionStates[id] = DomainVerificationUserState(id).apply {
            addHosts(setOf("$packageName-user.com"))
            isDisallowLinkHandling = true
        }
    }

    private fun pkgName(id: Int) = "${PKG_PREFIX}.pkg$id"
}