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