• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ebourg / jsign / #418

23 Jul 2026 12:50PM UTC coverage: 81.28% (+0.5%) from 80.829%
#418

push

ebourg
CodeQL workflow

5397 of 6640 relevant lines covered (81.28%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.97
/jsign-core/src/main/java/net/jsign/zip/ZipFile.java
1
/*
2
 * Copyright 2023 Emmanuel Bourg
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 net.jsign.zip;
18

19
import java.io.ByteArrayOutputStream;
20
import java.io.Closeable;
21
import java.io.File;
22
import java.io.IOException;
23
import java.io.InputStream;
24
import java.nio.ByteBuffer;
25
import java.nio.channels.Channels;
26
import java.nio.channels.SeekableByteChannel;
27
import java.util.zip.CRC32;
28
import java.util.zip.Deflater;
29
import java.util.zip.DeflaterOutputStream;
30
import java.util.zip.Inflater;
31
import java.util.zip.InflaterInputStream;
32

33
import org.apache.commons.io.input.BoundedInputStream;
34

35
import net.jsign.ChannelUtils;
36

37
import static java.nio.charset.StandardCharsets.*;
38

39
/**
40
 * Simplified implementation of the ZIP file format, just good enough to add an entry to an existing file.
41
 *
42
 * @since 6.0
43
 */
44
public class ZipFile implements Closeable {
45

46
    /** The channel used for in-memory signing */
47
    protected final SeekableByteChannel channel;
48

49
    protected CentralDirectory centralDirectory;
50

51
    /**
52
     * Create a ZipFile from the specified file.
53
     *
54
     * @param file the file to open
55
     * @throws IOException if an I/O error occurs
56
     */
57
    public ZipFile(File file) throws IOException {
58
        this(ChannelUtils.openReadWriteOrReadOnly(file));
1✔
59
    }
1✔
60

61
    /**
62
     * Create a ZipFile from the specified channel.
63
     *
64
     * @param channel the channel to read the file from
65
     * @throws IOException if an I/O error occurs
66
     */
67
    public ZipFile(SeekableByteChannel channel) throws IOException {
1✔
68
        this.channel = channel;
1✔
69
        centralDirectory = new CentralDirectory();
1✔
70
        centralDirectory.read(channel);
1✔
71
    }
1✔
72

73
    public InputStream getInputStream(String name) throws IOException {
74
        return getInputStream(name, -1);
1✔
75
    }
76

77
    public InputStream getInputStream(String name, int limit) throws IOException {
78
        CentralDirectoryFileHeader header = centralDirectory.entries.get(name);
1✔
79
        if (header == null) {
1✔
80
            throw new IOException("Entry not found: " + name);
1✔
81
        }
82
        if (limit != -1 && header.getUncompressedSize() > limit) {
1✔
83
            throw new IOException("The entry " + name + " is too large to be read (" + header.getUncompressedSize() + " bytes)");
1✔
84
        }
85
        channel.position(header.getLocalHeaderOffset());
1✔
86

87
        LocalFileHeader localFileHeader = new LocalFileHeader();
1✔
88
        localFileHeader.read(channel);
1✔
89
        InputStream in = Channels.newInputStream(channel);
1✔
90
        in = new BoundedInputStream(in, header.getCompressedSize());
1✔
91
        switch (header.compressionMethod) {
1✔
92
            case 0 /* STORED */:
93
                return in;
1✔
94
            case 8 /* DEFLATED */:
95
                Inflater inflater = new Inflater(true);
1✔
96
                return new InflaterInputStream(in, inflater);
1✔
97
            default:
98
                throw new IOException("Unsupported compression method " + header.compressionMethod + " for entry " + name);
×
99
        }
100
    }
101

102
    public void addEntry(String name, byte[] data, boolean compressed) throws IOException {
103
        // compute CRC32 of the uncompressed data
104
        CRC32 crc32 = new CRC32();
1✔
105
        crc32.update(data);
1✔
106

107
        int uncompressedSize = data.length;
1✔
108
        int compressedSize;
109

110
        if (compressed) {
1✔
111
            // deflate the data
112
            Deflater deflater = new Deflater(9, true);
1✔
113
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
1✔
114
            DeflaterOutputStream dos = new DeflaterOutputStream(bos, deflater);
1✔
115
            dos.write(data);
1✔
116
            dos.flush();
1✔
117
            dos.close();
1✔
118

119
            data = bos.toByteArray();
1✔
120
            compressedSize = data.length;
1✔
121
        } else {
1✔
122
            compressedSize = uncompressedSize;
1✔
123
        }
124

125
        LocalFileHeader localFileHeader = new LocalFileHeader();
1✔
126
        localFileHeader.versionNeededToExtract = 20;
1✔
127
        localFileHeader.generalPurposeBitFlag = 0;
1✔
128
        localFileHeader.compressionMethod = compressed ? 8 : 0;
1✔
129
        localFileHeader.lastModFileTime = 0b00000_00000_00000; // 00:00:00
1✔
130
        localFileHeader.lastModFileDate = 0b0000000_0001_00001; // 1980-01-01
1✔
131
        localFileHeader.crc32 = (int) crc32.getValue();
1✔
132
        localFileHeader.compressedSize = compressedSize;
1✔
133
        localFileHeader.uncompressedSize = uncompressedSize;
1✔
134
        localFileHeader.fileName = name.getBytes(UTF_8);
1✔
135

136
        channel.position(centralDirectory.centralDirectoryOffset);
1✔
137
        long offset = channel.position();
1✔
138
        localFileHeader.write(channel);
1✔
139
        channel.write(ByteBuffer.wrap(data));
1✔
140

141
        boolean needsZip64 = offset > 0xFFFFFFFFL;
1✔
142

143
        CentralDirectoryFileHeader centralDirectoryFileHeader = new CentralDirectoryFileHeader();
1✔
144
        centralDirectoryFileHeader.versionMadeBy = 45;
1✔
145
        centralDirectoryFileHeader.versionNeededToExtract = 20;
1✔
146
        centralDirectoryFileHeader.generalPurposeBitFlag = localFileHeader.generalPurposeBitFlag;
1✔
147
        centralDirectoryFileHeader.compressionMethod = localFileHeader.compressionMethod;
1✔
148
        centralDirectoryFileHeader.lastModFileTime = localFileHeader.lastModFileTime;
1✔
149
        centralDirectoryFileHeader.lastModFileDate = localFileHeader.lastModFileDate;
1✔
150
        centralDirectoryFileHeader.crc32 = localFileHeader.crc32;
1✔
151
        centralDirectoryFileHeader.compressedSize = localFileHeader.compressedSize;
1✔
152
        centralDirectoryFileHeader.uncompressedSize = uncompressedSize;
1✔
153
        centralDirectoryFileHeader.diskNumberStart = 0;
1✔
154
        centralDirectoryFileHeader.internalFileAttributes = 0;
1✔
155
        centralDirectoryFileHeader.externalFileAttributes = 0;
1✔
156
        centralDirectoryFileHeader.localHeaderOffset = needsZip64 ? 0xFFFFFFFFL : offset;
1✔
157
        centralDirectoryFileHeader.fileName = localFileHeader.fileName;
1✔
158

159
        if (needsZip64) {
1✔
160
            Zip64ExtendedInfoExtraField zip64ExtraField = new Zip64ExtendedInfoExtraField(-1, -1, offset, -1);
×
161
            centralDirectoryFileHeader.extraFields.put(zip64ExtraField.id, zip64ExtraField);
×
162
        }
163

164
        centralDirectory.entries.put(name, centralDirectoryFileHeader);
1✔
165

166
        centralDirectory.write(channel);
1✔
167
    }
1✔
168

169
    public void renameEntry(String oldName, String newName) throws IOException {
170
        if (oldName.length() != newName.length()) {
1✔
171
            throw new IllegalArgumentException("The new name must have the same length");
1✔
172
        }
173
        CentralDirectoryFileHeader centralDirectoryFileHeader = centralDirectory.entries.get(oldName);
1✔
174
        centralDirectoryFileHeader.fileName = newName.getBytes(UTF_8);
1✔
175
        centralDirectory.entries.remove(oldName);
1✔
176
        centralDirectory.entries.put(newName, centralDirectoryFileHeader);
1✔
177

178
        long offset = centralDirectoryFileHeader.getLocalHeaderOffset();
1✔
179
        channel.position(offset);
1✔
180
        LocalFileHeader localFileHeader = new LocalFileHeader();
1✔
181
        localFileHeader.read(channel);
1✔
182
        localFileHeader.fileName = newName.getBytes(UTF_8);
1✔
183
        channel.position(offset);
1✔
184
        localFileHeader.write(channel);
1✔
185

186
        channel.position(centralDirectory.centralDirectoryOffset);
1✔
187
        centralDirectory.write(channel);
1✔
188
    }
1✔
189

190
    public void removeEntry(String name) throws IOException {
191
        CentralDirectoryFileHeader centralDirectoryFileHeader = centralDirectory.entries.get(name);
1✔
192
        ChannelUtils.delete(channel, centralDirectoryFileHeader.getLocalHeaderOffset(), centralDirectory.getEntrySize(name));
1✔
193

194
        centralDirectory.removeEntry(name);
1✔
195

196
        channel.position(centralDirectory.centralDirectoryOffset);
1✔
197
        centralDirectory.write(channel);
1✔
198
        channel.truncate(channel.position());
1✔
199
    }
1✔
200

201
    @Override
202
    public void close() throws IOException {
203
        if (channel != null) {
1✔
204
            channel.close();
1✔
205
        }
206
    }
1✔
207
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc