View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.utils;
20  
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.nio.ByteBuffer;
25  import java.nio.ByteOrder;
26  import java.nio.channels.ClosedChannelException;
27  import java.nio.channels.WritableByteChannel;
28  import java.util.concurrent.atomic.AtomicBoolean;
29  
30  import org.apache.commons.io.IOUtils;
31  
32  /**
33   * This class supports writing to an OutputStream or WritableByteChannel in fixed length blocks.
34   * <p>
35   * It can be be used to support output to devices such as tape drives that require output in this format. If the final block does not have enough content to
36   * fill an entire block, the output will be padded to a full block size.
37   * </p>
38   *
39   * <p>
40   * This class can be used to support TAR,PAX, and CPIO blocked output to character special devices. It is not recommended that this class be used unless writing
41   * to such devices, as the padding serves no useful purpose in such cases.
42   * </p>
43   *
44   * <p>
45   * This class should normally wrap a FileOutputStream or associated WritableByteChannel directly. If there is an intervening filter that modified the output,
46   * such as a CompressorOutputStream, or performs its own buffering, such as BufferedOutputStream, output to the device may no longer be of the specified size.
47   * </p>
48   *
49   * <p>
50   * Any content written to this stream should be self-delimiting and should tolerate any padding added to fill the last block.
51   * </p>
52   *
53   * @since 1.15
54   */
55  public class FixedLengthBlockOutputStream extends OutputStream implements WritableByteChannel {
56  
57      /**
58       * Helper class to provide channel wrapper for arbitrary output stream that doesn't alter the size of writes. We can't use Channels.newChannel, because for
59       * non FileOutputStreams, it breaks up writes into 8KB max chunks. Since the purpose of this class is to always write complete blocks, we need to write a
60       * simple class to take care of it.
61       */
62      private static final class BufferAtATimeOutputChannel implements WritableByteChannel {
63  
64          private final OutputStream out;
65          private final AtomicBoolean closed = new AtomicBoolean();
66  
67          private BufferAtATimeOutputChannel(final OutputStream out) {
68              this.out = out;
69          }
70  
71          @Override
72          public void close() throws IOException {
73              if (closed.compareAndSet(false, true)) {
74                  out.close();
75              }
76          }
77  
78          @Override
79          public boolean isOpen() {
80              return !closed.get();
81          }
82  
83          @Override
84          public int write(final ByteBuffer buffer) throws IOException {
85              if (!isOpen()) {
86                  throw new ClosedChannelException();
87              }
88              if (!buffer.hasArray()) {
89                  throw new IOException("Direct buffer somehow written to BufferAtATimeOutputChannel");
90              }
91  
92              try {
93                  final int pos = buffer.position();
94                  final int len = buffer.limit() - pos;
95                  out.write(buffer.array(), buffer.arrayOffset() + pos, len);
96                  buffer.position(buffer.limit());
97                  return len;
98              } catch (final IOException e) {
99                  IOUtils.closeQuietly(this);
100                 throw e;
101             }
102         }
103 
104     }
105 
106     private final WritableByteChannel out;
107     private final int blockSize;
108     private final ByteBuffer buffer;
109 
110     private final AtomicBoolean closed = new AtomicBoolean();
111 
112     /**
113      * Constructs a fixed length block output stream with given destination stream and block size.
114      *
115      * @param os        The stream to wrap.
116      * @param blockSize The block size to use.
117      */
118     public FixedLengthBlockOutputStream(final OutputStream os, final int blockSize) {
119         if (os instanceof FileOutputStream) {
120             final FileOutputStream fileOutputStream = (FileOutputStream) os;
121             out = fileOutputStream.getChannel();
122             buffer = ByteBuffer.allocateDirect(blockSize);
123         } else {
124             out = new BufferAtATimeOutputChannel(os);
125             buffer = ByteBuffer.allocate(blockSize);
126         }
127         this.blockSize = blockSize;
128     }
129 
130     /**
131      * Constructs a fixed length block output stream with given destination writable byte channel and block size.
132      *
133      * @param out       The writable byte channel to wrap.
134      * @param blockSize The block size to use.
135      */
136     public FixedLengthBlockOutputStream(final WritableByteChannel out, final int blockSize) {
137         this.out = out;
138         this.blockSize = blockSize;
139         this.buffer = ByteBuffer.allocateDirect(blockSize);
140     }
141 
142     @Override
143     public void close() throws IOException {
144         if (closed.compareAndSet(false, true)) {
145             try {
146                 flushBlock();
147             } finally {
148                 out.close();
149             }
150         }
151     }
152 
153     /**
154      * Potentially pads and then writes the current block to the underlying stream.
155      *
156      * @throws IOException if writing fails
157      */
158     public void flushBlock() throws IOException {
159         if (buffer.position() != 0) {
160             padBlock();
161             writeBlock();
162         }
163     }
164 
165     @Override
166     public boolean isOpen() {
167         if (!out.isOpen()) {
168             closed.set(true);
169         }
170         return !closed.get();
171     }
172 
173     private void maybeFlush() throws IOException {
174         if (!buffer.hasRemaining()) {
175             writeBlock();
176         }
177     }
178 
179     private void padBlock() {
180         buffer.order(ByteOrder.nativeOrder());
181         int bytesToWrite = buffer.remaining();
182         if (bytesToWrite > 8) {
183             final int align = buffer.position() & 7;
184             if (align != 0) {
185                 final int limit = 8 - align;
186                 for (int i = 0; i < limit; i++) {
187                     buffer.put((byte) 0);
188                 }
189                 bytesToWrite -= limit;
190             }
191 
192             while (bytesToWrite >= 8) {
193                 buffer.putLong(0L);
194                 bytesToWrite -= 8;
195             }
196         }
197         while (buffer.hasRemaining()) {
198             buffer.put((byte) 0);
199         }
200     }
201 
202     @Override
203     public void write(final byte[] b, final int offset, final int length) throws IOException {
204         if (!isOpen()) {
205             throw new ClosedChannelException();
206         }
207         int off = offset;
208         int len = length;
209         while (len > 0) {
210             final int n = Math.min(len, buffer.remaining());
211             buffer.put(b, off, n);
212             maybeFlush();
213             len -= n;
214             off += n;
215         }
216     }
217 
218     @Override
219     public int write(final ByteBuffer src) throws IOException {
220         if (!isOpen()) {
221             throw new ClosedChannelException();
222         }
223         final int srcRemaining = src.remaining();
224         if (srcRemaining >= buffer.remaining()) {
225             int srcLeft = srcRemaining;
226             final int savedLimit = src.limit();
227             // If we're not at the start of buffer, we have some bytes already buffered
228             // fill up the reset of buffer and write the block.
229             if (buffer.position() != 0) {
230                 final int n = buffer.remaining();
231                 src.limit(src.position() + n);
232                 buffer.put(src);
233                 writeBlock();
234                 srcLeft -= n;
235             }
236             // whilst we have enough bytes in src for complete blocks,
237             // write them directly from src without copying them to buffer
238             while (srcLeft >= blockSize) {
239                 src.limit(src.position() + blockSize);
240                 out.write(src);
241                 srcLeft -= blockSize;
242             }
243             // copy any remaining bytes into buffer
244             src.limit(savedLimit);
245         }
246         // if we don't have enough bytes in src to fill up a block we must buffer
247         buffer.put(src);
248         return srcRemaining;
249     }
250 
251     @Override
252     public void write(final int b) throws IOException {
253         if (!isOpen()) {
254             throw new ClosedChannelException();
255         }
256         buffer.put((byte) b);
257         maybeFlush();
258     }
259 
260     private void writeBlock() throws IOException {
261         buffer.flip();
262         final int i = out.write(buffer);
263         final boolean hasRemaining = buffer.hasRemaining();
264         if (i != blockSize || hasRemaining) {
265             throw new IOException(String.format("Failed to write %,d bytes atomically. Only wrote  %,d", blockSize, i));
266         }
267         buffer.clear();
268     }
269 
270 }