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