blob: 75ddc661153ef969b491a7cff262a301e9b8ba3e [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.tzdata.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 Integer transitionCountOverride; // Used to override the correct transition count.
private int[] transitionTimes; // Time of each transition, one per transition.
private byte[] transitionTypes; // Type of each transition, one per transition.
private Integer typesCountOverride; // Used to override the correct type count.
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;
}
public ZicDataBuilder setTypeCountOverride(int typesCountOverride) {
this.typesCountOverride = typesCountOverride;
return this;
}
public ZicDataBuilder setTransitionCountOverride(int transitionCountOverride) {
this.transitionCountOverride = transitionCountOverride;
return this;
}
/**
* See {@link #setTransitions(int[][])} and {@link #setTypes(int[][])}.
*/
public ZicDataBuilder setTransitionsAndTypes(
int[][] transitionPairs, int[][] typePairs) {
setTransitions(transitionPairs);
setTypes(typePairs);
return this;
}
/**
* Sets transition information using an array of pairs of ints. e.g.
*
* new int[][] {
* { transitionTimeSeconds1, typeIndex1 },
* { transitionTimeSeconds2, typeIndex1 },
* }
*/
public ZicDataBuilder setTransitions(int[][] transitionPairs) {
int[] transitions = new int[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.transitionTimes = transitions;
this.transitionTypes = types;
return this;
}
/**
* Sets transition information using an array of pairs of ints. e.g.
*
* new int[][] {
* { typeIsDst1, offsetSeconds1 },
* { typeIsDst2, offsetSeconds2 },
* }
*/
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 int[0][0]);
setTypes(new int[][] {
{ 3600, 0}
});
return this;
}
public byte[] build() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Magic number.
writeInt(baos, magic);
// Some useless stuff in the header.
for (int i = 0; i < 28; ++i) {
baos.write(i);
}
// Transition time count
int transitionsCount = transitionCountOverride != null
? transitionCountOverride : transitionTimes.length;
writeInt(baos, transitionsCount);
// Transition type count.
int typesCount = typesCountOverride != null
? typesCountOverride : offsetsSeconds.length;
writeInt(baos, typesCount);
// Useless stuff.
writeInt(baos, 0xdeadbeef);
// Transition time array, as ints.
writeIntArray(baos, transitionTimes);
// Transition type array.
writeByteArray(baos, transitionTypes);
// Offset / DST
for (int i = 0; i < offsetsSeconds.length; i++) {
writeInt(baos, offsetsSeconds[i]);
writeByte(baos, isDsts[i]);
// Useless stuff.
writeByte(baos, i);
}
return baos.toByteArray();
}
}
/**
* 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 String zoneTab;
private Integer indexOffsetOverride;
private Integer dataOffsetOverride;
private Integer zoneTabOffsetOverride;
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 setZoneTabOffsetOverride(int zoneTabOffset) {
this.zoneTabOffsetOverride = zoneTabOffset;
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 setZoneTab(String zoneTab) {
this.zoneTab = zoneTab;
return this;
}
public TzDataBuilder initializeToValid() {
setHeaderMagic("tzdata9999a");
addZicData("Europe/Elbonia", new ZicDataBuilder().initializeToValid().build());
setZoneTab("ZoneTab data");
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 zoneTabOffsetOffset = 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());
// Write the zoneTab section.
int zoneTabOffset = baos.size();
byte[] zoneTabBytes = zoneTab.getBytes(StandardCharsets.US_ASCII);
writeByteArray(baos, zoneTabBytes);
byte[] bytes = baos.toByteArray();
setInt(bytes, indexOffsetOffset,
indexOffsetOverride != null ? indexOffsetOverride : indexOffset);
setInt(bytes, dataOffsetOffset,
dataOffsetOverride != null ? dataOffsetOverride : dataOffset);
setInt(bytes, zoneTabOffsetOffset,
zoneTabOffsetOverride != null ? zoneTabOffsetOverride : zoneTabOffset);
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 writeByte(ByteArrayOutputStream baos, int value) {
baos.write(value);
}
static void writeIntArray(ByteArrayOutputStream baos, int[] array) {
for (int value : array) {
writeInt(baos, value);
}
}
static void writeInt(ByteArrayOutputStream os, int value) {
byte[] bytes = ByteBuffer.allocate(4).putInt(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;
}
}