• 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.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