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}