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     */
019    package org.apache.commons.compress.archivers.ar;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.io.OutputStream;
024    
025    import org.apache.commons.compress.archivers.ArchiveEntry;
026    import org.apache.commons.compress.archivers.ArchiveOutputStream;
027    import org.apache.commons.compress.utils.ArchiveUtils;
028    
029    /**
030     * Implements the "ar" archive format as an output stream.
031     * 
032     * @NotThreadSafe
033     */
034    public class ArArchiveOutputStream extends ArchiveOutputStream {
035        /** Fail if a long file name is required in the archive. */
036        public static final int LONGFILE_ERROR = 0;
037    
038        /** BSD ar extensions are used to store long file names in the archive. */
039        public static final int LONGFILE_BSD = 1;
040    
041        private final OutputStream out;
042        private long entryOffset = 0;
043        private ArArchiveEntry prevEntry;
044        private boolean haveUnclosedEntry = false;
045        private int longFileMode = LONGFILE_ERROR;
046    
047        /** indicates if this archive is finished */
048        private boolean finished = false;
049    
050        public ArArchiveOutputStream( final OutputStream pOut ) {
051            this.out = pOut;
052        }
053    
054        /**
055         * Set the long file mode.
056         * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
057         * This specifies the treatment of long file names (names >= 16).
058         * Default is LONGFILE_ERROR.
059         * @param longFileMode the mode to use
060         * @since 1.3
061         */
062        public void setLongFileMode(int longFileMode) {
063            this.longFileMode = longFileMode;
064        }
065    
066        private long writeArchiveHeader() throws IOException {
067            byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
068            out.write(header);
069            return header.length;
070        }
071    
072        /** {@inheritDoc} */
073        @Override
074        public void closeArchiveEntry() throws IOException {
075            if(finished) {
076                throw new IOException("Stream has already been finished");
077            }
078            if (prevEntry == null || !haveUnclosedEntry){
079                throw new IOException("No current entry to close");
080            }
081            if ((entryOffset % 2) != 0) {
082                out.write('\n'); // Pad byte
083            }
084            haveUnclosedEntry = false;
085        }
086    
087        /** {@inheritDoc} */
088        @Override
089        public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
090            if(finished) {
091                throw new IOException("Stream has already been finished");
092            }
093    
094            ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
095            if (prevEntry == null) {
096                writeArchiveHeader();
097            } else {
098                if (prevEntry.getLength() != entryOffset) {
099                    throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
100                }
101    
102                if (haveUnclosedEntry) {
103                    closeArchiveEntry();
104                }
105            }
106    
107            prevEntry = pArEntry;
108    
109            writeEntryHeader(pArEntry);
110    
111            entryOffset = 0;
112            haveUnclosedEntry = true;
113        }
114    
115        private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException { 
116            final long diff = pNewOffset - pOffset;
117    
118            if (diff > 0) {
119                for (int i = 0; i < diff; i++) {
120                    write(pFill);
121                }
122            }
123    
124            return pNewOffset;
125        }
126    
127        private long write( final String data ) throws IOException {
128            final byte[] bytes = data.getBytes("ascii");
129            write(bytes);
130            return bytes.length;
131        }
132    
133        private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
134    
135            long offset = 0;
136            boolean mustAppendName = false;
137    
138            final String n = pEntry.getName();
139            if (LONGFILE_ERROR == longFileMode && n.length() > 16) {
140                throw new IOException("filename too long, > 16 chars: "+n);
141            }
142            if (LONGFILE_BSD == longFileMode && 
143                (n.length() > 16 || n.indexOf(" ") > -1)) {
144                mustAppendName = true;
145                offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
146                                + String.valueOf(n.length()));
147            } else {
148                offset += write(n);
149            }
150    
151            offset = fill(offset, 16, ' ');
152            final String m = "" + (pEntry.getLastModified());
153            if (m.length() > 12) {
154                throw new IOException("modified too long");
155            }
156            offset += write(m);
157    
158            offset = fill(offset, 28, ' ');
159            final String u = "" + pEntry.getUserId();
160            if (u.length() > 6) {
161                throw new IOException("userid too long");
162            }
163            offset += write(u);
164    
165            offset = fill(offset, 34, ' ');
166            final String g = "" + pEntry.getGroupId();
167            if (g.length() > 6) {
168                throw new IOException("groupid too long");
169            }
170            offset += write(g);
171    
172            offset = fill(offset, 40, ' ');
173            final String fm = "" + Integer.toString(pEntry.getMode(), 8);
174            if (fm.length() > 8) {
175                throw new IOException("filemode too long");
176            }
177            offset += write(fm);
178    
179            offset = fill(offset, 48, ' ');
180            final String s =
181                String.valueOf(pEntry.getLength()
182                               + (mustAppendName ? n.length() : 0));
183            if (s.length() > 10) {
184                throw new IOException("size too long");
185            }
186            offset += write(s);
187    
188            offset = fill(offset, 58, ' ');
189    
190            offset += write(ArArchiveEntry.TRAILER);
191    
192            if (mustAppendName) {
193                offset += write(n);
194            }
195    
196            return offset;
197        }
198    
199        @Override
200        public void write(byte[] b, int off, int len) throws IOException {
201            out.write(b, off, len);
202            count(len);
203            entryOffset += len;
204        }
205    
206        /**
207         * Calls finish if necessary, and then closes the OutputStream
208         */
209        @Override
210        public void close() throws IOException {
211            if(!finished) {
212                finish();
213            }
214            out.close();
215            prevEntry = null;
216        }
217    
218        /** {@inheritDoc} */
219        @Override
220        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
221                throws IOException {
222            if(finished) {
223                throw new IOException("Stream has already been finished");
224            }
225            return new ArArchiveEntry(inputFile, entryName);
226        }
227    
228        /** {@inheritDoc} */
229        @Override
230        public void finish() throws IOException {
231            if(haveUnclosedEntry) {
232                throw new IOException("This archive contains unclosed entries.");
233            } else if(finished) {
234                throw new IOException("This archive has already been finished");
235            }
236            finished = true;
237        }
238    }