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 * http://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.ar;
020
021import java.io.File;
022import java.io.IOException;
023import java.nio.file.Files;
024import java.nio.file.LinkOption;
025import java.nio.file.Path;
026import java.util.Date;
027import java.util.Objects;
028
029import org.apache.commons.compress.archivers.ArchiveEntry;
030
031/**
032 * Represents an archive entry in the "ar" format.
033 * <p>
034 * Each AR archive starts with "!&lt;arch&gt;" followed by a LF. After these 8 bytes
035 * the archive entries are listed. The format of an entry header is as it follows:
036 * </p>
037 * <pre>
038 * START BYTE   END BYTE    NAME                    FORMAT      LENGTH
039 * 0            15          File name               ASCII       16
040 * 16           27          Modification timestamp  Decimal     12
041 * 28           33          Owner ID                Decimal     6
042 * 34           39          Group ID                Decimal     6
043 * 40           47          File mode               Octal       8
044 * 48           57          File size (bytes)       Decimal     10
045 * 58           59          File magic              \140\012    2
046 * </pre>
047 * <p>
048 * This specifies that an ar archive entry header contains 60 bytes.
049 * </p>
050 * <p>
051 * Due to the limitation of the file name length to 16 bytes GNU and
052 * BSD has their own variants of this format. Currently Commons
053 * Compress can read but not write the GNU variant.  It fully supports
054 * the BSD variant.
055 * </p>
056 *
057 * @see <a href="https://www.freebsd.org/cgi/man.cgi?query=ar&sektion=5">ar man page</a>
058 * @Immutable
059 */
060public class ArArchiveEntry implements ArchiveEntry {
061
062    /** The header for each entry */
063    public static final String HEADER = "!<arch>\n";
064
065    /** The trailer for each entry */
066    public static final String TRAILER = "`\012";
067
068    private static final int DEFAULT_MODE = 33188; // = (octal) 0100644
069
070    /**
071     * SVR4/GNU adds a trailing / to names; BSD does not.
072     * They also vary in how names longer than 16 characters are represented.
073     * (Not yet fully supported by this implementation)
074     */
075    private final String name;
076    private final int userId;
077    private final int groupId;
078    private final int mode;
079    private final long lastModified;
080    private final long length;
081
082    /**
083     * Creates a new instance using the attributes of the given file
084     * @param inputFile the file to create an entry from
085     * @param entryName the name of the entry
086     */
087    public ArArchiveEntry(final File inputFile, final String entryName) {
088        // TODO sort out mode
089        this(entryName, inputFile.isFile() ? inputFile.length() : 0,
090             0, 0, DEFAULT_MODE, inputFile.lastModified() / 1000);
091    }
092
093    /**
094     * Creates a new instance using the attributes of the given file
095     * @param inputPath the file to create an entry from
096     * @param entryName the name of the entry
097     * @param options options indicating how symbolic links are handled.
098     * @throws IOException if an I/O error occurs.
099     * @since 1.21
100     */
101    public ArArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
102        this(entryName, Files.isRegularFile(inputPath, options) ? Files.size(inputPath) : 0, 0, 0, DEFAULT_MODE,
103            Files.getLastModifiedTime(inputPath, options).toMillis() / 1000);
104    }
105
106    /**
107     * Create a new instance using a couple of default values.
108     *
109     * <p>Sets userId and groupId to 0, the octal file mode to 644 and
110     * the last modified time to the current time.</p>
111     *
112     * @param name name of the entry
113     * @param length length of the entry in bytes
114     */
115    public ArArchiveEntry(final String name, final long length) {
116        this(name, length, 0, 0, DEFAULT_MODE,
117             System.currentTimeMillis() / 1000);
118    }
119
120    /**
121     * Create a new instance.
122     *
123     * @param name name of the entry
124     * @param length length of the entry in bytes
125     * @param userId numeric user id
126     * @param groupId numeric group id
127     * @param mode file mode
128     * @param lastModified last modified time in seconds since the epoch
129     */
130    public ArArchiveEntry(final String name, final long length, final int userId, final int groupId,
131                          final int mode, final long lastModified) {
132        this.name = name;
133        if (length < 0) {
134            throw new IllegalArgumentException("length must not be negative");
135        }
136        this.length = length;
137        this.userId = userId;
138        this.groupId = groupId;
139        this.mode = mode;
140        this.lastModified = lastModified;
141    }
142
143    @Override
144    public boolean equals(final Object obj) {
145        if (this == obj) {
146            return true;
147        }
148        if (obj == null || getClass() != obj.getClass()) {
149            return false;
150        }
151        final ArArchiveEntry other = (ArArchiveEntry) obj;
152        if (name == null) {
153            return other.name == null;
154        }
155        return name.equals(other.name);
156    }
157
158    /**
159     * Gets the group ID.
160     *
161     * @return the group ID.
162     */
163    public int getGroupId() {
164        return groupId;
165    }
166
167    /**
168     * Gets the last modified time in seconds since the epoch.
169     *
170     * @return the last modified date.
171     */
172    public long getLastModified() {
173        return lastModified;
174    }
175
176    @Override
177    public Date getLastModifiedDate() {
178        return new Date(1000 * getLastModified());
179    }
180
181    /**
182     * Gets the length.
183     *
184     * @return the length.
185     */
186    public long getLength() {
187        return length;
188    }
189
190    /**
191     * Gets the mode.
192     *
193     * @return the mode.
194     */
195    public int getMode() {
196        return mode;
197    }
198
199    @Override
200    public String getName() {
201        return name;
202    }
203
204    @Override
205    public long getSize() {
206        return this.getLength();
207    }
208
209    /**
210     * Gets the user ID.
211     *
212     * @return the user ID.
213     */
214    public int getUserId() {
215        return userId;
216    }
217
218    @Override
219    public int hashCode() {
220        return Objects.hash(name);
221    }
222
223    @Override
224    public boolean isDirectory() {
225        return false;
226    }
227}