1 /*
2  * Copyright (C) 2019 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.dynsystem;
18 
19 import static java.lang.Math.min;
20 
21 import java.io.BufferedInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.nio.ByteBuffer;
25 import java.nio.ByteOrder;
26 import java.util.Arrays;
27 
28 /**
29  * SparseInputStream read from upstream and detects the data format. If the upstream is a valid
30  * sparse data, it will unsparse it on the fly. Otherwise, it just passthrough as is.
31  */
32 public class SparseInputStream extends InputStream {
33     static final int FILE_HDR_SIZE = 28;
34     static final int CHUNK_HDR_SIZE = 12;
35 
36     /**
37      * This class represents a chunk in the Android sparse image.
38      *
39      * @see system/core/libsparse/sparse_format.h
40      */
41     private class SparseChunk {
42         static final short RAW = (short) 0xCAC1;
43         static final short FILL = (short) 0xCAC2;
44         static final short DONTCARE = (short) 0xCAC3;
45         public short mChunkType;
46         public int mChunkSize;
47         public int mTotalSize;
48         public byte[] fill;
toString()49         public String toString() {
50             return String.format(
51                     "type: %x, chunk_size: %d, total_size: %d", mChunkType, mChunkSize, mTotalSize);
52         }
53     }
54 
readFull(InputStream in, int size)55     private byte[] readFull(InputStream in, int size) throws IOException {
56         byte[] buf = new byte[size];
57         for (int done = 0, n = 0; done < size; done += n) {
58             if ((n = in.read(buf, done, size - done)) < 0) {
59                 throw new IOException("Failed to readFull");
60             }
61         }
62         return buf;
63     }
64 
readBuffer(InputStream in, int size)65     private ByteBuffer readBuffer(InputStream in, int size) throws IOException {
66         return ByteBuffer.wrap(readFull(in, size)).order(ByteOrder.LITTLE_ENDIAN);
67     }
68 
readChunk(InputStream in)69     private SparseChunk readChunk(InputStream in) throws IOException {
70         SparseChunk chunk = new SparseChunk();
71         ByteBuffer buf = readBuffer(in, CHUNK_HDR_SIZE);
72         chunk.mChunkType = buf.getShort();
73         buf.getShort();
74         chunk.mChunkSize = buf.getInt();
75         chunk.mTotalSize = buf.getInt();
76         return chunk;
77     }
78 
79     private BufferedInputStream mIn;
80     private boolean mIsSparse;
81     private long mBlockSize;
82     private long mTotalBlocks;
83     private long mTotalChunks;
84     private SparseChunk mCur;
85     private long mLeft;
86     private int mCurChunks;
87 
SparseInputStream(BufferedInputStream in)88     public SparseInputStream(BufferedInputStream in) throws IOException {
89         mIn = in;
90         in.mark(FILE_HDR_SIZE * 2);
91         ByteBuffer buf = readBuffer(mIn, FILE_HDR_SIZE);
92         mIsSparse = (buf.getInt() == 0xed26ff3a);
93         if (!mIsSparse) {
94             mIn.reset();
95             return;
96         }
97         int major = buf.getShort();
98         int minor = buf.getShort();
99 
100         if (major > 0x1 || minor > 0x0) {
101             throw new IOException("Unsupported sparse version: " + major + "." + minor);
102         }
103 
104         if (buf.getShort() != FILE_HDR_SIZE) {
105             throw new IOException("Illegal file header size");
106         }
107         if (buf.getShort() != CHUNK_HDR_SIZE) {
108             throw new IOException("Illegal chunk header size");
109         }
110         mBlockSize = buf.getInt();
111         if ((mBlockSize & 0x3) != 0) {
112             throw new IOException("Illegal block size, must be a multiple of 4");
113         }
114         mTotalBlocks = buf.getInt();
115         mTotalChunks = buf.getInt();
116         mLeft = mCurChunks = 0;
117     }
118 
119     /**
120      * Check if it needs to open a new chunk.
121      *
122      * @return true if it's EOF
123      */
prepareChunk()124     private boolean prepareChunk() throws IOException {
125         if (mCur == null || mLeft <= 0) {
126             if (++mCurChunks > mTotalChunks) return true;
127             mCur = readChunk(mIn);
128             if (mCur.mChunkType == SparseChunk.FILL) {
129                 mCur.fill = readFull(mIn, 4);
130             }
131             mLeft = mCur.mChunkSize * mBlockSize;
132         }
133         return mLeft == 0;
134     }
135 
136     /**
137      * It overrides the InputStream.read(byte[] buf)
138      */
read(byte[] buf)139     public int read(byte[] buf) throws IOException {
140         if (!mIsSparse) {
141             return mIn.read(buf);
142         }
143         if (prepareChunk()) return -1;
144         int n = -1;
145         switch (mCur.mChunkType) {
146             case SparseChunk.RAW:
147                 n = mIn.read(buf, 0, (int) min(mLeft, buf.length));
148                 mLeft -= n;
149                 return n;
150             case SparseChunk.DONTCARE:
151                 n = (int) min(mLeft, buf.length);
152                 Arrays.fill(buf, 0, n - 1, (byte) 0);
153                 mLeft -= n;
154                 return n;
155             case SparseChunk.FILL:
156                 // The FILL type is rarely used, so use a simple implmentation.
157                 return super.read(buf);
158             default:
159                 throw new IOException("Unsupported Chunk:" + mCur.toString());
160         }
161     }
162 
163     /**
164      * It overrides the InputStream.read()
165      */
read()166     public int read() throws IOException {
167         if (!mIsSparse) {
168             return mIn.read();
169         }
170         if (prepareChunk()) return -1;
171         int ret = -1;
172         switch (mCur.mChunkType) {
173             case SparseChunk.RAW:
174                 ret = mIn.read();
175                 break;
176             case SparseChunk.DONTCARE:
177                 ret = 0;
178                 break;
179             case SparseChunk.FILL:
180                 ret = mCur.fill[(4 - ((int) mLeft & 0x3)) & 0x3];
181                 break;
182             default:
183                 throw new IOException("Unsupported Chunk:" + mCur.toString());
184         }
185         mLeft--;
186         return ret;
187     }
188 
189     /**
190      * Get the unsparse size
191      * @return -1 if unknown
192      */
getUnsparseSize()193     public long getUnsparseSize() {
194         if (!mIsSparse) {
195             return -1;
196         }
197         return mBlockSize * mTotalBlocks;
198     }
199 }
200