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.Throwables.throwIfUnchecked;
019import static com.google.common.collect.Sets.newConcurrentHashSet;
020import static java.util.Objects.requireNonNull;
021
022import com.google.common.annotations.Beta;
023import com.google.common.annotations.GwtIncompatible;
024import com.google.common.annotations.J2ktIncompatible;
025import com.google.common.annotations.VisibleForTesting;
026import com.google.common.base.FinalizablePhantomReference;
027import com.google.common.base.FinalizableReferenceQueue;
028import com.google.errorprone.annotations.concurrent.GuardedBy;
029import com.google.j2objc.annotations.J2ObjCIncompatible;
030import java.io.ByteArrayInputStream;
031import java.io.ByteArrayOutputStream;
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.FileOutputStream;
035import java.io.IOException;
036import java.io.InputStream;
037import java.io.OutputStream;
038import java.lang.ref.Reference;
039import java.lang.reflect.InvocationTargetException;
040import java.lang.reflect.Method;
041import java.util.Set;
042import org.jspecify.annotations.Nullable;
043
044/**
045 * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
046 * once the data reaches a configurable size.
047 *
048 * <p>When this stream creates a temporary file, it restricts the file's permissions to the current
049 * user or, in the case of Android, the current app. If that is not possible (as is the case under
050 * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
051 * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
052 * versions would create a file that is more accessible, as discussed in <a
053 * href="https://nvd.nist.gov/vuln/detail/cve-2023-2976">CVE-2023-2976</a>.
054 *
055 * <p>Temporary files created by this stream may live in the local filesystem until either:
056 *
057 * <ul>
058 *   <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
059 *   <li>this stream (or, more precisely, its {@link #asByteSource} view) is garbage collected,
060 *       <strong>AND</strong> this stream was constructed with {@linkplain
061 *       #FileBackedOutputStream(int, boolean) the two-arg constructor} with {@code true} for the
062 *       second parameter. Relying on this second approach is discouraged.
063 * </ul>
064 *
065 * <p>This class is thread-safe.
066 *
067 * @author Chris Nokleberg
068 * @since 1.0
069 */
070@Beta
071@J2ktIncompatible
072@GwtIncompatible
073@J2ObjCIncompatible
074public final class FileBackedOutputStream extends OutputStream {
075  private final FbosByteSource byteSource;
076  private final State state;
077
078  /** ByteArrayOutputStream that exposes its internals. */
079  private static final class MemoryOutput extends ByteArrayOutputStream {
080    byte[] getBuffer() {
081      return buf;
082    }
083
084    int getCount() {
085      return count;
086    }
087  }
088
089  /** Returns the file holding the data (possibly null). */
090  @VisibleForTesting
091  @Nullable File getFile() {
092    return state.getFile();
093  }
094
095  /**
096   * Creates a new instance that uses the given file threshold, and does not reset the data when the
097   * {@link ByteSource} returned by {@link #asByteSource} is garbage collected.
098   *
099   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
100   * @throws IllegalArgumentException if {@code fileThreshold} is negative
101   */
102  public FileBackedOutputStream(int fileThreshold) {
103    this(fileThreshold, false);
104  }
105
106  /**
107   * Creates a new instance that uses the given file threshold, and optionally resets the data when
108   * the {@link ByteSource} returned by {@link #asByteSource} is garbage collected. Prefer to {@link
109   * #reset} the stream explicitly, rather than rely on garbage collection: If you call {@link
110   * #reset} explicitly, you can call it more promptly, and you can appropriately handle any
111   * exception that results.
112   *
113   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
114   * @param resetWhenGarbageCollected if true, the {@link #reset} method will be called when the
115   *     {@link ByteSource} returned by {@link #asByteSource} is garbage collected.
116   * @throws IllegalArgumentException if {@code fileThreshold} is negative
117   */
118  public FileBackedOutputStream(int fileThreshold, boolean resetWhenGarbageCollected) {
119    checkArgument(
120        fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
121    this.state = new State(fileThreshold, resetWhenGarbageCollected);
122    this.byteSource = new FbosByteSource(state);
123
124    if (resetWhenGarbageCollected) {
125      FinalizableReference.register(byteSource);
126    }
127  }
128
129  private static final class FinalizableReference extends FinalizablePhantomReference<ByteSource> {
130    static final FinalizableReferenceQueue referenceQueue = new FinalizableReferenceQueue();
131    static final Set<FinalizableReference> references = newConcurrentHashSet();
132
133    static void register(FbosByteSource referent) {
134      references.add(new FinalizableReference(referent));
135    }
136
137    final State state;
138
139    FinalizableReference(FbosByteSource referent) {
140      super(referent, referenceQueue);
141      this.state = referent.state;
142    }
143
144    @Override
145    public void finalizeReferent() {
146      references.remove(this);
147      try {
148        state.reset();
149      } catch (Throwable t) {
150        t.printStackTrace(System.err);
151      }
152    }
153  }
154
155  private static final class FbosByteSource extends ByteSource {
156    final State state;
157
158    FbosByteSource(State state) {
159      this.state = state;
160    }
161
162    @Override
163    public InputStream openStream() throws IOException {
164      return state.openInputStream();
165    }
166  }
167
168  /**
169   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
170   *
171   * @since 15.0
172   */
173  public ByteSource asByteSource() {
174    return byteSource;
175  }
176
177  /**
178   * Calls {@link #close} if not already closed, and then resets this object back to its initial
179   * state, for reuse. If data was buffered to a file, it will be deleted.
180   *
181   * @throws IOException if an I/O error occurred while deleting the file buffer
182   */
183  public void reset() throws IOException {
184    try {
185      state.reset();
186    } finally {
187      reachabilityFence(byteSource);
188    }
189  }
190
191  @Override
192  public void write(int b) throws IOException {
193    try {
194      state.write(b);
195    } finally {
196      reachabilityFence(byteSource);
197    }
198  }
199
200  @Override
201  public void write(byte[] b) throws IOException {
202    write(b, 0, b.length);
203  }
204
205  @Override
206  public void write(byte[] b, int off, int len) throws IOException {
207    try {
208      state.write(b, off, len);
209    } finally {
210      reachabilityFence(byteSource);
211    }
212  }
213
214  @Override
215  public void close() throws IOException {
216    try {
217      state.close();
218    } finally {
219      reachabilityFence(byteSource);
220    }
221  }
222
223  @Override
224  public void flush() throws IOException {
225    try {
226      state.flush();
227    } finally {
228      reachabilityFence(byteSource);
229    }
230  }
231
232  /**
233   * Per-instance state, extracted to a separate object so that {@link FinalizableReference} can
234   * still access it after the {@link FileBackedOutputStream} and {@link ByteSource} have been
235   * garbage collected.
236   */
237  private static final class State {
238    final int fileThreshold;
239    final boolean resetWhenGarbageCollected;
240
241    @GuardedBy("this")
242    @Nullable MemoryOutput memory;
243
244    @GuardedBy("this")
245    @Nullable File file;
246
247    @GuardedBy("this")
248    OutputStream out;
249
250    State(int fileThreshold, boolean resetWhenGarbageCollected) {
251      this.fileThreshold = fileThreshold;
252      this.resetWhenGarbageCollected = resetWhenGarbageCollected;
253      this.memory = new MemoryOutput();
254      this.out = memory;
255    }
256
257    synchronized @Nullable File getFile() {
258      return file;
259    }
260
261    synchronized InputStream openInputStream() throws IOException {
262      if (file != null) {
263        return new FileInputStream(file);
264      } else {
265        // requireNonNull is safe because we always have either `file` or `memory`.
266        requireNonNull(memory);
267        return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
268      }
269    }
270
271    synchronized void reset() throws IOException {
272      try {
273        close();
274      } finally {
275        if (memory == null) {
276          memory = new MemoryOutput();
277        } else {
278          memory.reset();
279        }
280        out = memory;
281        if (file != null) {
282          File deleteMe = file;
283          file = null;
284          if (!deleteMe.delete()) {
285            throw new IOException("Could not delete: " + deleteMe);
286          }
287        }
288      }
289    }
290
291    synchronized void write(int b) throws IOException {
292      update(1);
293      out.write(b);
294    }
295
296    synchronized void write(byte[] b, int off, int len) throws IOException {
297      update(len);
298      out.write(b, off, len);
299    }
300
301    synchronized void close() throws IOException {
302      out.close();
303    }
304
305    synchronized void flush() throws IOException {
306      out.flush();
307    }
308
309    /**
310     * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering
311     * if so.
312     */
313    @GuardedBy("this")
314    void update(int len) throws IOException {
315      if (memory != null && (memory.getCount() + len > fileThreshold)) {
316        File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
317        if (resetWhenGarbageCollected) {
318          // References are not guaranteed to be collected on system shutdown; this is insurance.
319          temp.deleteOnExit();
320        }
321        FileOutputStream transfer = null;
322        try {
323          transfer = new FileOutputStream(temp);
324          transfer.write(memory.getBuffer(), 0, memory.getCount());
325          transfer.flush();
326          // We've successfully transferred the data; switch to writing to file.
327          out = transfer;
328        } catch (IOException e) {
329          if (transfer != null) {
330            try {
331              transfer.close();
332            } catch (IOException closeException) {
333              e.addSuppressed(closeException);
334            }
335          }
336          temp.delete();
337          throw e;
338        }
339
340        file = temp;
341        memory = null;
342      }
343    }
344  }
345
346  /** Ensures that {@code o} is not garbage collected until this point in the code. */
347  @VisibleForTesting
348  // We call the method only after checking that it's present.
349  @IgnoreJRERequirement
350  @SuppressWarnings({
351    "Java8ApiChecker",
352    "NewApi",
353    // This method is a helper, which we call from a `finally` block, as recommended.
354    "ReachabilityFenceUsage",
355  })
356  static void reachabilityFence(@Nullable Object o) {
357    if (IS_REACHABILITY_FENCE_METHOD_USABLE) {
358      Reference.reachabilityFence(o);
359    }
360  }
361
362  private static final boolean IS_REACHABILITY_FENCE_METHOD_USABLE =
363      computeIsReachabilityFenceMethodUsable();
364
365  private static boolean computeIsReachabilityFenceMethodUsable() {
366    try {
367      Method method = Reference.class.getMethod("reachabilityFence", Object.class);
368      method.invoke(null, FileBackedOutputStream.class); // to make sure the method is accessible
369      return true;
370    } catch (NoSuchMethodException | IllegalAccessException probablyBeforeJava9OrAndroid28) {
371      /*
372       * It's theoretically possible for Reference.reachabilityFence to exist under older VMs in an
373       * inaccessible form.
374       */
375      return false;
376    } catch (InvocationTargetException e) {
377      /*
378       * It's theoretically possible for Reference.reachabilityFence to exist under older VMs but
379       * not work. (Under Android in particular, we really should check the API Level instead of
380       * probing for methods....) But it's hard to imagine how reachabilityFence in particular could
381       * exist but throw, so we propagate anything that's thrown, presumably an unchecked Exception
382       * or Error.
383       */
384      throwIfUnchecked(e.getCause());
385      throw new AssertionError(e.getCause());
386    }
387  }
388}