blob: 33c47c65c09a7fa03ff6c4307275cab613316f29 [file] [log] [blame]
/*
* Copyright (C) 2016 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.apkzlib.sign;
import com.android.apkzlib.utils.CachedSupplier;
import com.android.apkzlib.utils.IOExceptionRunnable;
import com.android.apkzlib.zfile.ManifestAttributes;
import com.android.apkzlib.zip.StoredEntry;
import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Extension to {@link ZFile} that will generate a manifest. The extension will register
* automatically with the {@link ZFile}.
*
* <p>Creating this extension will ensure a manifest for the zip exists.
* This extension will generate a manifest if one does not exist and will update an existing
* manifest, if one does exist. The extension will also provide access to the manifest so that
* others may update the manifest.
*
* <p>Apart from standard manifest elements, this extension does not handle any particular manifest
* features such as signing or adding custom attributes. It simply generates a plain manifest and
* provides infrastructure so that other extensions can add data in the manifest.
*
* <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
* notification is received, meaning all manifest manipulation is done in-memory.
*/
public class ManifestGenerationExtension {
/**
* Name of META-INF directory.
*/
private static final String META_INF_DIR = "META-INF";
/**
* Name of the manifest file.
*/
static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
/**
* Who should be reported as the manifest builder.
*/
@Nonnull
private final String builtBy;
/**
* Who should be reported as the manifest creator.
*/
@Nonnull
private final String createdBy;
/**
* The file this extension is attached to. {@code null} if not yet registered.
*/
@Nullable
private ZFile zFile;
/**
* The zip file's manifest.
*/
@Nonnull
private final Manifest manifest;
/**
* Byte representation of the manifest. There is no guarantee that two writes of the java's
* {@code Manifest} object will yield the same byte array (there is no guaranteed order
* of entries in the manifest).
*
* <p>Because we need the byte representation of the manifest to be stable if there are
* no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte
* representation every time we need the byte representation.
*
* <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
* and will cache it. All further requests of the manifest's byte representation will
* receive the same byte array.
*/
@Nonnull
private CachedSupplier<byte[]> manifestBytes;
/**
* Has the current manifest been changed and not yet flushed? If {@link #dirty} is
* {@code true}, then {@link #manifestBytes} should not be valid. This means that
* marking the manifest as dirty should also invalidate {@link #manifestBytes}. To avoid
* breaking the invariant, instead of setting {@link #dirty}, {@link #markDirty()} should
* be called.
*/
private boolean dirty;
/**
* The extension to register with the {@link ZFile}. {@code null} if not registered.
*/
@Nullable
private ZFileExtension extension;
/**
* Creates a new extension. This will not register the extension with the provided
* {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used.
*
* @param builtBy who built the manifest?
* @param createdBy who created the manifest?
*/
public ManifestGenerationExtension(@Nonnull String builtBy, @Nonnull String createdBy) {
this.builtBy = builtBy;
this.createdBy = createdBy;
manifest = new Manifest();
dirty = false;
manifestBytes = new CachedSupplier<>(() -> {
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
try {
manifest.write(outBytes);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return outBytes.toByteArray();
});
}
/**
* Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last
* read and/or written.
*/
private void markDirty() {
dirty = true;
manifestBytes.reset();
}
/**
* Registers the extension with the {@link ZFile} provided in the constructor.
*
* @param zFile the zip file to add the extension to
* @throws IOException failed to analyze the zip
*/
public void register(@Nonnull ZFile zFile) throws IOException {
Preconditions.checkState(extension == null, "register() has already been invoked.");
this.zFile = zFile;
rebuildManifest();
extension = new ZFileExtension() {
@Nullable
@Override
public IOExceptionRunnable beforeUpdate() {
return ManifestGenerationExtension.this::updateManifest;
}
};
this.zFile.addZFileExtension(extension);
}
/**
* Rebuilds the zip file's manifest, if it needs changes.
*/
private void rebuildManifest() throws IOException {
Verify.verifyNotNull(zFile, "zFile == null");
StoredEntry manifestEntry = zFile.get(MANIFEST_NAME);
if (manifestEntry != null) {
/*
* Read the manifest entry in the zip file. Make sure we store these byte sequence
* because writing the manifest may not generate the same byte sequence, which may
* trigger an unnecessary re-sign of the jar.
*/
manifest.clear();
byte[] manifestBytes = manifestEntry.read();
manifest.read(new ByteArrayInputStream(manifestBytes));
this.manifestBytes.precomputed(manifestBytes);
}
Attributes mainAttributes = manifest.getMainAttributes();
String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
if (currentVersion == null) {
setMainAttribute(
ManifestAttributes.MANIFEST_VERSION,
ManifestAttributes.CURRENT_MANIFEST_VERSION);
} else {
if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
throw new IOException("Unsupported manifest version: " + currentVersion + ".");
}
}
/*
* We "blindly" override all other main attributes.
*/
setMainAttribute(ManifestAttributes.BUILT_BY, builtBy);
setMainAttribute(ManifestAttributes.CREATED_BY, createdBy);
}
/**
* Sets the value of a main attribute.
*
* @param attribute the attribute
* @param value the value
*/
private void setMainAttribute(@Nonnull String attribute, @Nonnull String value) {
Attributes mainAttributes = manifest.getMainAttributes();
String current = mainAttributes.getValue(attribute);
if (!value.equals(current)) {
mainAttributes.putValue(attribute, value);
markDirty();
}
}
/**
* Updates the manifest in the zip file, if it has been changed.
*
* @throws IOException failed to update the manifest
*/
private void updateManifest() throws IOException {
Verify.verifyNotNull(zFile, "zFile == null");
if (!dirty) {
return;
}
zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get()));
dirty = false;
}
}