• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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