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.IOUtils;
32 import org.apache.commons.io.input.BoundedInputStream;
33 import org.apache.commons.io.input.ChecksumInputStream;
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
142 currentInputStream = BoundedInputStream.builder()
143 .setInputStream(dis)
144 .setMaxCount(currentLocalFileHeader.compressedSize)
145 .setPropagateClose(false)
146 .get();
147
148 if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
149
150 currentInputStream = ChecksumInputStream.builder()
151 .setChecksum(new CRC32())
152 .setInputStream(currentInputStream)
153 .setCountThreshold(currentLocalFileHeader.originalSize)
154 .setExpectedChecksumValue(currentLocalFileHeader.originalCrc32)
155 .get();
156
157 }
158 return new ArjArchiveEntry(currentLocalFileHeader);
159 }
160 currentInputStream = null;
161 return null;
162 }
163
164 @Override
165 public int read(final byte[] b, final int off, final int len) throws IOException {
166 if (len == 0) {
167 return 0;
168 }
169 if (currentLocalFileHeader == null) {
170 throw new IllegalStateException("No current arj entry");
171 }
172 if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
173 throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
174 }
175 return currentInputStream.read(b, off, len);
176 }
177
178 private int read16(final DataInputStream dataIn) throws IOException {
179 final int value = dataIn.readUnsignedShort();
180 count(2);
181 return Integer.reverseBytes(value) >>> 16;
182 }
183
184 private int read32(final DataInputStream dataIn) throws IOException {
185 final int value = dataIn.readInt();
186 count(4);
187 return Integer.reverseBytes(value);
188 }
189
190 private int read8(final DataInputStream dataIn) throws IOException {
191 final int value = dataIn.readUnsignedByte();
192 count(1);
193 return value;
194 }
195
196 private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException {
197 if (firstHeaderSize >= 33) {
198 localFileHeader.extendedFilePosition = read32(firstHeader);
199 if (firstHeaderSize >= 45) {
200 localFileHeader.dateTimeAccessed = read32(firstHeader);
201 localFileHeader.dateTimeCreated = read32(firstHeader);
202 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
203 pushedBackBytes(12);
204 }
205 pushedBackBytes(4);
206 }
207 }
208
209 private byte[] readHeader() throws IOException {
210 boolean found = false;
211 byte[] basicHeaderBytes = null;
212 do {
213 int first;
214 int second = read8(dis);
215 do {
216 first = second;
217 second = read8(dis);
218 } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
219 final int basicHeaderSize = read16(dis);
220 if (basicHeaderSize == 0) {
221
222 return null;
223 }
224 if (basicHeaderSize <= 2600) {
225 basicHeaderBytes = readRange(dis, basicHeaderSize);
226 final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
227 final CRC32 crc32 = new CRC32();
228 crc32.update(basicHeaderBytes);
229 if (basicHeaderCrc32 == crc32.getValue()) {
230 found = true;
231 }
232 }
233 } while (!found);
234 return basicHeaderBytes;
235 }
236
237 private LocalFileHeader readLocalFileHeader() throws IOException {
238 final byte[] basicHeaderBytes = readHeader();
239 if (basicHeaderBytes == null) {
240 return null;
241 }
242 try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
243
244 final int firstHeaderSize = basicHeader.readUnsignedByte();
245 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
246 pushedBackBytes(firstHeaderBytes.length);
247 try (DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
248
249 final LocalFileHeader localFileHeader = new LocalFileHeader();
250 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
251 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
252 localFileHeader.hostOS = firstHeader.readUnsignedByte();
253 localFileHeader.arjFlags = firstHeader.readUnsignedByte();
254 localFileHeader.method = firstHeader.readUnsignedByte();
255 localFileHeader.fileType = firstHeader.readUnsignedByte();
256 localFileHeader.reserved = firstHeader.readUnsignedByte();
257 localFileHeader.dateTimeModified = read32(firstHeader);
258 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
259 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
260 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
261 localFileHeader.fileSpecPosition = read16(firstHeader);
262 localFileHeader.fileAccessMode = read16(firstHeader);
263 pushedBackBytes(20);
264 localFileHeader.firstChapter = firstHeader.readUnsignedByte();
265 localFileHeader.lastChapter = firstHeader.readUnsignedByte();
266
267 readExtraData(firstHeaderSize, firstHeader, localFileHeader);
268
269 localFileHeader.name = readString(basicHeader);
270 localFileHeader.comment = readString(basicHeader);
271
272 final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
273 int extendedHeaderSize;
274 while ((extendedHeaderSize = read16(dis)) > 0) {
275 final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
276 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
277 final CRC32 crc32 = new CRC32();
278 crc32.update(extendedHeaderBytes);
279 if (extendedHeaderCrc32 != crc32.getValue()) {
280 throw new IOException("Extended header CRC32 verification failure");
281 }
282 extendedHeaders.add(extendedHeaderBytes);
283 }
284 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
285
286 return localFileHeader;
287 }
288 }
289 }
290
291 private MainHeader readMainHeader() throws IOException {
292 final byte[] basicHeaderBytes = readHeader();
293 if (basicHeaderBytes == null) {
294 throw new IOException("Archive ends without any headers");
295 }
296 final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes));
297
298 final int firstHeaderSize = basicHeader.readUnsignedByte();
299 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
300 pushedBackBytes(firstHeaderBytes.length);
301
302 final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes));
303
304 final MainHeader hdr = new MainHeader();
305 hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
306 hdr.minVersionToExtract = firstHeader.readUnsignedByte();
307 hdr.hostOS = firstHeader.readUnsignedByte();
308 hdr.arjFlags = firstHeader.readUnsignedByte();
309 hdr.securityVersion = firstHeader.readUnsignedByte();
310 hdr.fileType = firstHeader.readUnsignedByte();
311 hdr.reserved = firstHeader.readUnsignedByte();
312 hdr.dateTimeCreated = read32(firstHeader);
313 hdr.dateTimeModified = read32(firstHeader);
314 hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
315 hdr.securityEnvelopeFilePosition = read32(firstHeader);
316 hdr.fileSpecPosition = read16(firstHeader);
317 hdr.securityEnvelopeLength = read16(firstHeader);
318 pushedBackBytes(20);
319 hdr.encryptionVersion = firstHeader.readUnsignedByte();
320 hdr.lastChapter = firstHeader.readUnsignedByte();
321
322 if (firstHeaderSize >= 33) {
323 hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
324 hdr.arjFlags2 = firstHeader.readUnsignedByte();
325 firstHeader.readUnsignedByte();
326 firstHeader.readUnsignedByte();
327 }
328
329 hdr.name = readString(basicHeader);
330 hdr.comment = readString(basicHeader);
331
332 final int extendedHeaderSize = read16(dis);
333 if (extendedHeaderSize > 0) {
334 hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
335 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
336 final CRC32 crc32 = new CRC32();
337 crc32.update(hdr.extendedHeaderBytes);
338 if (extendedHeaderCrc32 != crc32.getValue()) {
339 throw new IOException("Extended header CRC32 verification failure");
340 }
341 }
342
343 return hdr;
344 }
345
346 private byte[] readRange(final InputStream in, final int len) throws IOException {
347 final byte[] b = IOUtils.readRange(in, len);
348 count(b.length);
349 if (b.length < len) {
350 throw new EOFException();
351 }
352 return b;
353 }
354
355 private String readString(final DataInputStream dataIn) throws IOException {
356 try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
357 int nextByte;
358 while ((nextByte = dataIn.readUnsignedByte()) != 0) {
359 buffer.write(nextByte);
360 }
361 return buffer.toString(getCharset().name());
362 }
363 }
364 }