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