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.File; 020import java.io.IOException; 021import java.io.RandomAccessFile; 022import java.nio.channels.SeekableByteChannel; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.Files; 025import java.util.ArrayList; 026import java.util.Comparator; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030 031/** 032 * Central directory of a ZIP file. 033 * 034 * @since 6.0 035 */ 036public class CentralDirectory { 037 038 private final EndOfCentralDirectoryRecord endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord(); 039 private final Zip64EndOfCentralDirectoryLocator zip64EndOfCentralDirectoryLocator = new Zip64EndOfCentralDirectoryLocator(); 040 private final Zip64EndOfCentralDirectoryRecord zip64EndOfCentralDirectoryRecord = new Zip64EndOfCentralDirectoryRecord(); 041 042 /** The location of the central directory */ 043 public long centralDirectoryOffset = -1; 044 045 /** The entries of the central directory */ 046 public Map<String, CentralDirectoryFileHeader> entries = new LinkedHashMap<>(); 047 048 public void read(SeekableByteChannel channel) throws IOException { 049 endOfCentralDirectoryRecord.load(channel); 050 if (endOfCentralDirectoryRecord.numberOfThisDisk > 0) { 051 throw new IOException("Multi-volume archives are not supported"); 052 } 053 054 long numberOfEntries; 055 056 if (endOfCentralDirectoryRecord.centralDirectoryOffset == -1) { 057 // look for the ZIP64 End of Central Directory Locator 058 channel.position(channel.position() - Zip64EndOfCentralDirectoryLocator.SIZE); 059 zip64EndOfCentralDirectoryLocator.read(channel); 060 061 // read the ZIP64 End of Central Directory Record 062 channel.position(zip64EndOfCentralDirectoryLocator.zip64EndOfCentralDirectoryRecordOffset); 063 zip64EndOfCentralDirectoryRecord.read(channel); 064 065 centralDirectoryOffset = zip64EndOfCentralDirectoryRecord.centralDirectoryOffset; 066 numberOfEntries = (int) zip64EndOfCentralDirectoryRecord.numberOfEntries; 067 } else { 068 centralDirectoryOffset = endOfCentralDirectoryRecord.centralDirectoryOffset; 069 numberOfEntries = endOfCentralDirectoryRecord.numberOfEntries; 070 } 071 072 // check if the offset is valid 073 if (centralDirectoryOffset < 0 || centralDirectoryOffset > channel.size()) { 074 throw new IOException("Invalid central directory offset: " + centralDirectoryOffset); 075 } 076 077 // read the entries 078 channel.position(centralDirectoryOffset); 079 for (int i = 0; i < numberOfEntries; i++) { 080 CentralDirectoryFileHeader entry = new CentralDirectoryFileHeader(); 081 entry.read(channel); 082 entries.put(new String(entry.fileName, StandardCharsets.ISO_8859_1), entry); 083 } 084 } 085 086 /** 087 * Write the central directory at the current position of the channel and update the offset. 088 * 089 * @param channel the channel to write to 090 */ 091 public void write(SeekableByteChannel channel) throws IOException { 092 long offset = channel.position(); 093 centralDirectoryOffset = offset; 094 write(channel, offset); 095 } 096 097 /** 098 * Write the central directory at the current position of the channel but don't update the offset. 099 * 100 * @param channel the channel to write to 101 * @param offset the offset of the central directory written in the End of Central Directory Record 102 */ 103 public void write(SeekableByteChannel channel, long offset) throws IOException { 104 // sort and write the entries 105 List<CentralDirectoryFileHeader> entries = new ArrayList<>(this.entries.values()); 106 entries.sort(Comparator.comparing(CentralDirectoryFileHeader::getLocalHeaderOffset)); 107 long position = channel.position(); 108 for (CentralDirectoryFileHeader entry : entries) { 109 entry.write(channel); 110 } 111 112 long centralDirectorySize = channel.position() - position; 113 114 // write the End of Central Directory Record 115 if (endOfCentralDirectoryRecord.centralDirectoryOffset == -1 || offset > 0xFFFFFFFFL) { 116 endOfCentralDirectoryRecord.centralDirectoryOffset = -1; 117 endOfCentralDirectoryRecord.centralDirectorySize = -1; 118 119 zip64EndOfCentralDirectoryRecord.numberOfEntriesOnThisDisk = entries.size(); 120 zip64EndOfCentralDirectoryRecord.numberOfEntries = entries.size(); 121 zip64EndOfCentralDirectoryRecord.centralDirectorySize = centralDirectorySize; 122 zip64EndOfCentralDirectoryRecord.centralDirectoryOffset = offset; 123 zip64EndOfCentralDirectoryRecord.write(channel); 124 125 zip64EndOfCentralDirectoryLocator.zip64EndOfCentralDirectoryRecordOffset = offset + centralDirectorySize; 126 zip64EndOfCentralDirectoryLocator.write(channel); 127 128 } else { 129 endOfCentralDirectoryRecord.numberOfEntriesOnThisDisk = entries.size(); 130 endOfCentralDirectoryRecord.numberOfEntries = entries.size(); 131 endOfCentralDirectoryRecord.centralDirectorySize = (int) centralDirectorySize; 132 endOfCentralDirectoryRecord.centralDirectoryOffset = (int) offset; 133 } 134 135 endOfCentralDirectoryRecord.numberOfThisDisk = 0; 136 endOfCentralDirectoryRecord.numberOfTheDiskWithTheStartOfTheCentralDirectory = 0; 137 endOfCentralDirectoryRecord.write(channel); 138 } 139 140 /** 141 * Removes the entry specified if it exists. Only the last entry can be removed. 142 * 143 * @param name the name of the entry to remove 144 */ 145 public void removeEntry(String name) { 146 if (entries.containsKey(name)) { 147 CentralDirectoryFileHeader centralDirectoryFileHeader = entries.get(name); 148 long size = getEntrySize(name); 149 150 // remove the entry 151 entries.remove(name); 152 153 // shift the central directory offset 154 centralDirectoryOffset = centralDirectoryOffset - size; 155 156 // shift the local header offset of the following entries 157 for (CentralDirectoryFileHeader entry : entries.values()) { 158 if (entry.getLocalHeaderOffset() > centralDirectoryFileHeader.getLocalHeaderOffset()) { 159 entry.setLocalHeaderOffset(entry.getLocalHeaderOffset() - size); 160 } 161 } 162 } 163 } 164 165 /** 166 * Returns the size of the specified entry (local header + compressed data). 167 * 168 * @param name the name of the entry 169 * @since 7.0 170 */ 171 public long getEntrySize(String name) { 172 CentralDirectoryFileHeader centralDirectoryFileHeader = entries.get(name); 173 174 // the size is the smallest strictly positive distance between the offset and the entry and the others 175 long size = centralDirectoryOffset - centralDirectoryFileHeader.getLocalHeaderOffset(); 176 for (CentralDirectoryFileHeader entry : entries.values()) { 177 long distance = entry.getLocalHeaderOffset() - centralDirectoryFileHeader.getLocalHeaderOffset(); 178 if (distance > 0 && distance < size) { 179 size = distance; 180 } 181 } 182 183 return size; 184 } 185 186 /** 187 * Returns the central directory as a byte array. 188 */ 189 public byte[] toBytes() throws IOException { 190 File tmp = File.createTempFile("jsign-zip-central-directory", ".bin"); 191 tmp.deleteOnExit(); 192 try (RandomAccessFile raf = new RandomAccessFile(tmp, "rw")) { 193 write(raf.getChannel(), centralDirectoryOffset); 194 return Files.readAllBytes(tmp.toPath()); 195 } finally { 196 tmp.delete(); 197 } 198 } 199}