View | Details | Raw Unified | Return to bug 60963
Collapse All | Expand All

(-)a/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java (-1 / +1 lines)
Lines 179-185 Link Here
179
     * @return The archives entries mapped to their names or null if
179
     * @return The archives entries mapped to their names or null if
180
     *         {@link #getArchiveEntry(String)} should be used.
180
     *         {@link #getArchiveEntry(String)} should be used.
181
     */
181
     */
182
    protected abstract HashMap<String,JarEntry> getArchiveEntries(boolean single);
182
    protected abstract Map<String,JarEntry> getArchiveEntries(boolean single);
183
183
184
184
185
    /**
185
    /**
(-)a/java/org/apache/catalina/webresources/AbstractSingleArchiveResourceSet.java (-1 / +2 lines)
Lines 21-26 Link Here
21
import java.net.MalformedURLException;
21
import java.net.MalformedURLException;
22
import java.util.Enumeration;
22
import java.util.Enumeration;
23
import java.util.HashMap;
23
import java.util.HashMap;
24
import java.util.Map;
24
import java.util.jar.JarEntry;
25
import java.util.jar.JarEntry;
25
import java.util.jar.JarFile;
26
import java.util.jar.JarFile;
26
27
Lines 59-65 Link Here
59
60
60
61
61
    @Override
62
    @Override
62
    protected HashMap<String,JarEntry> getArchiveEntries(boolean single) {
63
    protected Map<String,JarEntry> getArchiveEntries(boolean single) {
63
        synchronized (archiveLock) {
64
        synchronized (archiveLock) {
64
            if (archiveEntries == null && !single) {
65
            if (archiveEntries == null && !single) {
65
                JarFile jarFile = null;
66
                JarFile jarFile = null;
(-)a/java/org/apache/catalina/webresources/JarWarResource.java (-27 / +25 lines)
Lines 21-26 Link Here
21
import java.util.jar.JarEntry;
21
import java.util.jar.JarEntry;
22
import java.util.jar.JarFile;
22
import java.util.jar.JarFile;
23
import java.util.jar.JarInputStream;
23
import java.util.jar.JarInputStream;
24
import java.util.zip.ZipEntry;
25
import java.util.zip.ZipInputStream;
24
26
25
import org.apache.juli.logging.Log;
27
import org.apache.juli.logging.Log;
26
import org.apache.juli.logging.LogFactory;
28
import org.apache.juli.logging.LogFactory;
Lines 47-71 Link Here
47
49
48
    @Override
50
    @Override
49
    protected JarInputStreamWrapper getJarInputStreamWrapper() {
51
    protected JarInputStreamWrapper getJarInputStreamWrapper() {
50
        JarFile warFile = null;
51
        JarInputStream jarIs = null;
52
        JarEntry entry = null;
53
        try {
52
        try {
54
            warFile = getArchiveResourceSet().openJarFile();
53
            JarEntry entry = getArchiveResourceSet().getArchiveEntries(false).get(getResource().getName());
55
            JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
56
            InputStream isInWar = warFile.getInputStream(jarFileInWar);
57
58
            jarIs = new JarInputStream(isInWar);
59
            entry = jarIs.getNextJarEntry();
60
            while (entry != null &&
61
                    !entry.getName().equals(getResource().getName())) {
62
                entry = jarIs.getNextJarEntry();
63
            }
64
65
            if (entry == null) {
54
            if (entry == null) {
66
                return null;
55
                return null;
67
            }
56
            }
57
            assert entry.getName() != null;
68
58
59
            JarFile warFile = getArchiveResourceSet().openJarFile();
60
            JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
61
            InputStream isInWar = warFile.getInputStream(jarFileInWar);
62
            ZipInputStream jarIs = null;
63
            ZipEntry searchedEntry = null;
64
65
            Long skipPos = ((JarWarResourceSet)getArchiveResourceSet()).getArchiveEntryPositions().get(entry.getName());
66
            if(skipPos != null) {
67
                // parser did work correctly, fast forward
68
                isInWar.skip(skipPos);
69
                jarIs = new ZipInputStream(isInWar);
70
                searchedEntry = jarIs.getNextEntry();
71
            } else {
72
                // parser failed to parse zip file, use old seach method
73
                jarIs = new JarInputStream(isInWar);
74
                searchedEntry = jarIs.getNextEntry();
75
                while (searchedEntry != null && !entry.getName().equals(searchedEntry.getName())) {
76
                    searchedEntry = jarIs.getNextEntry();
77
                }
78
            }
79
            assert entry.getName().equals(searchedEntry.getName());
69
            return new JarInputStreamWrapper(entry, jarIs);
80
            return new JarInputStreamWrapper(entry, jarIs);
70
        } catch (IOException e) {
81
        } catch (IOException e) {
71
            if (log.isDebugEnabled()) {
82
            if (log.isDebugEnabled()) {
Lines 73-91 Link Here
73
                        getResource().getName(), getBaseUrl()), e);
84
                        getResource().getName(), getBaseUrl()), e);
74
            }
85
            }
75
            return null;
86
            return null;
76
        } finally {
77
            if (entry == null) {
78
                if (jarIs != null) {
79
                    try {
80
                        jarIs.close();
81
                    } catch (IOException ioe) {
82
                        // Ignore
83
                    }
84
                }
85
                if (warFile != null) {
86
                    getArchiveResourceSet().closeJarFile();
87
                }
88
            }
89
        }
87
        }
90
    }
88
    }
91
89
(-)a/java/org/apache/catalina/webresources/JarWarResourceSet.java (-12 / +196 lines)
Lines 16-26 Link Here
16
 */
16
 */
17
package org.apache.catalina.webresources;
17
package org.apache.catalina.webresources;
18
18
19
import java.io.Closeable;
19
import java.io.File;
20
import java.io.File;
20
import java.io.IOException;
21
import java.io.IOException;
21
import java.io.InputStream;
22
import java.io.InputStream;
22
import java.net.MalformedURLException;
23
import java.net.MalformedURLException;
24
import java.nio.charset.StandardCharsets;
25
import java.util.Collections;
26
import java.util.EventListener;
27
import java.util.EventObject;
23
import java.util.HashMap;
28
import java.util.HashMap;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.concurrent.CopyOnWriteArrayList;
24
import java.util.jar.JarEntry;
32
import java.util.jar.JarEntry;
25
import java.util.jar.JarFile;
33
import java.util.jar.JarFile;
26
import java.util.jar.JarInputStream;
34
import java.util.jar.JarInputStream;
Lines 39-44 Link Here
39
public class JarWarResourceSet extends AbstractArchiveResourceSet {
47
public class JarWarResourceSet extends AbstractArchiveResourceSet {
40
48
41
    private final String archivePath;
49
    private final String archivePath;
50
    private Map<String, Long> archiveEntryPositions;
42
51
43
    /**
52
    /**
44
     * Creates a new {@link org.apache.catalina.WebResourceSet} based on a JAR
53
     * Creates a new {@link org.apache.catalina.WebResourceSet} based on a JAR
Lines 94-117 Link Here
94
     * returned.
103
     * returned.
95
     */
104
     */
96
    @Override
105
    @Override
97
    protected HashMap<String,JarEntry> getArchiveEntries(boolean single) {
106
    protected Map<String,JarEntry> getArchiveEntries(boolean single) {
98
        synchronized (archiveLock) {
107
        synchronized (archiveLock) {
99
            if (archiveEntries == null) {
108
            if (archiveEntries == null) {
100
                JarFile warFile = null;
109
                JarFile warFile = null;
101
                InputStream jarFileIs = null;
102
                archiveEntries = new HashMap<>();
110
                archiveEntries = new HashMap<>();
111
                archiveEntryPositions = new HashMap<>();
112
103
                try {
113
                try {
104
                    warFile = openJarFile();
114
                    warFile = openJarFile();
105
                    JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
115
                    JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
106
                    jarFileIs = warFile.getInputStream(jarFileInWar);
107
116
108
                    try (JarInputStream jarIs = new JarInputStream(jarFileIs)) {
117
                    /* process and validate the JAR file */
118
                    try (InputStream jarFileIs = warFile.getInputStream(jarFileInWar);
119
                        JarInputStream jarIs = new JarInputStream(jarFileIs)) {
109
                        JarEntry entry = jarIs.getNextJarEntry();
120
                        JarEntry entry = jarIs.getNextJarEntry();
110
                        while (entry != null) {
121
                        while (entry != null) {
111
                            archiveEntries.put(entry.getName(), entry);
122
                            archiveEntries.put(entry.getName(), entry);
112
                            entry = jarIs.getNextJarEntry();
123
                            entry = jarIs.getNextJarEntry();
113
                        }
124
                        }
114
                        setManifest(jarIs.getManifest());
125
                        setManifest(jarIs.getManifest());
126
                    }
127
128
                    /* process again, this time determine the positions of all PK entries in the InputStream */
129
                    try (InputStream jarFileIs = warFile.getInputStream(jarFileInWar);
130
                        ZipEntryParser zipParser = new ZipEntryParser(jarFileIs)) {
131
                        /* sadly we are still stuck with Java 7... */
132
                        ZipEventListener listener = new ZipEventListener() {
133
                            @Override
134
                            public void onZipEntry(ZipEvent ze) {
135
                                archiveEntryPositions.put(ze.entryName, ze.position);
136
                            }
137
                        };
138
                        zipParser.addEventListener(listener);
139
                        zipParser.run();
115
                    }
140
                    }
116
                } catch (IOException ioe) {
141
                } catch (IOException ioe) {
117
                    // Should never happen
142
                    // Should never happen
Lines 121-136 Link Here
121
                    if (warFile != null) {
146
                    if (warFile != null) {
122
                        closeJarFile();
147
                        closeJarFile();
123
                    }
148
                    }
124
                    if (jarFileIs != null) {
125
                        try {
126
                            jarFileIs.close();
127
                        } catch (IOException e) {
128
                            // Ignore
129
                        }
130
                    }
131
                }
149
                }
132
            }
150
            }
133
            return archiveEntries;
151
            return Collections.unmodifiableMap(archiveEntries);
134
        }
152
        }
135
    }
153
    }
136
154
Lines 146-151 Link Here
146
        throw new IllegalStateException("Coding error");
164
        throw new IllegalStateException("Coding error");
147
    }
165
    }
148
166
167
    public Map<String, Long> getArchiveEntryPositions() {
168
        return Collections.unmodifiableMap(archiveEntryPositions);
169
    }
149
170
150
    //-------------------------------------------------------- Lifecycle methods
171
    //-------------------------------------------------------- Lifecycle methods
151
    @Override
172
    @Override
Lines 169-171 Link Here
169
        }
190
        }
