/*
 * Copyright (C) 2017 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 android.os;

import android.content.Context;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Slog;

import libcore.io.IoUtils;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;

/**
 * Variant of {@link FileDescriptor} that allows its creator to revoke all
 * access to the underlying resource.
 * <p>
 * This is useful when the code that originally opened a file needs to strongly
 * assert that any clients are completely hands-off for security purposes.
 *
 * @hide
 */
public class RevocableFileDescriptor {
    private static final String TAG = "RevocableFileDescriptor";
    private static final boolean DEBUG = true;

    private FileDescriptor mInner;
    private ParcelFileDescriptor mOuter;

    private volatile boolean mRevoked;

    private ParcelFileDescriptor.OnCloseListener mOnCloseListener;

    /** {@hide} */
    public RevocableFileDescriptor() {
    }

    /**
     * Create an instance that references the given {@link File}.
     */
    public RevocableFileDescriptor(Context context, File file) throws IOException {
        try {
            init(context, Os.open(file.getAbsolutePath(),
                    OsConstants.O_CREAT | OsConstants.O_RDWR, 0700));
        } catch (ErrnoException e) {
            throw e.rethrowAsIOException();
        }
    }

    /**
     * Create an instance that references the given {@link FileDescriptor}.
     */
    public RevocableFileDescriptor(Context context, FileDescriptor fd) throws IOException {
        init(context, fd);
    }

    public RevocableFileDescriptor(Context context, FileDescriptor fd, Handler handler)
            throws IOException {
        init(context, fd, handler);
    }

    /** {@hide} */
    public void init(Context context, FileDescriptor fd) throws IOException {
        init(context, fd, null);
    }

    /** {@hide} */
    public void init(Context context, FileDescriptor fd, Handler handler) throws IOException {
        mInner = fd;
        StorageManager sm = context.getSystemService(StorageManager.class);
        if (handler != null) {
            mOuter = sm.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_WRITE, mCallback,
                    handler);
        } else {
            mOuter = sm.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_WRITE, mCallback);
        }
    }

    /**
     * Return a {@link ParcelFileDescriptor} which can safely be passed to an
     * untrusted process. After {@link #revoke()} is called, all operations will
     * fail with {@link OsConstants#EPERM}.
     */
    public ParcelFileDescriptor getRevocableFileDescriptor() {
        return mOuter;
    }

    /**
     * Revoke all future access to the {@link ParcelFileDescriptor} returned by
     * {@link #getRevocableFileDescriptor()}. From this point forward, all
     * operations will fail with {@link OsConstants#EPERM}.
     */
    public void revoke() {
        mRevoked = true;
        IoUtils.closeQuietly(mInner);
    }

    /**
     * Callback for indicating that {@link ParcelFileDescriptor} passed to the client
     * process ({@link #getRevocableFileDescriptor()}) has been closed.
     */
    public void addOnCloseListener(ParcelFileDescriptor.OnCloseListener onCloseListener) {
        mOnCloseListener = onCloseListener;
    }

    public boolean isRevoked() {
        return mRevoked;
    }

    private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
        private void checkRevoked() throws ErrnoException {
            if (mRevoked) {
                throw new ErrnoException(TAG, OsConstants.EPERM);
            }
        }

        @Override
        public long onGetSize() throws ErrnoException {
            checkRevoked();
            return Os.fstat(mInner).st_size;
        }

        @Override
        public int onRead(long offset, int size, byte[] data) throws ErrnoException {
            checkRevoked();
            int n = 0;
            while (n < size) {
                try {
                    n += Os.pread(mInner, data, n, size - n, offset + n);
                    break;
                } catch (InterruptedIOException e) {
                    n += e.bytesTransferred;
                }
            }
            return n;
        }

        @Override
        public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
            checkRevoked();
            int n = 0;
            while (n < size) {
                try {
                    n += Os.pwrite(mInner, data, n, size - n, offset + n);
                    break;
                } catch (InterruptedIOException e) {
                    n += e.bytesTransferred;
                }
            }
            return n;
        }

        @Override
        public void onFsync() throws ErrnoException {
            if (DEBUG) Slog.v(TAG, "onFsync()");
            checkRevoked();
            Os.fsync(mInner);
        }

        @Override
        public void onRelease() {
            if (DEBUG) Slog.v(TAG, "onRelease()");
            mRevoked = true;
            IoUtils.closeQuietly(mInner);
            if (mOnCloseListener != null) {
                mOnCloseListener.onClose(null);
            }
        }
    };
}
