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    
036        private final OutputStream out;
037        private long archiveOffset = 0;
038        private long entryOffset = 0;
039        private ArArchiveEntry prevEntry;
040        private boolean haveUnclosedEntry = false;
041        
042        /** indicates if this archive is finished */
043        private boolean finished = false;
044    
045        public ArArchiveOutputStream( final OutputStream pOut ) {
046            this.out = pOut;
047        }
048    
049        private long writeArchiveHeader() throws IOException {
050            byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
051            out.write(header);
052            return header.length;
053        }
054    
055        /** {@inheritDoc} */
056        public void closeArchiveEntry() throws IOException {
057            if(finished) {
058                throw new IOException("Stream has already been finished");
059            }
060            if (prevEntry == null || !haveUnclosedEntry){
061                throw new IOException("No current entry to close");
062            }
063            if ((entryOffset % 2) != 0) {
064                out.write('\n'); // Pad byte
065                archiveOffset++;
066            }
067            haveUnclosedEntry = false;
068        }
069    
070        /** {@inheritDoc} */
071        public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
072            if(finished) {
073                throw new IOException("Stream has already been finished");
074            }
075            
076            ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
077            if (prevEntry == null) {
078                archiveOffset += writeArchiveHeader();
079            } else {
080                if (prevEntry.getLength() != entryOffset) {
081                    throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
082                }
083    
084                if (haveUnclosedEntry) {
085                    closeArchiveEntry();
086                }
087            }
088    
089            prevEntry = pArEntry;
090    
091            archiveOffset += writeEntryHeader(pArEntry);
092    
093            entryOffset = 0;
094            haveUnclosedEntry = true;
095        }
096    
097        private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException { 
098            final long diff = pNewOffset - pOffset;
099    
100            if (diff > 0) {
101                for (int i = 0; i < diff; i++) {
102                    write(pFill);
103                }
104            }
105    
106            return pNewOffset;
107        }
108    
109        private long write( final String data ) throws IOException {
110            final byte[] bytes = data.getBytes("ascii");
111            write(bytes);
112            return bytes.length;
113        }
114    
115        private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
116    
117            long offset = 0;
118    
119            final String n = pEntry.getName();
120            if (n.length() > 16) {
121                throw new IOException("filename too long, > 16 chars: "+n);
122            }
123            offset += write(n);
124    
125            offset = fill(offset, 16, ' ');
126            final String m = "" + (pEntry.getLastModified());
127            if (m.length() > 12) {
128                throw new IOException("modified too long");
129            }
130            offset += write(m);
131    
132            offset = fill(offset, 28, ' ');
133            final String u = "" + pEntry.getUserId();
134            if (u.length() > 6) {
135                throw new IOException("userid too long");
136            }
137            offset += write(u);
138    
139            offset = fill(offset, 34, ' ');
140            final String g = "" + pEntry.getGroupId();
141            if (g.length() > 6) {
142                throw new IOException("groupid too long");
143            }
144            offset += write(g);
145    
146            offset = fill(offset, 40, ' ');
147            final String fm = "" + Integer.toString(pEntry.getMode(), 8);
148            if (fm.length() > 8) {
149                throw new IOException("filemode too long");
150            }
151            offset += write(fm);
152    
153            offset = fill(offset, 48, ' ');
154            final String s = "" + pEntry.getLength();
155            if (s.length() > 10) {
156                throw new IOException("size too long");
157            }
158            offset += write(s);
159    
160            offset = fill(offset, 58, ' ');
161    
162            offset += write(ArArchiveEntry.TRAILER);
163    
164            return offset;
165        }
166    
167        public void write(byte[] b, int off, int len) throws IOException {
168            out.write(b, off, len);
169            count(len);
170            entryOffset += len;
171        }
172    
173        /**
174         * Calls finish if necessary, and then closes the OutputStream
175         */
176        public void close() throws IOException {
177            if(!finished) {
178                finish();
179            }
180            out.close();
181            prevEntry = null;
182        }
183    
184        /** {@inheritDoc} */
185        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
186                throws IOException {
187            if(finished) {
188                throw new IOException("Stream has already been finished");
189            }
190            return new ArArchiveEntry(inputFile, entryName);
191        }
192    
193        /** {@inheritDoc} */
194        public void finish() throws IOException {
195            if(haveUnclosedEntry) {
196                throw new IOException("This archive contains unclosed entries.");
197            } else if(finished) {
198                throw new IOException("This archive has already been finished");
199            }
200            finished = true;
201        }
202    }