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     @Override
read(byte[] buf, int off, int len)137     public int read(byte[] buf, int off, int len) throws IOException {
138         if (!mIsSparse) {
139             return mIn.read(buf, off, len);
140         }
141         if (prepareChunk()) return -1;
142         int n = -1;
143         switch (mCur.mChunkType) {
144             case SparseChunk.RAW:
145                 n = mIn.read(buf, off, (int) min(mLeft, len));
146                 mLeft -= n;
147                 return n;
148             case SparseChunk.DONTCARE:
149                 n = (int) min(mLeft, len);
150                 Arrays.fill(buf, off, off + n, (byte) 0);
151                 mLeft -= n;
152                 return n;
153             case SparseChunk.FILL:
154                 // The FILL type is rarely used, so use a simple implmentation.
155                 return super.read(buf, off, len);
156             default:
157                 throw new IOException("Unsupported Chunk:" + mCur.toString());
158         }
159     }
160 
161     @Override
read()162     public int read() throws IOException {
163         if (!mIsSparse) {
164             return mIn.read();
165         }
166         if (prepareChunk()) return -1;
167         int ret = -1;
168         switch (mCur.mChunkType) {
169             case SparseChunk.RAW:
170                 ret = mIn.read();
171                 break;
172             case SparseChunk.DONTCARE:
173                 ret = 0;
174                 break;
175             case SparseChunk.FILL:
176                 ret = Byte.toUnsignedInt(mCur.fill[(4 - ((int) mLeft & 0x3)) & 0x3]);
177                 break;
178             default:
179                 throw new IOException("Unsupported Chunk:" + mCur.toString());
180         }
181         mLeft--;
182         return ret;
183     }
184 
185     /**
186      * Get the unsparse size
187      * @return -1 if unknown
188      */
getUnsparseSize()189     public long getUnsparseSize() {
190         if (!mIsSparse) {
191             return -1;
192         }
193         return mBlockSize * mTotalBlocks;
194     }
195 }
196