/*
 * This file is part of LSPosed.
 *
 * LSPosed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LSPosed is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2020 EdXposed Contributors
 * Copyright (C) 2021 LSPosed Contributors
 */

package de.robv.android.xposed.services;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import de.robv.android.xposed.SELinuxHelper;

/**
 * General definition of a file access service provided by the Xposed framework.
 *
 * <p>References to a concrete subclass should generally be retrieved from {@link SELinuxHelper}.
 */
public abstract class BaseService {
    /**
     * Flag for {@link #checkFileAccess}: Read access.
     */
    public static final int R_OK = 4;
    /**
     * Flag for {@link #checkFileAccess}: Write access.
     */
    public static final int W_OK = 2;
    /**
     * Flag for {@link #checkFileAccess}: Executable access.
     */
    public static final int X_OK = 1;
    /**
     * Flag for {@link #checkFileAccess}: File/directory exists.
     */
    public static final int F_OK = 0;

    /**
     * Checks whether the services accesses files directly (instead of using IPC).
     *
     * @return {@code true} in case direct access is possible.
     */
    public boolean hasDirectFileAccess() {
        return false;
    }

    /**
     * Check whether a file is accessible. SELinux might enforce stricter checks.
     *
     * @param filename The absolute path of the file to check.
     * @param mode     The mode for POSIX's {@code access()} function.
     * @return The result of the {@code access()} function.
     */
    public abstract boolean checkFileAccess(String filename, int mode);

    /**
     * Check whether a file exists.
     *
     * @param filename The absolute path of the file to check.
     * @return The result of the {@code access()} function.
     */
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public boolean checkFileExists(String filename) {
        return checkFileAccess(filename, F_OK);
    }

    /**
     * Determine the size and modification time of a file.
     *
     * @param filename The absolute path of the file to check.
     * @return A {@link FileResult} object holding the result.
     * @throws IOException In case an error occurred while retrieving the information.
     */
    public abstract FileResult statFile(String filename) throws IOException;

    /**
     * Determine the size time of a file.
     *
     * @param filename The absolute path of the file to check.
     * @return The file size.
     * @throws IOException In case an error occurred while retrieving the information.
     */
    public long getFileSize(String filename) throws IOException {
        return statFile(filename).size;
    }

    /**
     * Determine the size time of a file.
     *
     * @param filename The absolute path of the file to check.
     * @return The file modification time.
     * @throws IOException In case an error occurred while retrieving the information.
     */
    public long getFileModificationTime(String filename) throws IOException {
        return statFile(filename).mtime;
    }

    /**
     * Read a file into memory.
     *
     * @param filename The absolute path of the file to read.
     * @return A {@code byte} array with the file content.
     * @throws IOException In case an error occurred while reading the file.
     */
    public abstract byte[] readFile(String filename) throws IOException;

    /**
     * Read a file into memory, but only if it has changed since the last time.
     *
     * @param filename     The absolute path of the file to read.
     * @param previousSize File size of last read.
     * @param previousTime File modification time of last read.
     * @return A {@link FileResult} object holding the result.
     * <p>The {@link FileResult#content} field might be {@code null} if the file
     * is unmodified ({@code previousSize} and {@code previousTime} are still valid).
     * @throws IOException In case an error occurred while reading the file.
     */
    public abstract FileResult readFile(String filename, long previousSize, long previousTime) throws IOException;

    /**
     * Read a file into memory, optionally only if it has changed since the last time.
     *
     * @param filename     The absolute path of the file to read.
     * @param offset       Number of bytes to skip at the beginning of the file.
     * @param length       Number of bytes to read (0 means read to end of file).
     * @param previousSize Optional: File size of last read.
     * @param previousTime Optional: File modification time of last read.
     * @return A {@link FileResult} object holding the result.
     * <p>The {@link FileResult#content} field might be {@code null} if the file
     * is unmodified ({@code previousSize} and {@code previousTime} are still valid).
     * @throws IOException In case an error occurred while reading the file.
     */
    public abstract FileResult readFile(String filename, int offset, int length,
                                        long previousSize, long previousTime) throws IOException;

    /**
     * Get a stream to the file content.
     * Depending on the service, it may or may not be read completely into memory.
     *
     * @param filename The absolute path of the file to read.
     * @return An {@link InputStream} to the file content.
     * @throws IOException In case an error occurred while reading the file.
     */
    public InputStream getFileInputStream(String filename) throws IOException {
        return new ByteArrayInputStream(readFile(filename));
    }

    /**
     * Get a stream to the file content, but only if it has changed since the last time.
     * Depending on the service, it may or may not be read completely into memory.
     *
     * @param filename     The absolute path of the file to read.
     * @param previousSize Optional: File size of last read.
     * @param previousTime Optional: File modification time of last read.
     * @return A {@link FileResult} object holding the result.
     * <p>The {@link FileResult#stream} field might be {@code null} if the file
     * is unmodified ({@code previousSize} and {@code previousTime} are still valid).
     * @throws IOException In case an error occurred while reading the file.
     */
    public FileResult getFileInputStream(String filename, long previousSize, long previousTime) throws IOException {
        FileResult result = readFile(filename, previousSize, previousTime);
        if (result.content == null)
            return result;
        return new FileResult(new ByteArrayInputStream(result.content), result.size, result.mtime);
    }


    // ----------------------------------------------------------------------------
    /*package*/ BaseService() {
    }

    /*package*/
    static void ensureAbsolutePath(String filename) {
        if (!filename.startsWith("/")) {
            throw new IllegalArgumentException("Only absolute filenames are allowed: " + filename);
        }
    }

    /*package*/
    static void throwCommonIOException(int errno, String errorMsg, String filename, String defaultText) throws IOException {
        switch (errno) {
            case 1: // EPERM
            case 13: // EACCES
                throw new FileNotFoundException(errorMsg != null ? errorMsg : "Permission denied: " + filename);
            case 2: // ENOENT
                throw new FileNotFoundException(errorMsg != null ? errorMsg : "No such file or directory: " + filename);
            case 12: // ENOMEM
                throw new OutOfMemoryError(errorMsg);
            case 21: // EISDIR
                throw new FileNotFoundException(errorMsg != null ? errorMsg : "Is a directory: " + filename);
            default:
                throw new IOException(errorMsg != null ? errorMsg : "Error " + errno + defaultText + filename);
        }
    }
}
