001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.io;
016
017import static com.google.common.base.Preconditions.checkArgument;
018import static com.google.common.base.Preconditions.checkPositionIndexes;
019import static com.google.common.base.Throwables.throwIfUnchecked;
020import static com.google.common.collect.Sets.newConcurrentHashSet;
021import static java.lang.Math.max;
022import static java.lang.Math.min;
023import static java.nio.channels.Channels.newOutputStream;
024import static java.util.Objects.requireNonNull;
025
026import com.google.common.annotations.Beta;
027import com.google.common.annotations.GwtIncompatible;
028import com.google.common.annotations.J2ktIncompatible;
029import com.google.common.annotations.VisibleForTesting;
030import com.google.common.base.FinalizablePhantomReference;
031import com.google.common.base.FinalizableReferenceQueue;
032import com.google.common.io.TempFileCreator.TempFileAndChannel;
033import com.google.common.primitives.Ints;
034import com.google.errorprone.annotations.concurrent.GuardedBy;
035import com.google.j2objc.annotations.J2ObjCIncompatible;
036import java.io.ByteArrayInputStream;
037import java.io.ByteArrayOutputStream;
038import java.io.File;
039import java.io.IOException;
040import java.io.InputStream;
041import java.io.OutputStream;
042import java.lang.ref.Reference;
043import java.lang.reflect.InvocationTargetException;
044import java.lang.reflect.Method;
045import java.nio.ByteBuffer;
046import java.nio.channels.FileChannel;
047import java.util.Set;
048import org.jspecify.annotations.Nullable;
049
050/**
051 * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
052 * once the data reaches a configurable size.
053 *
054 * <p>When this stream creates a temporary file, it restricts the file's permissions to the current
055 * user or, in the case of Android, the current app. If that is not possible (as is the case under
056 * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
057 * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
058 * versions would create a file that is more accessible, as discussed in <a
059 * href="https://nvd.nist.gov/vuln/detail/cve-2023-2976">CVE-2023-2976</a>.
060 *
061 * <p>Temporary files created by this stream may live in the local filesystem until either:
062 *
063 * <ul>
064 *   <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
065 *   <li>this stream (or, more precisely, its {@link #asByteSource} view) is garbage collected,
066 *       <strong>AND</strong> this stream was constructed with {@linkplain
067 *       #FileBackedOutputStream(int, boolean) the two-arg constructor} with {@code true} for the
068 *       second parameter. Relying on this second approach is discouraged.
069 * </ul>
070 *
071 * <p>This class is thread-safe.
072 *
073 * @author Chris Nokleberg
074 * @since 1.0
075 */
076@Beta
077@J2ktIncompatible
078@GwtIncompatible
079@J2ObjCIncompatible
080public final class FileBackedOutputStream extends OutputStream {
081  private final FbosByteSource byteSource;
082  private final State state;
083
084  /** ByteArrayOutputStream that exposes its internals. */
085  private static final class MemoryOutput extends ByteArrayOutputStream {
086    byte[] getBuffer() {
087      return buf;
088    }
089
090    int getCount() {
091      return count;
092    }
093  }
094
095  /** Returns the file holding the data (possibly null). */
096  @VisibleForTesting
097  @Nullable File getFile() {
098    return state.getFile();
099  }
100
101  /**
102   * Creates a new instance that uses the given file threshold, and does not reset the data when the
103   * {@link ByteSource} returned by {@link #asByteSource} is garbage collected.
104   *
105   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
106   * @throws IllegalArgumentException if {@code fileThreshold} is negative
107   */
108  public FileBackedOutputStream(int fileThreshold) {
109    this(fileThreshold, false);
110  }
111
112  /**
113   * Creates a new instance that uses the given file threshold, and optionally resets the data when
114   * the {@link ByteSource} returned by {@link #asByteSource} is garbage collected. Prefer to {@link
115   * #reset} the stream explicitly, rather than rely on garbage collection: If you call {@link
116   * #reset} explicitly, you can call it more promptly, and you can appropriately handle any
117   * exception that results.
118   *
119   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
120   * @param resetWhenGarbageCollected if true, the {@link #reset} method will be called when the
121   *     {@link ByteSource} returned by {@link #asByteSource} is garbage collected.
122   * @throws IllegalArgumentException if {@code fileThreshold} is negative
123   */
124  public FileBackedOutputStream(int fileThreshold, boolean resetWhenGarbageCollected) {
125    checkArgument(
126        fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
127    this.state = new State(fileThreshold, resetWhenGarbageCollected);
128    this.byteSource = new FbosByteSource(state);
129
130    if (resetWhenGarbageCollected) {
131      FinalizableReference.register(byteSource);
132    }
133  }
134
135  private static final class FinalizableReference extends FinalizablePhantomReference<ByteSource> {
136    static final FinalizableReferenceQueue referenceQueue = new FinalizableReferenceQueue();
137    static final Set<FinalizableReference> references = newConcurrentHashSet();
138
139    static void register(FbosByteSource referent) {
140      try {
141        references.add(new FinalizableReference(referent));
142      } finally {
143        FileBackedOutputStream.reachabilityFence(referent);
144      }
145    }
146
147    final State state;
148
149    FinalizableReference(FbosByteSource referent) {
150      super(referent, referenceQueue);
151      this.state = referent.state;
152    }
153
154    @Override
155    public void finalizeReferent() {
156      references.remove(this);
157      try {
158        state.reset();
159      } catch (Throwable t) {
160        t.printStackTrace(System.err);
161      }
162    }
163  }
164
165  private static final class FbosByteSource extends ByteSource {
166    final State state;
167
168    FbosByteSource(State state) {
169      this.state = state;
170    }
171
172    @Override
173    public InputStream openStream() throws IOException {
174      return state.openInputStream();
175    }
176  }
177
178  /**
179   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
180   *
181   * @since 15.0
182   */
183  public ByteSource asByteSource() {
184    return byteSource;
185  }
186
187  /**
188   * Calls {@link #close} if not already closed, and then resets this object back to its initial
189   * state, for reuse. If data was buffered to a file, it will be deleted.
190   *
191   * @throws IOException if an I/O error occurred while deleting the file buffer
192   */
193  public void reset() throws IOException {
194    try {
195      state.reset();
196    } finally {
197      reachabilityFence(byteSource);
198    }
199  }
200
201  @Override
202  public void write(int b) throws IOException {
203    try {
204      state.write(b);
205    } finally {
206      reachabilityFence(byteSource);
207    }
208  }
209
210  @Override
211  public void write(byte[] b) throws IOException {
212    write(b, 0, b.length);
213  }
214
215  @Override
216  public void write(byte[] b, int off, int len) throws IOException {
217    try {
218      state.write(b, off, len);
219    } finally {
220      reachabilityFence(byteSource);
221    }
222  }
223
224  @Override
225  public void close() throws IOException {
226    try {
227      state.close();
228    } finally {
229      reachabilityFence(byteSource);
230    }
231  }
232
233  @Override
234  public void flush() throws IOException {
235    try {
236      state.flush();
237    } finally {
238      reachabilityFence(byteSource);
239    }
240  }
241
242  /**
243   * Per-instance state, extracted to a separate object so that {@link FinalizableReference} can
244   * still access it after the {@link FileBackedOutputStream} and {@link ByteSource} have been
245   * garbage collected.
246   */
247  private static final class State {
248    final int fileThreshold;
249    final boolean resetWhenGarbageCollected;
250
251    @GuardedBy("this")
252    @Nullable MemoryOutput memory;
253
254    @GuardedBy("this")
255    @Nullable TempFileAndChannel fileAndChannel;
256
257    @GuardedBy("this")
258    OutputStream out;
259
260    State(int fileThreshold, boolean resetWhenGarbageCollected) {
261      this.fileThreshold = fileThreshold;
262      this.resetWhenGarbageCollected = resetWhenGarbageCollected;
263      this.memory = new MemoryOutput();
264      this.out = memory;
265    }
266
267    synchronized @Nullable File getFile() {
268      return fileAndChannel != null ? fileAndChannel.file : null;
269    }
270
271    synchronized InputStream openInputStream() throws IOException {
272      if (fileAndChannel != null) {
273        return new ChannelInputStream(fileAndChannel.channel);
274      } else {
275        // requireNonNull is safe because we always have either `fileAndChannel` or `memory`.
276        requireNonNull(memory);
277        return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
278      }
279    }
280
281    synchronized void reset() throws IOException {
282      /*
283       * We don't need to call this.close(): It does nothing but update `out`, and we handle
284       * updating `out` here.
285       */
286
287      if (memory == null) {
288        memory = new MemoryOutput();
289      } else {
290        memory.reset();
291      }
292      out = memory;
293
294      try (TempFileAndChannel closeMe = fileAndChannel) {
295        fileAndChannel = null;
296      }
297    }
298
299    synchronized void write(int b) throws IOException {
300      update(1);
301      out.write(b);
302    }
303
304    synchronized void write(byte[] b, int off, int len) throws IOException {
305      update(len);
306      out.write(b, off, len);
307    }
308
309    synchronized void close() {
310      /*
311       * It would make sense to switch to CLOSED_OUTPUT_STREAM whether we've spilled to a file or
312       * not. But that would be a difference from our initial behavior of allowing writes after
313       * close() in the non-spilled case, thanks to behavior inherited from ByteArrayOutputStream.
314       *
315       * TODO(cpovirk): Maybe change that behavior, which is fragile.
316       */
317      if (fileAndChannel != null) {
318        // Do not close the FileChannel; it must stay open for subsequent reads via asByteSource().
319        out = CLOSED_OUTPUT_STREAM;
320        /*
321         * We don't even need to set a bit that marks the old OutputStream as closed, since we
322         * control all access to that stream (and we have now cut off that access).
323         */
324      }
325    }
326
327    synchronized void flush() throws IOException {
328      out.flush();
329    }
330
331    /**
332     * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering
333     * if so.
334     */
335    @GuardedBy("this")
336    void update(int len) throws IOException {
337      if (memory != null && (memory.getCount() + len > fileThreshold)) {
338        TempFileAndChannel temp = TempFileCreator.INSTANCE.openTempFile("FileBackedOutputStream");
339        if (resetWhenGarbageCollected) {
340          // References are not guaranteed to be collected on system shutdown; this is insurance.
341          temp.file.deleteOnExit();
342        }
343        try {
344          OutputStream transfer = newOutputStream(temp.channel);
345          transfer.write(memory.getBuffer(), 0, memory.getCount());
346          transfer.flush();
347          // We've successfully transferred the data; switch to writing to file.
348          out = transfer;
349        } catch (IOException e) {
350          try {
351            temp.close();
352          } catch (IOException closeException) {
353            e.addSuppressed(closeException);
354          }
355          throw e;
356        }
357
358        fileAndChannel = temp;
359        memory = null;
360      }
361    }
362  }
363
364  private static final OutputStream CLOSED_OUTPUT_STREAM =
365      new OutputStream() {
366        @Override
367        public void write(int b) throws IOException {
368          throw new IOException("Stream closed");
369        }
370
371        @Override
372        public void write(byte[] b, int off, int len) throws IOException {
373          checkPositionIndexes(off, off + len, b.length);
374          throw new IOException("Stream closed");
375        }
376
377        @Override
378        public void flush() throws IOException {
379          throw new IOException("Stream closed");
380        }
381      };
382
383  private static final class ChannelInputStream extends InputStream {
384    final FileChannel channel;
385
386    @GuardedBy("this")
387    long position;
388
389    @GuardedBy("this")
390    boolean closed;
391
392    ChannelInputStream(FileChannel channel) {
393      this.channel = channel;
394    }
395
396    @Override
397    public synchronized int read() throws IOException {
398      byte[] b = new byte[1];
399      return read(b, 0, 1) == 1 ? b[0] & 0xFF : -1;
400    }
401
402    @Override
403    public synchronized int read(byte[] b, int off, int len) throws IOException {
404      ByteBuffer buf = ByteBuffer.wrap(b, off, len);
405      checkOpen();
406      if (len == 0) {
407        return 0;
408      }
409      int read = channel.read(buf, position);
410      if (read > 0) {
411        position += read;
412      }
413      return read;
414    }
415
416    @Override
417    public synchronized long skip(long n) throws IOException {
418      checkOpen();
419      if (n <= 0) {
420        return 0;
421      }
422      long available = channel.size() - position;
423      if (available <= 0) {
424        return 0;
425      }
426      long skipped = min(n, available);
427      position += skipped;
428      return skipped;
429    }
430
431    @Override
432    public synchronized int available() throws IOException {
433      checkOpen();
434      return Ints.saturatedCast(max(0, channel.size() - position));
435    }
436
437    @Override
438    public synchronized void close() {
439      // Do not close the FileChannel; it must stay open for subsequent reads via asByteSource().
440      closed = true;
441    }
442
443    @GuardedBy("this")
444    void checkOpen() throws IOException {
445      if (closed) {
446        throw new IOException("Stream closed");
447      }
448    }
449  }
450
451  /** Ensures that {@code o} is not garbage collected until this point in the code. */
452  @VisibleForTesting
453  // We call the method only after checking that it's present.
454  @IgnoreJRERequirement
455  @SuppressWarnings({
456    "Java8ApiChecker",
457    "NewApi",
458    // This method is a helper, which we call from a `finally` block, as recommended.
459    "ReachabilityFenceUsage",
460  })
461  static void reachabilityFence(@Nullable Object o) {
462    if (IS_REACHABILITY_FENCE_METHOD_USABLE) {
463      Reference.reachabilityFence(o);
464    }
465  }
466
467  private static final boolean IS_REACHABILITY_FENCE_METHOD_USABLE =
468      computeIsReachabilityFenceMethodUsable();
469
470  private static boolean computeIsReachabilityFenceMethodUsable() {
471    try {
472      Method method = Reference.class.getMethod("reachabilityFence", Object.class);
473      method.invoke(null, FileBackedOutputStream.class); // to make sure the method is accessible
474      return true;
475    } catch (NoSuchMethodException | IllegalAccessException probablyBeforeJava9OrAndroid28) {
476      /*
477       * It's theoretically possible for Reference.reachabilityFence to exist under older VMs in an
478       * inaccessible form.
479       */
480      return false;
481    } catch (InvocationTargetException e) {
482      /*
483       * It's theoretically possible for Reference.reachabilityFence to exist under older VMs but
484       * not work. (Under Android in particular, we really should check the API Level instead of
485       * probing for methods....) But it's hard to imagine how reachabilityFence in particular could
486       * exist but throw, so we propagate anything that's thrown, presumably an unchecked Exception
487       * or Error.
488       */
489      throwIfUnchecked(e.getCause());
490      throw new AssertionError(e.getCause());
491    }
492  }
493}