170
    }
191
    }
171
}
192
}
193
194
class ZipEvent extends EventObject {
195
    private static final long serialVersionUID = 1L;
196
    final long position;
197
    final String entryName;
198
    public ZipEvent(Object source, long position, String entryName) {
199
        super(source);
200
        this.position = position;
201
        this.entryName = entryName;
202
    }
203
}
204
205
interface ZipEventListener extends EventListener {
206
    void onZipEntry(ZipEvent ze);
207
}
208
209
/**
210
 * Parses a ZIP format from an InputStream and determines the start offset in the stream
211
 * for all valid PK entries.
212
 * If parser encounters something it can't handle it will early abort processing
213
 * See also https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
214
 */
215
class ZipEntryParser implements Runnable, Closeable {
216
    private static final long SIG_LOCAL_FILE_HEADER = 0x04034b50l;
217
    private static final long SIG_CENTRAL_DIRECTORY_HEADER = 0x02014b50l;
218
219
    private List<ZipEventListener> listeners = new CopyOnWriteArrayList<>();
220
    private long bytesRead;
221
    private InputStream in;
222
223
    public ZipEntryParser(final InputStream in) throws IOException {
224
        this.in = in;
225
    }
226
227
    public void addEventListener(ZipEventListener listener) {
228
        listeners.add(listener);
229
    }
230
231
    @Override
232
    public void run() {
233
        try {
234
            long signature = fromLittleEndian(in, 4);
235
            while(signature == SIG_LOCAL_FILE_HEADER) {
236
                // remember current position
237
                long pos = bytesRead - 4l;
238
                if(pos < 0) return; // something is fishy
239
240
                // skip "version needed to extract"
241
                skip(2);
242
243
                long flags = fromLittleEndian(in, 2);
244
                long compressionMethod = fromLittleEndian(in, 2);
245
246
                // skip "last mod file time", "last mod file date", "crc-32"
247
                skip(8);
248
                long compressedSize = fromLittleEndian(in, 4);
249
                long uncompressedSize = fromLittleEndian(in, 4);
250
                long fileNameLength = fromLittleEndian(in, 2);
251
                long extraFieldLength = fromLittleEndian(in, 2);
252
                byte[] fileName = new byte[(int) fileNameLength];
253
                long lex = fileNameLength, l = in.read(fileName);
254
                if(l < lex) return;
255
                count(l);
256
257
                boolean searchNextLocalFileSignature = false;
258
                if((flags & 8l) > 1) { // correct values of "compressedSize" et al. are in data descriptor
259
                    if(compressionMethod == 0) { // file is stored, give up
260
                        return;
261
                    } else {
262
                        searchNextLocalFileSignature = true;
263
                    }
264
                }
265
266
                skip(extraFieldLength);
267
268
                // we did fully parse the "local file header", fire event now
269
                fireEvent(new ZipEvent(in, pos, new String(fileName, StandardCharsets.UTF_8))); // always assume UTF-8
270
271
                skip(compressedSize);
272
273
                if(searchNextLocalFileSignature) {
274
                    signature = searchNextSignature();
275
                } else {
276
                    signature = fromLittleEndian(in, 4);
277
                }
278
            }
279
        } catch(Exception e) {
280
            // something bad happened, abort
281
        }
282
    }
283
284
    /**
285
     * we try our luck and search for the next local file header resp. central directory header signature in the stream,
286
     * this can result in wrong results
287
     * @throws IOException in case of an IO error
288
     */
289
    private long searchNextSignature() throws IOException {
290
        long sig = fromLittleEndian(in, 4);
291
        while(!(sig == SIG_LOCAL_FILE_HEADER || sig == SIG_CENTRAL_DIRECTORY_HEADER)) {
292
            int b = in.read();
293
            if(b < 0) {
294
                return -1;
295
            }
296
            count(1);
297
            sig = ((sig >> 8) + (b << 24)) & 0xff_ff_ff_ffl;
298
        }
299
        return sig;
300
    }
301
302
    private void fireEvent(ZipEvent zipEvent) {
303
        for(ZipEventListener l : listeners) {
304
            l.onZipEntry(zipEvent);
305
        }
306
    }
307
308
    private long skip(final long len) throws IOException {
309
        final long al = in.skip(len);
310
        if(al < len)
311
            throw new IOException("premature end of data");
312
        count(al);
313
        return al;
314
    }
315
316
    /**
317
     * Reads the given number of bytes from the given stream as a little endian long.
318
     * @param in the stream to read from
319
     * @param length the number of bytes representing the value
320
     * @return the number read
321
     * @throws IllegalArgumentException if len is bigger than eight
322
     * @throws IOException if reading fails or the stream doesn't
323
     * contain the given number of bytes anymore
324
     */
325
    public long fromLittleEndian(InputStream in, int length) throws IOException {
326
        long l = 0;
327
        for (int i = 0; i < length; i++) {
328
            long b = in.read();
329
            if (b == -1) {
330
                throw new IOException("premature end of data");
331
            }
332
            count(1);
333
            l |= (b << (i * 8));
334
        }
335
        return l;
336
    }
337
    /**
338
     * Increments the counter of already read bytes.
339
     * Doesn't increment if the EOF has been hit (read == -1)
340
     *
341
     * @param read the number of bytes read
342
     */
343
    private void count(final long read) {
344
        if (read != -1) {
345
            bytesRead += read;
346
        }
347
    }
348
349
    @Override
350
    public void close() throws IOException {
351
        if(in != null) {
352
            in.close();
353
        }
354
    }
355
}

Return to bug 60963