001/* 002 * Copyright 2023 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.zip; 018 019import java.io.ByteArrayOutputStream; 020import java.io.Closeable; 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.nio.ByteBuffer; 025import java.nio.channels.Channels; 026import java.nio.channels.SeekableByteChannel; 027import java.nio.file.Files; 028import java.nio.file.StandardOpenOption; 029import java.util.zip.CRC32; 030import java.util.zip.Deflater; 031import java.util.zip.DeflaterOutputStream; 032import java.util.zip.Inflater; 033import java.util.zip.InflaterInputStream; 034 035import org.apache.commons.io.input.BoundedInputStream; 036 037import net.jsign.ChannelUtils; 038 039import static java.nio.charset.StandardCharsets.*; 040 041/** 042 * Simplified implementation of the ZIP file format, just good enough to add an entry to an existing file. 043 * 044 * @since 6.0 045 */ 046public class ZipFile implements Closeable { 047 048 /** The channel used for in-memory signing */ 049 protected final SeekableByteChannel channel; 050 051 protected CentralDirectory centralDirectory; 052 053 /** 054 * Create a ZipFile from the specified file. 055 * 056 * @param file the file to open 057 * @throws IOException if an I/O error occurs 058 */ 059 public ZipFile(File file) throws IOException { 060 this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); 061 } 062 063 /** 064 * Create a ZipFile from the specified channel. 065 * 066 * @param channel the channel to read the file from 067 * @throws IOException if an I/O error occurs 068 */ 069 public ZipFile(SeekableByteChannel channel) throws IOException { 070 this.channel = channel; 071 centralDirectory = new CentralDirectory(); 072 centralDirectory.read(channel); 073 } 074 075 public InputStream getInputStream(String name) throws IOException { 076 return getInputStream(name, -1); 077 } 078 079 public InputStream getInputStream(String name, int limit) throws IOException { 080 CentralDirectoryFileHeader header = centralDirectory.entries.get(name); 081 if (header == null) { 082 throw new IOException("Entry not found: " + name); 083 } 084 if (limit != -1 && header.getUncompressedSize() > limit) { 085 throw new IOException("The entry " + name + " is too large to be read (" + header.getUncompressedSize() + " bytes)"); 086 } 087 channel.position(header.getLocalHeaderOffset()); 088 089 LocalFileHeader localFileHeader = new LocalFileHeader(); 090 localFileHeader.read(channel); 091 InputStream in = Channels.newInputStream(channel); 092 in = new BoundedInputStream(in, header.getCompressedSize()); 093 switch (header.compressionMethod) { 094 case 0 /* STORED */: 095 return in; 096 case 8 /* DEFLATED */: 097 Inflater inflater = new Inflater(true); 098 return new InflaterInputStream(in, inflater); 099 default: 100 throw new IOException("Unsupported compression method " + header.compressionMethod + " for entry " + name); 101 } 102 } 103 104 public void addEntry(String name, byte[] data, boolean compressed) throws IOException { 105 // compute CRC32 of the uncompressed data 106 CRC32 crc32 = new CRC32(); 107 crc32.update(data); 108 109 int uncompressedSize = data.length; 110 int compressedSize; 111 112 if (compressed) { 113 // deflate the data 114 Deflater deflater = new Deflater(9, true); 115 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 116 DeflaterOutputStream dos = new DeflaterOutputStream(bos, deflater); 117 dos.write(data); 118 dos.flush(); 119 dos.close(); 120 121 data = bos.toByteArray(); 122 compressedSize = data.length; 123 } else { 124 compressedSize = uncompressedSize; 125 } 126 127 LocalFileHeader localFileHeader = new LocalFileHeader(); 128 localFileHeader.versionNeededToExtract = 20; 129 localFileHeader.generalPurposeBitFlag = 0; 130 localFileHeader.compressionMethod = compressed ? 8 : 0; 131 localFileHeader.lastModFileTime = 0b00000_00000_00000; // 00:00:00 132 localFileHeader.lastModFileDate = 0b0000000_0001_00001; // 1980-01-01 133 localFileHeader.crc32 = (int) crc32.getValue(); 134 localFileHeader.compressedSize = compressedSize; 135 localFileHeader.uncompressedSize = uncompressedSize; 136 localFileHeader.fileName = name.getBytes(UTF_8); 137 138 channel.position(centralDirectory.centralDirectoryOffset); 139 long offset = channel.position(); 140 localFileHeader.write(channel); 141 channel.write(ByteBuffer.wrap(data)); 142 143 boolean needsZip64 = offset > 0xFFFFFFFFL; 144 145 CentralDirectoryFileHeader centralDirectoryFileHeader = new CentralDirectoryFileHeader(); 146 centralDirectoryFileHeader.versionMadeBy = 45; 147 centralDirectoryFileHeader.versionNeededToExtract = 20; 148 centralDirectoryFileHeader.generalPurposeBitFlag = localFileHeader.generalPurposeBitFlag; 149 centralDirectoryFileHeader.compressionMethod = localFileHeader.compressionMethod; 150 centralDirectoryFileHeader.lastModFileTime = localFileHeader.lastModFileTime; 151 centralDirectoryFileHeader.lastModFileDate = localFileHeader.lastModFileDate; 152 centralDirectoryFileHeader.crc32 = localFileHeader.crc32; 153 centralDirectoryFileHeader.compressedSize = localFileHeader.compressedSize; 154 centralDirectoryFileHeader.uncompressedSize = uncompressedSize; 155 centralDirectoryFileHeader.diskNumberStart = 0; 156 centralDirectoryFileHeader.internalFileAttributes = 0; 157 centralDirectoryFileHeader.externalFileAttributes = 0; 158 centralDirectoryFileHeader.localHeaderOffset = needsZip64 ? 0xFFFFFFFFL : offset; 159 centralDirectoryFileHeader.fileName = localFileHeader.fileName; 160 161 if (needsZip64) { 162 Zip64ExtendedInfoExtraField zip64ExtraField = new Zip64ExtendedInfoExtraField(-1, -1, offset, -1); 163 centralDirectoryFileHeader.extraFields.put(zip64ExtraField.id, zip64ExtraField); 164 } 165 166 centralDirectory.entries.put(name, centralDirectoryFileHeader); 167 168 centralDirectory.write(channel); 169 } 170 171 public void renameEntry(String oldName, String newName) throws IOException { 172 if (oldName.length() != newName.length()) { 173 throw new IllegalArgumentException("The new name must have the same length"); 174 } 175 CentralDirectoryFileHeader centralDirectoryFileHeader = centralDirectory.entries.get(oldName); 176 centralDirectoryFileHeader.fileName = newName.getBytes(UTF_8); 177 centralDirectory.entries.remove(oldName); 178 centralDirectory.entries.put(newName, centralDirectoryFileHeader); 179 180 long offset = centralDirectoryFileHeader.getLocalHeaderOffset(); 181 channel.position(offset); 182 LocalFileHeader localFileHeader = new LocalFileHeader(); 183 localFileHeader.read(channel); 184 localFileHeader.fileName = newName.getBytes(UTF_8); 185 channel.position(offset); 186 localFileHeader.write(channel); 187 188 channel.position(centralDirectory.centralDirectoryOffset); 189 centralDirectory.write(channel); 190 } 191 192 public void removeEntry(String name) throws IOException { 193 CentralDirectoryFileHeader centralDirectoryFileHeader = centralDirectory.entries.get(name); 194 ChannelUtils.delete(channel, centralDirectoryFileHeader.getLocalHeaderOffset(), centralDirectory.getEntrySize(name)); 195 196 centralDirectory.removeEntry(name); 197 198 channel.position(centralDirectory.centralDirectoryOffset); 199 centralDirectory.write(channel); 200 channel.truncate(channel.position()); 201 } 202 203 @Override 204 public void close() throws IOException { 205 if (channel != null) { 206 channel.close(); 207 } 208 } 209}