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.IOException; 020import java.nio.ByteBuffer; 021import java.nio.channels.ReadableByteChannel; 022import java.util.LinkedHashMap; 023import java.util.Map; 024 025import static java.nio.ByteOrder.*; 026 027/** 028 * Central Directory File Header: 029 * 030 * <pre> 031 * central file header signature 4 bytes (0x02014b50) 032 * version made by 2 bytes 033 * version needed to extract 2 bytes 034 * general purpose bit flag 2 bytes 035 * compression method 2 bytes 036 * last mod file time 2 bytes 037 * last mod file date 2 bytes 038 * crc-32 4 bytes 039 * compressed size 4 bytes 040 * uncompressed size 4 bytes 041 * file name length 2 bytes 042 * extra field length 2 bytes 043 * file comment length 2 bytes 044 * disk number start 2 bytes 045 * internal file attributes 2 bytes 046 * external file attributes 4 bytes 047 * relative offset of local header 4 bytes 048 * 049 * file name (variable size) 050 * extra field (variable size) 051 * file comment (variable size) 052 * </pre> 053 * 054 * @since 6.0 055 */ 056public class CentralDirectoryFileHeader extends ZipRecord { 057 058 public static final int SIGNATURE = 0x02014b50; 059 private static final int MIN_SIZE = 46; 060 061 public int versionMadeBy; 062 public int versionNeededToExtract; 063 public int generalPurposeBitFlag; 064 public int compressionMethod; 065 public int lastModFileTime; 066 public int lastModFileDate; 067 public int crc32; 068 public long compressedSize; 069 public long uncompressedSize; 070 public int diskNumberStart; 071 public int internalFileAttributes; 072 public int externalFileAttributes; 073 public long localHeaderOffset; 074 public byte[] fileName = new byte[0]; 075 public byte[] fileComment = new byte[0]; 076 077 public Map<Integer, ExtraField> extraFields = new LinkedHashMap<>(); 078 079 @Override 080 public void read(ReadableByteChannel channel) throws IOException { 081 ByteBuffer buffer = ByteBuffer.allocate(MIN_SIZE).order(LITTLE_ENDIAN); 082 channel.read(buffer); 083 buffer.flip(); 084 if (buffer.remaining() < MIN_SIZE) { 085 throw new IOException("Invalid Central Directory File Header"); 086 } 087 088 int signature = buffer.getInt(); 089 if (signature != SIGNATURE) { 090 throw new IOException("Invalid Central Directory File Header signature " + String.format("0x%04x", signature & 0xFFFFFFFFL)); 091 } 092 versionMadeBy = buffer.getShort(); 093 versionNeededToExtract = buffer.getShort(); 094 generalPurposeBitFlag = buffer.getShort(); 095 compressionMethod = buffer.getShort(); 096 lastModFileTime = buffer.getShort(); 097 lastModFileDate = buffer.getShort(); 098 crc32 = buffer.getInt(); 099 compressedSize = buffer.getInt() & 0xFFFFFFFFL; 100 uncompressedSize = buffer.getInt() & 0xFFFFFFFFL; 101 int fileNameLength = buffer.getShort() & 0xFFFF; 102 int extraFieldsLength = buffer.getShort() & 0xFFFF; 103 int fileCommentLength = buffer.getShort() & 0xFFFF; 104 diskNumberStart = buffer.getShort(); 105 internalFileAttributes = buffer.getShort(); 106 externalFileAttributes = buffer.getInt(); 107 localHeaderOffset = buffer.getInt() & 0xFFFFFFFFL; 108 if (fileNameLength > 0) { 109 fileName = new byte[fileNameLength]; 110 channel.read(ByteBuffer.wrap(fileName)); 111 } 112 if (extraFieldsLength > 0) { 113 byte[] extraFields = new byte[extraFieldsLength]; 114 channel.read(ByteBuffer.wrap(extraFields)); 115 116 this.extraFields = ExtraField.parseAll(ByteBuffer.wrap(extraFields).order(LITTLE_ENDIAN), 117 uncompressedSize == 0xFFFFFFFFL, 118 compressedSize == 0xFFFFFFFFL, 119 localHeaderOffset == 0xFFFFFFFFL, 120 diskNumberStart == 0xFFFF); 121 } 122 if (fileCommentLength > 0) { 123 fileComment = new byte[fileCommentLength]; 124 channel.read(ByteBuffer.wrap(fileComment)); 125 } 126 127 // validate the offset and sizes 128 if (!extraFields.containsKey(1) && (localHeaderOffset == 0xFFFFFFFFL || compressedSize == 0xFFFFFFFFL || uncompressedSize == 0xFFFFFFFFL)) { 129 throw new IOException("Missing ZIP64 extra field in the Central Directory File Header"); 130 } 131 } 132 133 private int getExtraFieldsLength() { 134 int length = 0; 135 for (ExtraField field : extraFields.values()) { 136 length += field.size(); 137 } 138 return length; 139 } 140 141 @Override 142 public ByteBuffer toBuffer() { 143 ByteBuffer buffer = ByteBuffer.allocate(MIN_SIZE + fileName.length + getExtraFieldsLength() + fileComment.length).order(LITTLE_ENDIAN); 144 buffer.putInt(SIGNATURE); 145 buffer.putShort((short) versionMadeBy); 146 buffer.putShort((short) versionNeededToExtract); 147 buffer.putShort((short) generalPurposeBitFlag); 148 buffer.putShort((short) compressionMethod); 149 buffer.putShort((short) lastModFileTime); 150 buffer.putShort((short) lastModFileDate); 151 buffer.putInt(crc32); 152 buffer.putInt((int) compressedSize); 153 buffer.putInt((int) uncompressedSize); 154 buffer.putShort((short) fileName.length); 155 buffer.putShort((short) getExtraFieldsLength()); 156 buffer.putShort((short) fileComment.length); 157 buffer.putShort((short) diskNumberStart); 158 buffer.putShort((short) internalFileAttributes); 159 buffer.putInt(externalFileAttributes); 160 buffer.putInt((int) localHeaderOffset); 161 buffer.put(fileName); 162 if (!extraFields.isEmpty()) { 163 for (ExtraField field : extraFields.values()) { 164 field.write(buffer); 165 } 166 } 167 buffer.put(fileComment); 168 buffer.flip(); 169 170 return buffer; 171 } 172 173 public long getCompressedSize() { 174 if (compressedSize == 0xFFFFFFFFL) { 175 Zip64ExtendedInfoExtraField zip64ExtraField = (Zip64ExtendedInfoExtraField) extraFields.get(1); 176 return zip64ExtraField.compressedSize; 177 } else { 178 return compressedSize; 179 } 180 } 181 182 public long getUncompressedSize() { 183 if (uncompressedSize == 0xFFFFFFFFL) { 184 Zip64ExtendedInfoExtraField zip64ExtraField = (Zip64ExtendedInfoExtraField) extraFields.get(1); 185 return zip64ExtraField.uncompressedSize; 186 } else { 187 return uncompressedSize; 188 } 189 } 190 191 public long getLocalHeaderOffset() { 192 if (localHeaderOffset == 0xFFFFFFFFL) { 193 Zip64ExtendedInfoExtraField zip64ExtraField = (Zip64ExtendedInfoExtraField) extraFields.get(1); 194 return zip64ExtraField.localHeaderOffset; 195 } else { 196 return localHeaderOffset; 197 } 198 } 199 200 public void setLocalHeaderOffset(long offset) { 201 if (offset > 0xFFFFFFFFL || localHeaderOffset == 0xFFFFFFFFL) { 202 Zip64ExtendedInfoExtraField zip64ExtraField = (Zip64ExtendedInfoExtraField) extraFields.get(1); 203 zip64ExtraField.localHeaderOffset = offset; 204 localHeaderOffset = 0xFFFFFFFFL; 205 } else { 206 localHeaderOffset = offset; 207 } 208 } 209}