/*
 * Copyright (C) 2018 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.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import libcore.io.IoUtils;
import libcore.util.EmptyArray;

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

/**
 * Variant of {@link FileDescriptor} that allows its creator to specify regions
 * that should be redacted.
 *
 * @hide
 */
public class RedactingFileDescriptor {
    private static final String TAG = "RedactingFileDescriptor";
    private static final boolean DEBUG = true;

    private volatile long[] mRedactRanges;
    private volatile long[] mFreeOffsets;

    private FileDescriptor mInner = null;
    private ParcelFileDescriptor mOuter = null;

    private RedactingFileDescriptor(
            Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
            throws IOException {
        mRedactRanges = checkRangesArgument(redactRanges);
        mFreeOffsets = freeOffsets;

        try {
            try {
                mInner = Os.open(file.getAbsolutePath(),
                        FileUtils.translateModePfdToPosix(mode), 0);
                mOuter = context.getSystemService(StorageManager.class)
                        .openProxyFileDescriptor(mode, mCallback);
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
        } catch (IOException e) {
            IoUtils.closeQuietly(mInner);
            IoUtils.closeQuietly(mOuter);
            throw e;
        }
    }

    private static long[] checkRangesArgument(long[] ranges) {
        if (ranges.length % 2 != 0) {
            throw new IllegalArgumentException();
        }
        for (int i = 0; i < ranges.length - 1; i += 2) {
            if (ranges[i] > ranges[i + 1]) {
                throw new IllegalArgumentException();
            }
        }
        return ranges;
    }

    /**
     * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
     * that offers a redacted view of the underlying data. If a redacted region
     * is written to, the newly written data can be read back correctly instead
     * of continuing to be redacted.
     *
     * @param file The underlying file to open.
     * @param mode The {@link ParcelFileDescriptor} mode to open with.
     * @param redactRanges List of file ranges that should be redacted, stored
     *            as {@code [start1, end1, start2, end2, ...]}. Start values are
     *            inclusive and end values are exclusive.
     * @param freePositions List of file offsets at which the four byte value 'free' should be
     *            written instead of zeros within parts of the file covered by {@code redactRanges}.
     *            Non-redacted bytes will not be modified even if covered by a 'free'. This is
     *            useful for overwriting boxes in ISOBMFF files with padding data.
     */
    public static ParcelFileDescriptor open(Context context, File file, int mode,
            long[] redactRanges, long[] freePositions) throws IOException {
        return new RedactingFileDescriptor(context, file, mode, redactRanges, freePositions).mOuter;
    }

    /**
     * Update the given ranges argument to remove any references to the given
     * offset and length. This is typically used when a caller has written over
     * a previously redacted region.
     */
    @VisibleForTesting
    public static long[] removeRange(long[] ranges, long start, long end) {
        if (start == end) {
            return ranges;
        } else if (start > end) {
            throw new IllegalArgumentException();
        }

        long[] res = EmptyArray.LONG;
        for (int i = 0; i < ranges.length; i += 2) {
            if (start <= ranges[i] && end >= ranges[i + 1]) {
                // Range entirely covered; remove it
            } else if (start >= ranges[i] && end <= ranges[i + 1]) {
                // Range partially covered; punch a hole
                res = Arrays.copyOf(res, res.length + 4);
                res[res.length - 4] = ranges[i];
                res[res.length - 3] = start;
                res[res.length - 2] = end;
                res[res.length - 1] = ranges[i + 1];
            } else {
                // Range might covered; adjust edges if needed
                res = Arrays.copyOf(res, res.length + 2);
                if (end >= ranges[i] && end <= ranges[i + 1]) {
                    res[res.length - 2] = Math.max(ranges[i], end);
                } else {
                    res[res.length - 2] = ranges[i];
                }
                if (start >= ranges[i] && start <= ranges[i + 1]) {
                    res[res.length - 1] = Math.min(ranges[i + 1], start);
                } else {
                    res[res.length - 1] = ranges[i + 1];
                }
            }
        }
        return res;
    }

    private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
        @Override
        public long onGetSize() throws ErrnoException {
            return Os.fstat(mInner).st_size;
        }

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

            // Redact any relevant ranges before returning
            final long[] ranges = mRedactRanges;
            for (int i = 0; i < ranges.length; i += 2) {
                final long start = Math.max(offset, ranges[i]);
                final long end = Math.min(offset + size, ranges[i + 1]);
                for (long j = start; j < end; j++) {
                    data[(int) (j - offset)] = 0;
                }
                // Overwrite data at 'free' offsets within the redaction ranges.
                for (long freeOffset : mFreeOffsets) {
                    final long freeEnd = freeOffset + 4;
                    final long redactFreeStart = Math.max(freeOffset, start);
                    final long redactFreeEnd = Math.min(freeEnd, end);
                    for (long j = redactFreeStart; j < redactFreeEnd; j++) {
                        data[(int) (j - offset)] = (byte) "free".charAt((int) (j - freeOffset));
                    }
                }
            }
            return n;
        }

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

            // Clear any relevant redaction ranges before returning, since the
            // writer should have access to see the data they just overwrote
            mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
            return n;
        }

        @Override
        public void onFsync() throws ErrnoException {
            Os.fsync(mInner);
        }

        @Override
        public void onRelease() {
            if (DEBUG) Slog.v(TAG, "onRelease()");
            IoUtils.closeQuietly(mInner);
        }
    };
}
