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