001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.archivers.zip;
020
021import java.util.zip.ZipException;
022
023/**
024 * An extra field who's sole purpose is to align and pad the local file header so that the entry's data starts at a certain position.
025 *
026 * <p>
027 * The padding content of the padding is ignored and not retained when reading a padding field.
028 * </p>
029 *
030 * <p>
031 * This enables Commons Compress to create "aligned" archives similar to Android's {@code zipalign} command line tool.
032 * </p>
033 *
034 * @since 1.14
035 * @see "https://developer.android.com/studio/command-line/zipalign.html"
036 * @see ZipArchiveEntry#setAlignment
037 */
038public class ResourceAlignmentExtraField implements ZipExtraField {
039
040    private static final int MAX_ALIGNMENT = Short.MAX_VALUE;
041
042    /**
043     * Extra field id used for storing alignment and padding.
044     */
045    public static final ZipShort ID = new ZipShort(0xa11e);
046
047    /**
048     * Base field size.
049     */
050    public static final int BASE_SIZE = 2;
051
052    private static final int ALLOW_METHOD_MESSAGE_CHANGE_FLAG = 0x8000;
053
054    private short alignment;
055
056    private boolean allowMethodChange;
057
058    private int padding;
059
060    /**
061     * Constructs a new instance.
062     */
063    public ResourceAlignmentExtraField() {
064    }
065
066    /**
067     * Constructs a new instance.
068     *
069     * @param alignment A positive alignment less than {@link Short#MAX_VALUE}.
070     */
071    public ResourceAlignmentExtraField(final int alignment) {
072        this(alignment, false);
073    }
074
075    /**
076     * Constructs a new instance.
077     *
078     * @param alignment A positive alignment less than {@link Short#MAX_VALUE}.
079     * @param allowMethodChange whether a method change is allowed when re-compressing the ZIP file.
080     */
081    public ResourceAlignmentExtraField(final int alignment, final boolean allowMethodChange) {
082        this(alignment, allowMethodChange, 0);
083    }
084
085    /**
086     * Constructs a new instance.
087     *
088     * @param alignment A positive alignment less than {@link Short#MAX_VALUE}.
089     * @param allowMethodChange whether a method change is allowed when re-compressing the ZIP file.
090     * @param padding padding.
091     */
092    public ResourceAlignmentExtraField(final int alignment, final boolean allowMethodChange, final int padding) {
093        if (alignment < 0 || alignment > MAX_ALIGNMENT) {
094            throw new IllegalArgumentException("Alignment must be between 0 and 0x7fff, was: " + alignment);
095        }
096        if (padding < 0) {
097            throw new IllegalArgumentException("Padding must not be negative, was: " + padding);
098        }
099        this.alignment = (short) alignment;
100        this.allowMethodChange = allowMethodChange;
101        this.padding = padding;
102    }
103
104    /**
105     * Indicates whether a method change is allowed when re-compressing the ZIP file.
106     *
107     * @return true if a method change is allowed, false otherwise.
108     */
109    public boolean allowMethodChange() {
110        return allowMethodChange;
111    }
112
113    /**
114     * Gets requested alignment.
115     *
116     * @return requested alignment.
117     */
118    public short getAlignment() {
119        return alignment;
120    }
121
122    @Override
123    public byte[] getCentralDirectoryData() {
124        return ZipShort.getBytes(alignment | (allowMethodChange ? ALLOW_METHOD_MESSAGE_CHANGE_FLAG : 0));
125    }
126
127    @Override
128    public ZipShort getCentralDirectoryLength() {
129        return new ZipShort(BASE_SIZE);
130    }
131
132    @Override
133    public ZipShort getHeaderId() {
134        return ID;
135    }
136
137    @Override
138    public byte[] getLocalFileDataData() {
139        final byte[] content = new byte[BASE_SIZE + padding];
140        ZipShort.putShort(alignment | (allowMethodChange ? ALLOW_METHOD_MESSAGE_CHANGE_FLAG : 0), content, 0);
141        return content;
142    }
143
144    @Override
145    public ZipShort getLocalFileDataLength() {
146        return new ZipShort(BASE_SIZE + padding);
147    }
148
149    @Override
150    public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, final int length) throws ZipException {
151        if (length < BASE_SIZE) {
152            throw new ZipException("Too short content for ResourceAlignmentExtraField (0xa11e): " + length);
153        }
154        final int alignmentValue = ZipShort.getValue(buffer, offset);
155        this.alignment = (short) (alignmentValue & ALLOW_METHOD_MESSAGE_CHANGE_FLAG - 1);
156        this.allowMethodChange = (alignmentValue & ALLOW_METHOD_MESSAGE_CHANGE_FLAG) != 0;
157    }
158
159    @Override
160    public void parseFromLocalFileData(final byte[] buffer, final int offset, final int length) throws ZipException {
161        parseFromCentralDirectoryData(buffer, offset, length);
162        this.padding = length - BASE_SIZE;
163    }
164}