/*
 * 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 com.android.server.backup.utils;

import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_VERSION;
import static com.android.server.backup.BackupManagerService.TAG;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Slog;
import android.util.StringBuilderPrinter;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Low-level utility methods for full backup.
 */
public class FullBackupUtils {
    /**
     * Reads data from pipe and writes it to the stream in chunks of up to 32KB.
     *
     * @param inPipe - pipe to read the data from.
     * @param out - stream to write the data to.
     * @throws IOException - in case of an error.
     */
    public static void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out)
            throws IOException {
        // We do not take close() responsibility for the pipe FD
        FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
        DataInputStream in = new DataInputStream(raw);

        byte[] buffer = new byte[32 * 1024];
        int chunkTotal;
        while ((chunkTotal = in.readInt()) > 0) {
            while (chunkTotal > 0) {
                int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal;
                int nRead = in.read(buffer, 0, toRead);
                if (nRead < 0) {
                    Slog.e(TAG, "Unexpectedly reached end of file while reading data");
                    throw new EOFException();
                }
                out.write(buffer, 0, nRead);
                chunkTotal -= nRead;
            }
        }
    }

    /**
     * Writes app manifest to the given manifest file.
     *
     * @param pkg - app package, which manifest to write.
     * @param packageManager - {@link PackageManager} instance.
     * @param manifestFile - target manifest file.
     * @param withApk - whether include apk or not.
     * @param withWidgets - whether to write widgets data.
     * @throws IOException - in case of an error.
     */
    // TODO: withWidgets is not used, decide whether it is needed.
    public static void writeAppManifest(PackageInfo pkg, PackageManager packageManager,
            File manifestFile, boolean withApk, boolean withWidgets) throws IOException {
        // Manifest format. All data are strings ending in LF:
        //     BACKUP_MANIFEST_VERSION, currently 1
        //
        // Version 1:
        //     package name
        //     package's versionCode
        //     platform versionCode
        //     getInstallerPackageName() for this package (maybe empty)
        //     boolean: "1" if archive includes .apk; any other string means not
        //     number of signatures == N
        // N*:    signature byte array in ascii format per Signature.toCharsString()
        StringBuilder builder = new StringBuilder(4096);
        StringBuilderPrinter printer = new StringBuilderPrinter(builder);

        printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
        printer.println(pkg.packageName);
        printer.println(Long.toString(pkg.getLongVersionCode()));
        printer.println(Integer.toString(Build.VERSION.SDK_INT));

        String installerName = packageManager.getInstallerPackageName(pkg.packageName);
        printer.println((installerName != null) ? installerName : "");

        printer.println(withApk ? "1" : "0");
        if (pkg.signatures == null) {
            printer.println("0");
        } else {
            printer.println(Integer.toString(pkg.signatures.length));
            for (Signature sig : pkg.signatures) {
                printer.println(sig.toCharsString());
            }
        }

        FileOutputStream outstream = new FileOutputStream(manifestFile);
        outstream.write(builder.toString().getBytes());
        outstream.close();

        // We want the manifest block in the archive stream to be idempotent:
        // each time we generate a backup stream for the app, we want the manifest
        // block to be identical.  The underlying tar mechanism sees it as a file,
        // though, and will propagate its mtime, causing the tar header to vary.
        // Avoid this problem by pinning the mtime to zero.
        manifestFile.setLastModified(0);
    }
}
