blob: 4b0b675937a327b5305ae21c7acc9653a654aba1 [file] [log] [blame]
/*
* Copyright (C) 2015 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.zip;
import com.android.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* The ZipField class represents a field in a record in a zip file. Zip files are made with records
* that have fields. This class makes it easy to read, write and verify field values.
* <p>
* There are two main types of fields: 2-byte fields and 4-byte fields. We represent each one as
* a subclass of {@code ZipField}, {@code F2} for the 2-byte field and {@code F4} for the 4-byte
* field. Because Java's {@code int} data type is guaranteed to be 4-byte, all methods use Java's
* native {@link int} as data type.
* <p>
* For each field we can either read, write or verify. Verification is used for fields whose value
* we know. Some fields, <em>e.g.</em> signature fields, have fixed value. Other fields have
* variable values, but in some situations we know which value they have. For example, the last
* modification time of a file's local header will have to match the value of the file's
* modification time as stored in the central directory.
* <p>
* Because records are compact, <em>i.e.</em> fields are stored sequentially with no empty spaces,
* fields are generally created in the sequence they exist and the end offset of a field is used
* as the offset of the next one. The end of a field can be obtained by invoking
* {@link #endOffset()}. This allows creating fields in sequence without doing offset computation:
* <pre>
* ZipField.F2 firstField = new ZipField.F2(0, "First field");
* ZipField.F4 secondField = new ZipField(firstField.endOffset(), "Second field");
* </pre>
*/
abstract class ZipField {
/**
* Field name. Used for providing (more) useful error messages.
*/
@Nonnull
private final String name;
/**
* Offset of the file in the record.
*/
protected final int offset;
/**
* Size of the field. Only 2 or 4 allowed.
*/
private final int size;
/**
* If a fixed value exists for the field, then this attribute will contain that value.
*/
@Nullable
private final Long expected;
/**
* All invariants that this field must verify.
*/
@Nonnull
private Set<ZipFieldInvariant> invariants;
/**
* Creates a new field that does not contain a fixed value.
*
* @param offset the field's offset in the record
* @param size the field size
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
ZipField(int offset, int size, @Nonnull String name, ZipFieldInvariant... invariants) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
this.name = name;
this.offset = offset;
this.size = size;
expected = null;
this.invariants = Sets.newHashSet(invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param size the field size
* @param expected the expected field value
* @param name the field's name
*/
ZipField(int offset, int size, long expected, @Nonnull String name) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
this.name = name;
this.offset = offset;
this.size = size;
this.expected = expected;
invariants = Sets.newHashSet();
}
/**
* Checks whether a value verifies the field's invariants. Nothing happens if the value verifies
* the invariants.
*
* @param value the value
* @throws IOException the invariants are not verified
*/
private void checkVerifiesInvariants(long value) throws IOException {
for (ZipFieldInvariant invariant : invariants) {
if (!invariant.isValid(value)) {
throw new IOException("Value " + value + " of field " + name + " is invalid "
+ "(fails '" + invariant.getName() + "').");
}
}
}
/**
* Advances the position in the provided byte buffer by the size of this field.
*
* @param bytes the byte buffer; at the end of the method its position will be greater by
* the size of this field
* @throws IOException failed to advance the buffer
*/
void skip(@Nonnull ByteBuffer bytes) throws IOException {
if (bytes.remaining() < size) {
throw new IOException("Cannot skip field " + name + " because only "
+ bytes.remaining() + " remain in the buffer.");
}
bytes.position(bytes.position() + size);
}
/**
* Reads a field value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer
* will be positioned at the first byte after the field
* @return the value of the field
* @throws IOException failed to read the field
*/
long read(@Nonnull ByteBuffer bytes) throws IOException {
if (bytes.remaining() < size) {
throw new IOException("Cannot skip field " + name + " because only "
+ bytes.remaining() + " remain in the buffer.");
}
bytes.order(ByteOrder.LITTLE_ENDIAN);
long r;
if (size == 2) {
r = LittleEndianUtils.readUnsigned2Le(bytes);
} else {
r = LittleEndianUtils.readUnsigned4Le(bytes);
}
checkVerifiesInvariants(r);
return r;
}
/**
* Verifies that the field at the current buffer position has the expected value. The field
* must have been created with the constructor that defines the expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer
* will be positioned at the first byte after the field
* @throws IOException failed to read the field or the field does not have the expected value
*/
void verify(@Nonnull ByteBuffer bytes) throws IOException {
verify(bytes, null);
}
/**
* Verifies that the field at the current buffer position has the expected value. The field
* must have been created with the constructor that defines the expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer
* will be positioned at the first byte after the field
* @param verifyLog if non-{@code null}, will log the verification error
* @throws IOException failed to read the data or the field does not have the expected value;
* only thrown if {@code verifyLog} is {@code null}
*/
void verify(@Nonnull ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException {
Preconditions.checkState(expected != null, "expected == null");
verify(bytes, expected, verifyLog);
}
/**
* Verifies that the field has an expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer
* will be positioned at the first byte after the field
* @param expected the value we expect the field to have; if this field has invariants, the
* value must verify them
* @throws IOException failed to read the data or the field does not have the expected value
*/
void verify(@Nonnull ByteBuffer bytes, long expected) throws IOException {
verify(bytes, expected, null);
}
/**
* Verifies that the field has an expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer
* will be positioned at the first byte after the field
* @param expected the value we expect the field to have; if this field has invariants, the
* value must verify them
* @param verifyLog if non-{@code null}, will log the verification error
* @throws IOException failed to read the data or the field does not have the expected value;
* only thrown if {@code verifyLog} is {@code null}
*/
void verify(
@Nonnull ByteBuffer bytes,
long expected,
@Nullable VerifyLog verifyLog) throws IOException {
checkVerifiesInvariants(expected);
long r = read(bytes);
if (r != expected) {
String error =
String.format(
"Incorrect value for field '%s': value is %s but %s expected.",
name,
r,
expected);
if (verifyLog == null) {
throw new IOException(error);
} else {
verifyLog.log(error);
}
}
}
/**
* Writes the value of the field.
*
* @param output where to write the field; the field will be written at the current position
* of the buffer
* @param value the value to write
* @throws IOException failed to write the value in the stream
*/
void write(@Nonnull ByteBuffer output, long value) throws IOException {
checkVerifiesInvariants(value);
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
if (size == 2) {
Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value);
LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value));
} else {
Verify.verify(size == 4);
Preconditions.checkArgument(value <= 0x00000000ffffffffL,
"value (%s) > 0x00000000ffffffffL", value);
LittleEndianUtils.writeUnsigned4Le(output, value);
}
}
/**
* Writes the value of the field. The field must have an expected value set in the constructor.
*
* @param output where to write the field; the field will be written at the current position
* of the buffer
* @throws IOException failed to write the value in the stream
*/
void write(@Nonnull ByteBuffer output) throws IOException {
Preconditions.checkState(expected != null, "expected == null");
write(output, expected);
}
/**
* Obtains the offset at which the field starts.
*
* @return the start offset
*/
int offset() {
return offset;
}
/**
* Obtains the offset at which the field ends. This is the exact offset at which the next
* field starts.
*
* @return the end offset
*/
int endOffset() {
return offset + size;
}
/**
* Concrete implementation of {@link ZipField} that represents a 2-byte field.
*/
static class F2 extends ZipField {
/**
* Creates a new field.
*
* @param offset the field's offset in the record
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
F2(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
super(offset, 2, name, invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param expected the expected field value
* @param name the field's name
*/
F2(int offset, long expected, @Nonnull String name) {
super(offset, 2, expected, name);
}
}
/**
* Concrete implementation of {@link ZipField} that represents a 4-byte field.
*/
static class F4 extends ZipField {
/**
* Creates a new field.
*
* @param offset the field's offset in the record
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
F4(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
super(offset, 4, name, invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param expected the expected field value
* @param name the field's name
*/
F4(int offset, long expected, @Nonnull String name) {
super(offset, 4, expected, name);
}
}
}