1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.compress.archivers.arj;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.DataInputStream;
22 import java.io.EOFException;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.util.ArrayList;
26 import java.util.zip.CRC32;
27
28 import org.apache.commons.compress.archivers.ArchiveEntry;
29 import org.apache.commons.compress.archivers.ArchiveException;
30 import org.apache.commons.compress.archivers.ArchiveInputStream;
31 import org.apache.commons.compress.utils.BoundedInputStream;
32 import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
33 import org.apache.commons.compress.utils.IOUtils;
34
35
36
37
38
39
40
41
42
43
44
45 public class ArjArchiveInputStream extends ArchiveInputStream<ArjArchiveEntry> {
46
47 private static final String ENCODING_NAME = "CP437";
48 private static final int ARJ_MAGIC_1 = 0x60;
49 private static final int ARJ_MAGIC_2 = 0xEA;
50
51
52
53
54
55
56
57
58 public static boolean matches(final byte[] signature, final int length) {
59 return length >= 2 && (0xff & signature[0]) == ARJ_MAGIC_1 && (0xff & signature[1]) == ARJ_MAGIC_2;
60 }
61
62 private final DataInputStream dis;
63 private final MainHeader mainHeader;
64 private LocalFileHeader currentLocalFileHeader;
65 private InputStream currentInputStream;
66
67
68
69
70
71
72
73 public ArjArchiveInputStream(final InputStream inputStream) throws ArchiveException {
74 this(inputStream, ENCODING_NAME);
75 }
76
77
78
79
80
81
82
83
84 public ArjArchiveInputStream(final InputStream inputStream, final String charsetName) throws ArchiveException {
85 super(inputStream, charsetName);
86 in = dis = new DataInputStream(inputStream);
87 try {
88 mainHeader = readMainHeader();
89 if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
90 throw new ArchiveException("Encrypted ARJ files are unsupported");
91 }
92 if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
93 throw new ArchiveException("Multi-volume ARJ files are unsupported");
94 }
95 } catch (final IOException ioException) {
96 throw new ArchiveException(ioException.getMessage(), ioException);
97 }
98 }
99
100 @Override
101 public boolean canReadEntryData(final ArchiveEntry ae) {
102 return ae instanceof ArjArchiveEntry && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
103 }
104
105 @Override
106 public void close() throws IOException {
107 dis.close();
108 }
109
110
111
112
113
114
115 public String getArchiveComment() {
116 return mainHeader.comment;
117 }
118
119
120
121
122
123
124 public String getArchiveName() {
125 return mainHeader.name;
126 }
127
128 @Override
129 public ArjArchiveEntry getNextEntry() throws IOException {
130 if (currentInputStream != null) {
131
132 final InputStream input = currentInputStream;
133 org.apache.commons.io.IOUtils.skip(input, Long.MAX_VALUE);
134 currentInputStream.close();
135 currentLocalFileHeader = null;
136 currentInputStream = null;
137 }
138
139 currentLocalFileHeader = readLocalFileHeader();
140 if (currentLocalFileHeader != null) {
141 currentInputStream = new BoundedInputStream(dis, currentLocalFileHeader.compressedSize);
142 if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
143 currentInputStream = new CRC32VerifyingInputStream(currentInputStream, currentLocalFileHeader.originalSize,
144 currentLocalFileHeader.originalCrc32);
145 }
146 return new ArjArchiveEntry(currentLocalFileHeader);
147 }
148 currentInputStream = null;
149 return null;
150 }
151
152 @Override
153 public int read(final byte[] b, final int off, final int len) throws IOException {
154 if (len == 0) {
155 return 0;
156 }
157 if (currentLocalFileHeader == null) {
158 throw new IllegalStateException("No current arj entry");
159 }
160 if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
161 throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
162 }
163 return currentInputStream.read(b, off, len);
164 }
165
166 private int read16(final DataInputStream dataIn) throws IOException {
167 final int value = dataIn.readUnsignedShort();
168 count(2);
169 return Integer.reverseBytes(value) >>> 16;
170 }
171
172 private int read32(final DataInputStream dataIn) throws IOException {
173 final int value = dataIn.readInt();
174 count(4);
175 return Integer.reverseBytes(value);
176 }
177
178 private int read8(final DataInputStream dataIn) throws IOException {
179 final int value = dataIn.readUnsignedByte();
180 count(1);
181 return value;
182 }
183
184 private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException {
185 if (firstHeaderSize >= 33) {
186 localFileHeader.extendedFilePosition = read32(firstHeader);
187 if (firstHeaderSize >= 45) {
188 localFileHeader.dateTimeAccessed = read32(firstHeader);
189 localFileHeader.dateTimeCreated = read32(firstHeader);
190 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
191 pushedBackBytes(12);
192 }
193 pushedBackBytes(4);
194 }
195 }
196
197 private byte[] readHeader() throws IOException {
198 boolean found = false;
199 byte[] basicHeaderBytes = null;
200 do {
201 int first;
202 int second = read8(dis);
203 do {
204 first = second;
205 second = read8(dis);
206 } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
207 final int basicHeaderSize = read16(dis);
208 if (basicHeaderSize == 0) {
209
210 return null;
211 }
212 if (basicHeaderSize <= 2600) {
213 basicHeaderBytes = readRange(dis, basicHeaderSize);
214 final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
215 final CRC32 crc32 = new CRC32();
216 crc32.update(basicHeaderBytes);
217 if (basicHeaderCrc32 == crc32.getValue()) {
218 found = true;
219 }
220 }
221 } while (!found);
222 return basicHeaderBytes;
223 }
224
225 private LocalFileHeader readLocalFileHeader() throws IOException {
226 final byte[] basicHeaderBytes = readHeader();
227 if (basicHeaderBytes == null) {
228 return null;
229 }
230 try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
231
232 final int firstHeaderSize = basicHeader.readUnsignedByte();
233 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
234 pushedBackBytes(firstHeaderBytes.length);
235 try (DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
236
237 final LocalFileHeader localFileHeader = new LocalFileHeader();
238 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
239 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
240 localFileHeader.hostOS = firstHeader.readUnsignedByte();
241 localFileHeader.arjFlags = firstHeader.readUnsignedByte();
242 localFileHeader.method = firstHeader.readUnsignedByte();
243 localFileHeader.fileType = firstHeader.readUnsignedByte();
244 localFileHeader.reserved = firstHeader.readUnsignedByte();
245 localFileHeader.dateTimeModified = read32(firstHeader);
246 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
247 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
248 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
249 localFileHeader.fileSpecPosition = read16(firstHeader);
250 localFileHeader.fileAccessMode = read16(firstHeader);
251 pushedBackBytes(20);
252 localFileHeader.firstChapter = firstHeader.readUnsignedByte();
253 localFileHeader.lastChapter = firstHeader.readUnsignedByte();
254
255 readExtraData(firstHeaderSize, firstHeader, localFileHeader);
256
257 localFileHeader.name = readString(basicHeader);
258 localFileHeader.comment = readString(basicHeader);
259
260 final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
261 int extendedHeaderSize;
262 while ((extendedHeaderSize = read16(dis)) > 0) {
263 final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
264 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
265 final CRC32 crc32 = new CRC32();
266 crc32.update(extendedHeaderBytes);
267 if (extendedHeaderCrc32 != crc32.getValue()) {
268 throw new IOException("Extended header CRC32 verification failure");
269 }
270 extendedHeaders.add(extendedHeaderBytes);
271 }
272 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
273
274 return localFileHeader;
275 }
276 }
277 }
278
279 private MainHeader readMainHeader() throws IOException {
280 final byte[] basicHeaderBytes = readHeader();
281 if (basicHeaderBytes == null) {
282 throw new IOException("Archive ends without any headers");
283 }
284 final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes));
285
286 final int firstHeaderSize = basicHeader.readUnsignedByte();
287 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
288 pushedBackBytes(firstHeaderBytes.length);
289
290 final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes));
291
292 final MainHeader hdr = new MainHeader();
293 hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
294 hdr.minVersionToExtract = firstHeader.readUnsignedByte();
295 hdr.hostOS = firstHeader.readUnsignedByte();
296 hdr.arjFlags = firstHeader.readUnsignedByte();
297 hdr.securityVersion = firstHeader.readUnsignedByte();
298 hdr.fileType = firstHeader.readUnsignedByte();
299 hdr.reserved = firstHeader.readUnsignedByte();
300 hdr.dateTimeCreated = read32(firstHeader);
301 hdr.dateTimeModified = read32(firstHeader);
302 hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
303 hdr.securityEnvelopeFilePosition = read32(firstHeader);
304 hdr.fileSpecPosition = read16(firstHeader);
305 hdr.securityEnvelopeLength = read16(firstHeader);
306 pushedBackBytes(20);
307 hdr.encryptionVersion = firstHeader.readUnsignedByte();
308 hdr.lastChapter = firstHeader.readUnsignedByte();
309
310 if (firstHeaderSize >= 33) {
311 hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
312 hdr.arjFlags2 = firstHeader.readUnsignedByte();
313 firstHeader.readUnsignedByte();
314 firstHeader.readUnsignedByte();
315 }
316
317 hdr.name = readString(basicHeader);
318 hdr.comment = readString(basicHeader);
319
320 final int extendedHeaderSize = read16(dis);
321 if (extendedHeaderSize > 0) {
322 hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
323 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
324 final CRC32 crc32 = new CRC32();
325 crc32.update(hdr.extendedHeaderBytes);
326 if (extendedHeaderCrc32 != crc32.getValue()) {
327 throw new IOException("Extended header CRC32 verification failure");
328 }
329 }
330
331 return hdr;
332 }
333
334 private byte[] readRange(final InputStream in, final int len) throws IOException {
335 final byte[] b = IOUtils.readRange(in, len);
336 count(b.length);
337 if (b.length < len) {
338 throw new EOFException();
339 }
340 return b;
341 }
342
343 private String readString(final DataInputStream dataIn) throws IOException {
344 try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
345 int nextByte;
346 while ((nextByte = dataIn.readUnsignedByte()) != 0) {
347 buffer.write(nextByte);
348 }
349 return buffer.toString(getCharset().name());
350 }
351 }
352 }