1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package libcore.tzdata.testing; 17 18 import java.io.ByteArrayOutputStream; 19 import java.nio.ByteBuffer; 20 import java.nio.charset.StandardCharsets; 21 import java.util.ArrayList; 22 import java.util.HashMap; 23 import java.util.List; 24 import java.util.Map; 25 26 /** 27 * Helps with creating valid and invalid test data. 28 */ 29 public class ZoneInfoTestHelper { 30 31 private ZoneInfoTestHelper() {} 32 33 /** 34 * Constructs valid and invalid zic data for tests. 35 */ 36 public static class ZicDataBuilder { 37 38 private int magic = 0x545a6966; // Default, valid magic. 39 private Integer transitionCountOverride; // Used to override the correct transition count. 40 private int[] transitionTimes; // Time of each transition, one per transition. 41 private byte[] transitionTypes; // Type of each transition, one per transition. 42 private Integer typesCountOverride; // Used to override the correct type count. 43 private int[] isDsts; // Whether a type uses DST, one per type. 44 private int[] offsetsSeconds; // The UTC offset, one per type. 45 46 public ZicDataBuilder() {} 47 48 public ZicDataBuilder setMagic(int magic) { 49 this.magic = magic; 50 return this; 51 } 52 53 public ZicDataBuilder setTypeCountOverride(int typesCountOverride) { 54 this.typesCountOverride = typesCountOverride; 55 return this; 56 } 57 58 public ZicDataBuilder setTransitionCountOverride(int transitionCountOverride) { 59 this.transitionCountOverride = transitionCountOverride; 60 return this; 61 } 62 63 /** 64 * See {@link #setTransitions(int[][])} and {@link #setTypes(int[][])}. 65 */ 66 public ZicDataBuilder setTransitionsAndTypes( 67 int[][] transitionPairs, int[][] typePairs) { 68 setTransitions(transitionPairs); 69 setTypes(typePairs); 70 return this; 71 } 72 /** 73 * Sets transition information using an array of pairs of ints. e.g. 74 * 75 * new int[][] { 76 * { transitionTimeSeconds1, typeIndex1 }, 77 * { transitionTimeSeconds2, typeIndex1 }, 78 * } 79 */ 80 public ZicDataBuilder setTransitions(int[][] transitionPairs) { 81 int[] transitions = new int[transitionPairs.length]; 82 byte[] types = new byte[transitionPairs.length]; 83 for (int i = 0; i < transitionPairs.length; i++) { 84 transitions[i] = transitionPairs[i][0]; 85 types[i] = (byte) transitionPairs[i][1]; 86 } 87 this.transitionTimes = transitions; 88 this.transitionTypes = types; 89 return this; 90 } 91 92 /** 93 * Sets transition information using an array of pairs of ints. e.g. 94 * 95 * new int[][] { 96 * { typeIsDst1, offsetSeconds1 }, 97 * { typeIsDst2, offsetSeconds2 }, 98 * } 99 */ 100 public ZicDataBuilder setTypes(int[][] typePairs) { 101 int[] isDsts = new int[typePairs.length]; 102 int[] offsetSeconds = new int[typePairs.length]; 103 for (int i = 0; i < typePairs.length; i++) { 104 offsetSeconds[i] = typePairs[i][0]; 105 isDsts[i] = typePairs[i][1]; 106 } 107 this.isDsts = isDsts; 108 this.offsetsSeconds = offsetSeconds; 109 return this; 110 } 111 112 /** Initializes to a minimum viable ZoneInfo data. */ 113 public ZicDataBuilder initializeToValid() { 114 setTransitions(new int[0][0]); 115 setTypes(new int[][] { 116 { 3600, 0} 117 }); 118 return this; 119 } 120 121 public byte[] build() { 122 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 123 124 // Magic number. 125 writeInt(baos, magic); 126 127 // Some useless stuff in the header. 128 for (int i = 0; i < 28; ++i) { 129 baos.write(i); 130 } 131 132 // Transition time count 133 int transitionsCount = transitionCountOverride != null 134 ? transitionCountOverride : transitionTimes.length; 135 writeInt(baos, transitionsCount); 136 137 // Transition type count. 138 int typesCount = typesCountOverride != null 139 ? typesCountOverride : offsetsSeconds.length; 140 writeInt(baos, typesCount); 141 // Useless stuff. 142 writeInt(baos, 0xdeadbeef); 143 144 // Transition time array, as ints. 145 writeIntArray(baos, transitionTimes); 146 147 // Transition type array. 148 writeByteArray(baos, transitionTypes); 149 150 // Offset / DST 151 for (int i = 0; i < offsetsSeconds.length; i++) { 152 writeInt(baos, offsetsSeconds[i]); 153 writeByte(baos, isDsts[i]); 154 // Useless stuff. 155 writeByte(baos, i); 156 } 157 return baos.toByteArray(); 158 } 159 } 160 161 /** 162 * Constructs valid and invalid tzdata files for tests. See also ZoneCompactor class in 163 * external/icu which is the real thing. 164 */ 165 public static class TzDataBuilder { 166 167 private String headerMagic; 168 // A list is used in preference to a Map to allow simulation of badly ordered / duplicate 169 // IDs. 170 private List<ZicDatum> zicData = new ArrayList<>(); 171 private String zoneTab; 172 private Integer indexOffsetOverride; 173 private Integer dataOffsetOverride; 174 private Integer zoneTabOffsetOverride; 175 176 public TzDataBuilder() {} 177 178 /** Sets the header. A valid header is in the form "tzdata2016g". */ 179 public TzDataBuilder setHeaderMagic(String headerMagic) { 180 this.headerMagic = headerMagic; 181 return this; 182 } 183 184 public TzDataBuilder setIndexOffsetOverride(int indexOffset) { 185 this.indexOffsetOverride = indexOffset; 186 return this; 187 } 188 189 public TzDataBuilder setDataOffsetOverride(int dataOffset) { 190 this.dataOffsetOverride = dataOffset; 191 return this; 192 } 193 194 public TzDataBuilder setZoneTabOffsetOverride(int zoneTabOffset) { 195 this.zoneTabOffsetOverride = zoneTabOffset; 196 return this; 197 } 198 199 /** 200 * Adds data for a new zone. These must be added in ID string order to generate 201 * a valid file. 202 */ 203 public TzDataBuilder addZicData(String id, byte[] data) { 204 zicData.add(new ZicDatum(id, data)); 205 return this; 206 } 207 208 public TzDataBuilder setZoneTab(String zoneTab) { 209 this.zoneTab = zoneTab; 210 return this; 211 } 212 213 public TzDataBuilder initializeToValid() { 214 setHeaderMagic("tzdata9999a"); 215 addZicData("Europe/Elbonia", new ZicDataBuilder().initializeToValid().build()); 216 setZoneTab("ZoneTab data"); 217 return this; 218 } 219 220 public TzDataBuilder clearZicData() { 221 zicData.clear(); 222 return this; 223 } 224 225 public byte[] build() { 226 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 227 228 byte[] headerMagicBytes = headerMagic.getBytes(StandardCharsets.US_ASCII); 229 baos.write(headerMagicBytes, 0, headerMagicBytes.length); 230 baos.write(0); 231 232 // Write out the offsets for later manipulation. 233 int indexOffsetOffset = baos.size(); 234 writeInt(baos, 0); 235 int dataOffsetOffset = baos.size(); 236 writeInt(baos, 0); 237 int zoneTabOffsetOffset = baos.size(); 238 writeInt(baos, 0); 239 240 // Construct the data section in advance, so we know the offsets. 241 ByteArrayOutputStream dataBytes = new ByteArrayOutputStream(); 242 Map<String, Integer> offsets = new HashMap<>(); 243 for (ZicDatum datum : zicData) { 244 int offset = dataBytes.size(); 245 offsets.put(datum.id, offset); 246 writeByteArray(dataBytes, datum.data); 247 } 248 249 int indexOffset = baos.size(); 250 251 // Write the index section. 252 for (ZicDatum zicDatum : zicData) { 253 // Write the ID. 254 String id = zicDatum.id; 255 byte[] idBytes = id.getBytes(StandardCharsets.US_ASCII); 256 byte[] paddedIdBytes = new byte[40]; 257 System.arraycopy(idBytes, 0, paddedIdBytes, 0, idBytes.length); 258 writeByteArray(baos, paddedIdBytes); 259 // Write offset of zic data in the data section. 260 Integer offset = offsets.get(id); 261 writeInt(baos, offset); 262 // Write the length of the zic data. 263 writeInt(baos, zicDatum.data.length); 264 // Write a filler value (not used) 265 writeInt(baos, 0); 266 } 267 268 // Write the data section. 269 int dataOffset = baos.size(); 270 writeByteArray(baos, dataBytes.toByteArray()); 271 272 // Write the zoneTab section. 273 int zoneTabOffset = baos.size(); 274 byte[] zoneTabBytes = zoneTab.getBytes(StandardCharsets.US_ASCII); 275 writeByteArray(baos, zoneTabBytes); 276 277 byte[] bytes = baos.toByteArray(); 278 setInt(bytes, indexOffsetOffset, 279 indexOffsetOverride != null ? indexOffsetOverride : indexOffset); 280 setInt(bytes, dataOffsetOffset, 281 dataOffsetOverride != null ? dataOffsetOverride : dataOffset); 282 setInt(bytes, zoneTabOffsetOffset, 283 zoneTabOffsetOverride != null ? zoneTabOffsetOverride : zoneTabOffset); 284 return bytes; 285 } 286 287 private static class ZicDatum { 288 public final String id; 289 public final byte[] data; 290 291 ZicDatum(String id, byte[] data) { 292 this.id = id; 293 this.data = data; 294 } 295 } 296 } 297 298 static void writeByteArray(ByteArrayOutputStream baos, byte[] array) { 299 baos.write(array, 0, array.length); 300 } 301 302 static void writeByte(ByteArrayOutputStream baos, int value) { 303 baos.write(value); 304 } 305 306 static void writeIntArray(ByteArrayOutputStream baos, int[] array) { 307 for (int value : array) { 308 writeInt(baos, value); 309 } 310 } 311 312 static void writeInt(ByteArrayOutputStream os, int value) { 313 byte[] bytes = ByteBuffer.allocate(4).putInt(value).array(); 314 writeByteArray(os, bytes); 315 } 316 317 static void setInt(byte[] bytes, int offset, int value) { 318 bytes[offset] = (byte) (value >>> 24); 319 bytes[offset + 1] = (byte) (value >>> 16); 320 bytes[offset + 2] = (byte) (value >>> 8); 321 bytes[offset + 3] = (byte) value; 322 } 323 } 324