1 /* 2 * Copyright (C) 2020 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 17 package com.android.timezone.location.storage.tzs2range.write; 18 19 import static com.android.timezone.location.storage.s2.S2Support.cellIdToString; 20 21 import com.android.timezone.location.storage.block.read.BlockData; 22 import com.android.timezone.location.storage.block.write.BlockWriter; 23 import com.android.timezone.location.storage.block.write.EmptyBlockWriter; 24 import com.android.timezone.location.storage.io.write.TypedOutputStream; 25 import com.android.timezone.location.storage.s2.S2Support; 26 import com.android.timezone.location.storage.table.packed.write.PackedTableWriter; 27 import com.android.timezone.location.storage.tzs2range.SuffixTableRange; 28 import com.android.timezone.location.storage.tzs2range.SuffixTableSharedData; 29 import com.android.timezone.location.storage.tzs2range.TzS2RangeFileFormat; 30 import com.android.timezone.location.storage.tzs2range.read.SuffixTableExtraInfo; 31 32 import java.io.ByteArrayOutputStream; 33 import java.io.File; 34 import java.io.FileOutputStream; 35 import java.io.IOException; 36 import java.nio.MappedByteBuffer; 37 import java.nio.channels.FileChannel; 38 import java.nio.file.StandardOpenOption; 39 40 /** 41 * A class used to generate suffix tables block info and block data. 42 * To write empty tables use {@link #createEmptyBlockWriter()}. 43 * To write populated tables use {@link 44 * #createPopulated(TzS2RangeFileFormat, SuffixTableSharedData)} and add entries with 45 * {@link #addRange(SuffixTableRange)} 46 */ 47 public final class SuffixTableWriter implements BlockWriter { 48 49 private final SuffixTableSharedData mSharedData; 50 51 private final TzS2RangeFileFormat mFileFormat; 52 53 private final PackedTableWriter mPackedTableWriter; 54 55 private final File mFile; 56 57 private SuffixTableRange mLastRangeAdded; 58 SuffixTableWriter(TzS2RangeFileFormat fileFormat, SuffixTableSharedData sharedData)59 private SuffixTableWriter(TzS2RangeFileFormat fileFormat, SuffixTableSharedData sharedData) 60 throws IOException { 61 mFileFormat = fileFormat; 62 mSharedData = sharedData; 63 64 int keySizeBits = fileFormat.getSuffixBitCount(); 65 int entrySizeByteCount = fileFormat.getTableEntryByteCount(); 66 mFile = File.createTempFile("suffixtablewriter", ".packed"); 67 68 byte[] blockSharedData = SuffixTableSharedDataWriter.toBytes(sharedData); 69 FileOutputStream fileOutputStream = new FileOutputStream(mFile); 70 boolean signedValue = false; 71 mPackedTableWriter = PackedTableWriter.create( 72 fileOutputStream, entrySizeByteCount, keySizeBits, signedValue, blockSharedData); 73 } 74 75 /** Returns a {@link BlockWriter} capable of generating the block data for an empty table. */ createEmptyBlockWriter()76 public static BlockWriter createEmptyBlockWriter() { 77 return new EmptyBlockWriter(TzS2RangeFileFormat.BLOCK_TYPE_SUFFIX_TABLE); 78 } 79 80 /** Returns a {@link BlockWriter} capable of generating the block data for a populated table. */ createPopulated( TzS2RangeFileFormat fileFormat, SuffixTableSharedData sharedData)81 public static SuffixTableWriter createPopulated( 82 TzS2RangeFileFormat fileFormat, SuffixTableSharedData sharedData) throws IOException { 83 return new SuffixTableWriter(fileFormat, sharedData); 84 } 85 86 /** 87 * Adds the supplied range to the table. The range must start after any previously added range, 88 * no overlap is allowed. Gaps are permitted. The range must have the expected S2 cell ID 89 * prefix. Invalid ranges will cause {@link IllegalArgumentException}. This method must be 90 * called at least once. See {@link SuffixTableWriter#createEmptyBlockWriter()} for empty 91 * tables. 92 */ addRange(SuffixTableRange suffixTableRange)93 public void addRange(SuffixTableRange suffixTableRange) throws IOException { 94 checkIsOpen(); 95 96 long rangeStartCellId = suffixTableRange.getStartCellId(); 97 long rangeEndCellId = suffixTableRange.getEndCellId(); 98 99 // Check range belongs in this table. 100 int rangeStartPrefixValue = mFileFormat.extractPrefixValueFromCellId(rangeStartCellId); 101 int rangeStartSuffixValue = mFileFormat.extractSuffixValueFromCellId(rangeStartCellId); 102 if (rangeStartPrefixValue != mSharedData.getTablePrefix()) { 103 throw new IllegalArgumentException( 104 "rangeStartCellId=" + cellIdToString(rangeStartCellId) 105 + " has a different prefix=" + rangeStartPrefixValue 106 + " than the table prefix=" + mSharedData.getTablePrefix()); 107 } 108 109 long rangeEndCellIdInclusive = S2Support.offsetCellId(rangeEndCellId, -1); 110 int rangeEndPrefixValue = mFileFormat.extractPrefixValueFromCellId(rangeEndCellIdInclusive); 111 if (rangeEndPrefixValue != rangeStartPrefixValue) { 112 // Because SuffixTableRange has an exclusive end value, rangeEndPrefixValue is allowed 113 // to be the next prefix value if the rangeEndSuffixValue == 0. 114 int rangeEndSuffixValue = mFileFormat.extractSuffixValueFromCellId(rangeEndCellId); 115 if (!(rangeEndPrefixValue == rangeStartPrefixValue + 1 && rangeEndSuffixValue == 0)) { 116 throw new IllegalArgumentException("rangeEndPrefixValue=" + rangeEndPrefixValue 117 + " != rangeStartPrefixValue=" + rangeStartPrefixValue); 118 } 119 } 120 121 // Confirm the new range starts after the end of the last one that was added, if any. 122 if (mLastRangeAdded != null) { 123 long lastRangeAddedEndCellId = mLastRangeAdded.getEndCellId(); 124 int lastRangeEndPrefixValue = 125 mFileFormat.extractPrefixValueFromCellId(lastRangeAddedEndCellId); 126 if (lastRangeEndPrefixValue != mSharedData.getTablePrefix()) { 127 // Deal with the special case where the last range added completed the table. 128 throw new IllegalArgumentException( 129 "Suffix table is full: last range added=" + mLastRangeAdded); 130 } else { 131 int lastRangeEndSuffixValue = 132 mFileFormat.extractSuffixValueFromCellId(lastRangeAddedEndCellId); 133 if (rangeStartSuffixValue < lastRangeEndSuffixValue) { 134 throw new IllegalArgumentException("suffixTableRange=" + suffixTableRange 135 + " overlaps with last range added=" + mLastRangeAdded); 136 } 137 } 138 } 139 140 int rangeLength = mFileFormat.calculateRangeLength(rangeStartCellId, rangeEndCellId); 141 142 int tzIdSetId = suffixTableRange.getTzIdSetId(); 143 if (tzIdSetId < 0 || tzIdSetId > mFileFormat.getMaxTzIdSetIdValue()) { 144 throw new IllegalArgumentException("suffixTableRange.getTzSetId()=" + tzIdSetId 145 + " is outside of the allowed range." 146 + " Max value=" + mFileFormat.getMaxTzIdSetIdValue()); 147 } 148 149 long value = mFileFormat.createSuffixTableValue(rangeLength, tzIdSetId); 150 mPackedTableWriter.addEntry(rangeStartSuffixValue, value); 151 mLastRangeAdded = suffixTableRange; 152 } 153 154 @Override close()155 public ReadBack close() throws IOException { 156 checkIsOpen(); 157 mPackedTableWriter.close(); 158 mLastRangeAdded = null; 159 160 int entryCount = mPackedTableWriter.getEntryCount(); 161 if (entryCount == 0) { 162 throw new IllegalStateException("No ranges added. For an empty suffix table, use" 163 + " createEmptySuffixTableBlockWriter()"); 164 } 165 166 FileChannel fileChannel = FileChannel.open(mFile.toPath(), StandardOpenOption.READ); 167 MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, mFile.length()); 168 fileChannel.close(); 169 170 // Writes the number of entries into the extra bytes stored in the BlockInfo. This means the 171 // number of entries can be known without reading the block data at all. 172 SuffixTableExtraInfo suffixTableExtraInfo = 173 new SuffixTableExtraInfo(mSharedData.getTablePrefix(), entryCount); 174 byte[] blockInfoExtraBytes = generateBlockInfoExtraBytes(suffixTableExtraInfo); 175 BlockData blockData = new BlockData(map); 176 return new ReadBack() { 177 @Override 178 public byte[] getExtraBytes() { 179 return blockInfoExtraBytes; 180 } 181 182 @Override 183 public int getType() { 184 return TzS2RangeFileFormat.BLOCK_TYPE_SUFFIX_TABLE; 185 } 186 187 @Override 188 public BlockData getBlockData() { 189 return blockData; 190 } 191 }; 192 } 193 194 private void checkIsOpen() { 195 if (!mPackedTableWriter.isOpen()) { 196 throw new IllegalStateException("Writer is closed."); 197 } 198 } 199 200 private static byte[] generateBlockInfoExtraBytes(SuffixTableExtraInfo suffixTableBlockInfo) { 201 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 202 try (TypedOutputStream tos = new TypedOutputStream(baos)) { 203 tos.writeInt(suffixTableBlockInfo.getEntryCount()); 204 } catch (IOException e) { 205 throw new IllegalStateException("Unexpected IOException writing to byte array", e); 206 } 207 return baos.toByteArray(); 208 } 209 } 210