blob: aa1c2539d0778817cd218980df46e9d0e1796a52 [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 libcore.timezone.testing;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helps with creating valid and invalid test data.
*/
public class ZoneInfoTestHelper {
private ZoneInfoTestHelper() {}
/**
* Constructs valid and invalid zic data for tests.
*/
public static class ZicDataBuilder {
private int magic = 0x545a6966; // Default, valid magic.
private long[] transitionTimes64Bit; // Time of each transition, one per transition.
private byte[] transitionTypes64Bit; // Type of each transition, one per transition.
private int[] isDsts; // Whether a type uses DST, one per type.
private int[] offsetsSeconds; // The UTC offset, one per type.
public ZicDataBuilder() {}
public ZicDataBuilder setMagic(int magic) {
this.magic = magic;
return this;
}
/**
* See {@link #setTransitions(long[][])} and {@link #setTypes(int[][])}.
*/
public ZicDataBuilder setTransitionsAndTypes(
long[][] transitionPairs, int[][] typePairs) {
setTransitions(transitionPairs);
setTypes(typePairs);
return this;
}
/**
* Sets transition information using an array of pairs of longs. e.g.
*
* new long[][] {
* { transitionTimeSeconds1, typeIndex1 },
* { transitionTimeSeconds2, typeIndex1 },
* }
*/
public ZicDataBuilder setTransitions(long[][] transitionPairs) {
long[] transitions = new long[transitionPairs.length];
byte[] types = new byte[transitionPairs.length];
for (int i = 0; i < transitionPairs.length; i++) {
transitions[i] = transitionPairs[i][0];
types[i] = (byte) transitionPairs[i][1];
}
this.transitionTimes64Bit = transitions;
this.transitionTypes64Bit = types;
return this;
}
/**
* Sets transition information using an array of pairs of ints. e.g.
*
* new int[][] {
* { offsetSeconds1, typeIsDst1 },
* { offsetSeconds2, typeIsDst2 },
* }
*/
public ZicDataBuilder setTypes(int[][] typePairs) {
int[] isDsts = new int[typePairs.length];
int[] offsetSeconds = new int[typePairs.length];
for (int i = 0; i < typePairs.length; i++) {
offsetSeconds[i] = typePairs[i][0];
isDsts[i] = typePairs[i][1];
}
this.isDsts = isDsts;
this.offsetsSeconds = offsetSeconds;
return this;
}
/** Initializes to a minimum viable ZoneInfo data. */
public ZicDataBuilder initializeToValid() {
setTransitions(new long[0][0]);
setTypes(new int[][] {
{ 3600, 0}
});
return this;
}
public byte[] build() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Compute the 32-bit transitions / types.
List<Integer> transitionTimes32Bit = new ArrayList<>();
List<Byte> transitionTypes32Bit = new ArrayList<>();
if (transitionTimes64Bit.length > 0) {
// Skip transitions < Integer.MIN_VALUE.
int i = 0;
while (i < transitionTimes64Bit.length
&& transitionTimes64Bit[i] < Integer.MIN_VALUE) {
i++;
}
// If we skipped any, add a transition at Integer.MIN_VALUE like zic does.
if (i > 0) {
transitionTimes32Bit.add(Integer.MIN_VALUE);
transitionTypes32Bit.add(transitionTypes64Bit[i - 1]);
}
// Copy remaining transitions / types that fit in the 32-bit range.
while (i < transitionTimes64Bit.length
&& transitionTimes64Bit[i] <= Integer.MAX_VALUE) {
transitionTimes32Bit.add((int) transitionTimes64Bit[i]);
transitionTypes32Bit.add(transitionTypes64Bit[i]);
i++;
}
}
int tzh_timecnt_32 = transitionTimes32Bit.size();
int tzh_timecnt_64 = transitionTimes64Bit.length;
int tzh_typecnt = offsetsSeconds.length;
int tzh_charcnt = 0;
int tzh_leapcnt = 0;
int tzh_ttisstdcnt = 0;
int tzh_ttisgmtcnt = 0;
// Write 32-bit data.
writeHeader(baos, magic, tzh_timecnt_32, tzh_typecnt, tzh_charcnt, tzh_leapcnt,
tzh_ttisstdcnt, tzh_ttisgmtcnt);
writeIntList(baos, transitionTimes32Bit);
writeByteList(baos, transitionTypes32Bit);
writeTypes(baos, offsetsSeconds, isDsts);
writeTrailingUnusued32BitData(baos, tzh_charcnt, tzh_leapcnt, tzh_ttisstdcnt,
tzh_ttisgmtcnt);
// 64-bit data.
writeHeader(baos, magic, tzh_timecnt_64, tzh_typecnt, tzh_charcnt, tzh_leapcnt,
tzh_ttisstdcnt, tzh_ttisgmtcnt);
writeLongArray(baos, transitionTimes64Bit);
writeByteArray(baos, transitionTypes64Bit);
writeTypes(baos, offsetsSeconds, isDsts);
return baos.toByteArray();
}
private static void writeTypes(
ByteArrayOutputStream baos, int[] offsetsSeconds, int[] isDsts) {
// Offset / DST
for (int i = 0; i < offsetsSeconds.length; i++) {
writeInt(baos, offsetsSeconds[i]);
writeByte(baos, isDsts[i]);
// Unused data on Android (abbreviation list index).
writeByte(baos, 0);
}
}
/**
* Writes the data after types information Android doesn't currently use but is needed so
* that the start of the 64-bit data can be found.
*/
private static void writeTrailingUnusued32BitData(
ByteArrayOutputStream baos, int tzh_charcnt, int tzh_leapcnt,
int tzh_ttisstdcnt, int tzh_ttisgmtcnt) {
// Time zone abbreviations text.
writeByteArray(baos, new byte[tzh_charcnt]);
// tzh_leapcnt repetitions of leap second transition time + correction.
int leapInfoSize = 4 + 4;
writeByteArray(baos, new byte[tzh_leapcnt * leapInfoSize]);
// tzh_ttisstdcnt bytes
writeByteArray(baos, new byte[tzh_ttisstdcnt]);
// tzh_ttisgmtcnt bytes
writeByteArray(baos, new byte[tzh_ttisgmtcnt]);
}
private static void writeHeader(ByteArrayOutputStream baos, int magic, int tzh_timecnt,
int tzh_typecnt, int tzh_charcnt, int tzh_leapcnt, int tzh_ttisstdcnt,
int tzh_ttisgmtcnt) {
// Magic number.
writeInt(baos, magic);
// tzh_version
writeByte(baos, '2');
// Write out the unused part of the header.
for (int i = 0; i < 15; ++i) {
baos.write(i);
}
// Write out the known header fields.
writeInt(baos, tzh_ttisgmtcnt);
writeInt(baos, tzh_ttisstdcnt);
writeInt(baos, tzh_leapcnt);
writeInt(baos, tzh_timecnt);
writeInt(baos, tzh_typecnt);
writeInt(baos, tzh_charcnt);
}
}
/**
* Constructs valid and invalid tzdata files for tests. See also ZoneCompactor class in
* system/timezone/zone_compactor which is the real thing.
*/
public static class TzDataBuilder {
private String headerMagic;
// A list is used in preference to a Map to allow simulation of badly ordered / duplicate
// IDs.
private List<ZicDatum> zicData = new ArrayList<>();
private Integer indexOffsetOverride;
private Integer dataOffsetOverride;
private Integer finalOffsetOverride;
public TzDataBuilder() {}
/** Sets the header. A valid header is in the form "tzdata2016g". */
public TzDataBuilder setHeaderMagic(String headerMagic) {
this.headerMagic = headerMagic;
return this;
}
public TzDataBuilder setIndexOffsetOverride(int indexOffset) {
this.indexOffsetOverride = indexOffset;
return this;
}
public TzDataBuilder setDataOffsetOverride(int dataOffset) {
this.dataOffsetOverride = dataOffset;
return this;
}
public TzDataBuilder setFinalOffsetOverride(int finalOffset) {
this.finalOffsetOverride = finalOffset;
return this;
}
/**
* Adds data for a new zone. These must be added in ID string order to generate
* a valid file.
*/
public TzDataBuilder addZicData(String id, byte[] data) {
zicData.add(new ZicDatum(id, data));
return this;
}
public TzDataBuilder initializeToValid() {
setHeaderMagic("tzdata9999a");
addZicData("Europe/Elbonia", new ZicDataBuilder().initializeToValid().build());
return this;
}
public TzDataBuilder clearZicData() {
zicData.clear();
return this;
}
public byte[] build() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] headerMagicBytes = headerMagic.getBytes(StandardCharsets.US_ASCII);
baos.write(headerMagicBytes, 0, headerMagicBytes.length);
baos.write(0);
// Write out the offsets for later manipulation.
int indexOffsetOffset = baos.size();
writeInt(baos, 0);
int dataOffsetOffset = baos.size();
writeInt(baos, 0);
int finalOffsetOffset = baos.size();
writeInt(baos, 0);
// Construct the data section in advance, so we know the offsets.
ByteArrayOutputStream dataBytes = new ByteArrayOutputStream();
Map<String, Integer> offsets = new HashMap<>();
for (ZicDatum datum : zicData) {
int offset = dataBytes.size();
offsets.put(datum.id, offset);
writeByteArray(dataBytes, datum.data);
}
int indexOffset = baos.size();
// Write the index section.
for (ZicDatum zicDatum : zicData) {
// Write the ID.
String id = zicDatum.id;
byte[] idBytes = id.getBytes(StandardCharsets.US_ASCII);
byte[] paddedIdBytes = new byte[40];
System.arraycopy(idBytes, 0, paddedIdBytes, 0, idBytes.length);
writeByteArray(baos, paddedIdBytes);
// Write offset of zic data in the data section.
Integer offset = offsets.get(id);
writeInt(baos, offset);
// Write the length of the zic data.
writeInt(baos, zicDatum.data.length);
// Write a filler value (not used)
writeInt(baos, 0);
}
// Write the data section.
int dataOffset = baos.size();
writeByteArray(baos, dataBytes.toByteArray());
int finalOffset = baos.size();
byte[] bytes = baos.toByteArray();
setInt(bytes, indexOffsetOffset,
indexOffsetOverride != null ? indexOffsetOverride : indexOffset);
setInt(bytes, dataOffsetOffset,
dataOffsetOverride != null ? dataOffsetOverride : dataOffset);
setInt(bytes, finalOffsetOffset,
finalOffsetOverride != null ? finalOffsetOverride : finalOffset);
return bytes;
}
private static class ZicDatum {
public final String id;
public final byte[] data;
ZicDatum(String id, byte[] data) {
this.id = id;
this.data = data;
}
}
}
static void writeByteArray(ByteArrayOutputStream baos, byte[] array) {
baos.write(array, 0, array.length);
}
static void writeByteList(ByteArrayOutputStream baos, List<Byte> list) {
for (byte value : list) {
baos.write(value);
}
}
static void writeByte(ByteArrayOutputStream baos, int value) {
baos.write(value);
}
static void writeLongArray(ByteArrayOutputStream baos, long[] array) {
for (long value : array) {
writeLong(baos, value);
}
}
static void writeIntList(ByteArrayOutputStream baos, List<Integer> list) {
for (int value : list) {
writeInt(baos, value);
}
}
static void writeInt(ByteArrayOutputStream os, int value) {
byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
writeByteArray(os, bytes);
}
static void writeLong(ByteArrayOutputStream os, long value) {
byte[] bytes = ByteBuffer.allocate(8).putLong(value).array();
writeByteArray(os, bytes);
}
static void setInt(byte[] bytes, int offset, int value) {
bytes[offset] = (byte) (value >>> 24);
bytes[offset + 1] = (byte) (value >>> 16);
bytes[offset + 2] = (byte) (value >>> 8);
bytes[offset + 3] = (byte) value;
}
}