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}