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.block.read; 18 19 import com.android.timezone.location.storage.io.read.TypedInputStream; 20 import com.android.timezone.location.storage.util.Visitor; 21 22 import java.io.File; 23 import java.io.FileInputStream; 24 import java.io.IOException; 25 import java.nio.ByteBuffer; 26 import java.nio.ByteOrder; 27 import java.nio.channels.FileChannel; 28 29 /** 30 * The entry point for reading a generic block file. This class is not thread-safe. 31 */ 32 public final class BlockFileReader implements AutoCloseable { 33 34 private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0).asReadOnlyBuffer(); 35 36 private Character mRequiredMagic; 37 38 private Integer mRequiredMinVersion; 39 40 private final boolean mMemoryMapBlocks; 41 42 private char mMagic; 43 44 private int mVersion; 45 46 private BlockInfo[] mBlockInfos; 47 48 private FileChannel mFileChannel; 49 BlockFileReader(boolean memoryMapBlocks)50 private BlockFileReader(boolean memoryMapBlocks) { 51 mMemoryMapBlocks = memoryMapBlocks; 52 } 53 setRequiredMagic(char magic)54 private void setRequiredMagic(char magic) { 55 mRequiredMagic = magic; 56 } 57 setRequiredMinVersion(int requiredMinVersion)58 private void setRequiredMinVersion(int requiredMinVersion) { 59 mRequiredMinVersion = requiredMinVersion; 60 } 61 62 /** 63 * Opens a block file. To open successfully the block file must have the expected 16-bit "magic" 64 * value and a version >= {@code minVersion}. 65 * 66 * @param memoryMapBlocks whether to read blocks via memory mapping, or reading the whole block 67 * into memory 68 * @param requiredMagic the expected file magic 69 * @param requiredMinVersion the required minimum file version 70 */ open(boolean memoryMapBlocks, File file, char requiredMagic, int requiredMinVersion)71 public static BlockFileReader open(boolean memoryMapBlocks, File file, 72 char requiredMagic, int requiredMinVersion) throws IOException { 73 BlockFileReader reader = new BlockFileReader(memoryMapBlocks); 74 reader.setRequiredMagic(requiredMagic); 75 reader.setRequiredMinVersion(requiredMinVersion); 76 reader.open(file); 77 return reader; 78 } 79 80 /** 81 * Opens a block file. 82 * 83 * @param memoryMapBlocks whether to read blocks via memory mapping, or reading the whole block 84 * into memory 85 */ open(boolean memoryMapBlocks, File file)86 public static BlockFileReader open(boolean memoryMapBlocks, File file) throws IOException { 87 BlockFileReader reader = new BlockFileReader(memoryMapBlocks); 88 reader.open(file); 89 return reader; 90 } 91 open(File file)92 private void open(File file) throws IOException { 93 try (TypedInputStream tis = new TypedInputStream(new FileInputStream(file))) { 94 mMagic = tis.readChar(); 95 if (mRequiredMagic != null && mMagic != mRequiredMagic) { 96 throw new IOException("Bad magic: expected " + Integer.toHexString(this.mMagic) 97 + " but was " + Integer.toHexString(mMagic)); 98 } 99 100 mVersion = tis.readInt(); 101 if (mRequiredMinVersion != null && mVersion < mRequiredMinVersion) { 102 throw new IOException("Bad version: " + mVersion 103 + ", reader requires at least " + mRequiredMinVersion); 104 } 105 106 // 10-bytes left for expansion. 107 tis.skipBytes(10); 108 109 int blockCount = tis.readInt(); 110 mBlockInfos = new BlockInfo[blockCount]; 111 for (int i = 0; i < blockCount; i++) { 112 int blockInfoSize = tis.readInt(); 113 final int minimumBlockInfoSize = 114 (2 * Integer.BYTES) + (2 * Long.BYTES) + Byte.BYTES; 115 if (blockInfoSize < minimumBlockInfoSize) { 116 throw new IOException("Minimum block info size:" + minimumBlockInfoSize); 117 } 118 119 int blockType = tis.readInt(); 120 long blockStartByteOffset = tis.readLong(); 121 long blockDataSizeBytes = tis.readLong(); 122 byte[] extraBytes = tis.readTinyVarByteArray(); 123 mBlockInfos[i] = new BlockInfo(i, blockType, blockStartByteOffset, 124 blockDataSizeBytes, extraBytes); 125 } 126 } 127 mFileChannel = FileChannel.open(file.toPath()); 128 } 129 130 /** 131 * Closes the block file. 132 */ close()133 public void close() throws IOException { 134 mFileChannel.close(); 135 } 136 137 /** Returns information from the file's header about the block with the specified ID. */ getBlockInfo(int blockId)138 public BlockInfo getBlockInfo(int blockId) { 139 checkFileOpen(); 140 return mBlockInfos[blockId]; 141 } 142 143 /** Returns the block with the specified ID. */ getBlock(int blockId)144 public Block getBlock(int blockId) throws IOException { 145 checkFileOpen(); 146 147 BlockInfo blockInfo = mBlockInfos[blockId]; 148 if (blockInfo.getBlockSizeBytes() == 0) { 149 return new Block(blockId, blockInfo.getType(), EMPTY_BYTE_BUFFER); 150 } 151 152 ByteBuffer allBlockBuffer; 153 if (mMemoryMapBlocks) { 154 // Map the entire block, including the block header. 155 allBlockBuffer = mFileChannel.map( 156 FileChannel.MapMode.READ_ONLY, 157 blockInfo.getBlockStartByteOffset(), 158 blockInfo.getBlockSizeBytes()); 159 if (allBlockBuffer.order() != ByteOrder.BIG_ENDIAN) { 160 throw new IllegalStateException("Byte order must be BIG_ENDIAN"); 161 } 162 } else { 163 // No memory map version 164 165 // Not thread safe because of the read call. 166 long blockSizeBytesLong = blockInfo.getBlockSizeBytes(); 167 if (blockSizeBytesLong > Integer.MAX_VALUE) { 168 throw new IOException("Block too large to read into memory. Try mapping instead."); 169 } 170 int blockSizeBytes = (int) blockSizeBytesLong; 171 allBlockBuffer = ByteBuffer.allocate(blockSizeBytes); 172 mFileChannel.position(blockInfo.getBlockStartByteOffset()); 173 int bytesRead = mFileChannel.read(allBlockBuffer); 174 if (bytesRead != blockSizeBytes) { 175 throw new IllegalStateException("Unable to read " + blockSizeBytes 176 + ", only read" + bytesRead); 177 } 178 allBlockBuffer.rewind(); 179 allBlockBuffer = allBlockBuffer.asReadOnlyBuffer(); 180 } 181 182 // Read the block header. 183 // This information is redundant, but serves as a check that nothing is wrong with reading 184 // or writing files. 185 int actualId = allBlockBuffer.getInt(); 186 if (actualId != blockId) { 187 throw new IllegalStateException("Expected id=" + blockId + ", but was " + actualId); 188 } 189 int actualType = allBlockBuffer.getInt(); 190 if (actualType != blockInfo.getType()) { 191 throw new IllegalStateException( 192 "Expected type=" + blockInfo.getType() + ", but was " + actualType); 193 } 194 195 // The part of the block that holds the data. 196 ByteBuffer blockDataBytes = allBlockBuffer.slice(); 197 return new Block(actualId, actualType, blockDataBytes); 198 } 199 200 /** Returns the number of blocks in the file. */ getBlockCount()201 public int getBlockCount() { 202 checkFileOpen(); 203 204 return mBlockInfos.length; 205 } 206 207 /** A {@link Visitor} for the {@link BlockFileReader}. See {@link #visit} */ 208 public interface BlockFileVisitor extends Visitor { 209 210 /** Called after {@link #begin()}, once. */ visitFileHeader(char magic, int version, int blockCount)211 void visitFileHeader(char magic, int version, int blockCount) throws VisitException; 212 213 /** Called after {@link #visitFileHeader}, once for each block info in the file. */ visitBlockInfo(BlockInfo blockInfo)214 void visitBlockInfo(BlockInfo blockInfo) throws VisitException; 215 216 /** 217 * Called after {@link #visitBlockInfo(BlockInfo)} has been called for all block infos, 218 * once for each block. 219 */ visitBlock(Block block)220 void visitBlock(Block block) throws VisitException; 221 } 222 223 /** 224 * Issues callbacks to the supplied {@link BlockFileVisitor} containing information from the 225 * block file. 226 */ visit(BlockFileVisitor visitor)227 public void visit(BlockFileVisitor visitor) throws Visitor.VisitException { 228 checkFileOpen(); 229 230 try { 231 visitor.begin(); 232 visitor.visitFileHeader(mMagic, mVersion, mBlockInfos.length); 233 for (int i = 0; i < mBlockInfos.length; i++) { 234 visitor.visitBlockInfo(mBlockInfos[i]); 235 } 236 try { 237 for (int i = 0; i < getBlockCount(); i++) { 238 visitor.visitBlock(getBlock(i)); 239 } 240 } catch (IOException e) { 241 throw new Visitor.VisitException(e); 242 } 243 } finally { 244 visitor.end(); 245 } 246 } 247 checkFileOpen()248 private void checkFileOpen() { 249 if (!mFileChannel.isOpen()) { 250 throw new IllegalStateException("BlockFile is closed."); 251 } 252 } 253 } 254