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}