Skip to content

Commit c755b97

Browse files
lmcreanGoogle Java Core Libraries
authored andcommitted
Fix FileBackedOutputStream to prevent leaks during threshold crossing.
Fixes #5756 Fixes #7986 RELNOTES=n/a PiperOrigin-RevId: 826065267
1 parent fb862e5 commit c755b97

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed

android/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.common.io;
1818

1919
import static com.google.common.base.StandardSystemProperty.OS_NAME;
20+
import static com.google.common.primitives.Bytes.concat;
2021
import static com.google.common.truth.Truth.assertThat;
2122
import static java.lang.Math.min;
2223
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
@@ -161,4 +162,39 @@ private static boolean isAndroid() {
161162
private static boolean isWindows() {
162163
return OS_NAME.value().startsWith("Windows");
163164
}
165+
166+
/**
167+
* Test that verifies the resource leak fix for <a
168+
* href="https://github.com/google/guava/issues/5756">Issue #5756</a>.
169+
*
170+
* <p>This test covers a scenario where we write a smaller amount of data first, then write a
171+
* large amount that crosses the threshold (transitioning from "not at threshold" to "over the
172+
* threshold"). (We then write some more afterward.) This differs from the existing
173+
* testThreshold() which writes exactly enough bytes to fill the buffer, then immediately writes
174+
* more bytes.
175+
*
176+
* <p>Note: Direct testing of the {@link IOException} scenario during write/flush is challenging
177+
* without mocking. This test verifies that normal operation with threshold crossing still works
178+
* correctly with the fix in place.
179+
*/
180+
public void testThresholdCrossing_resourceManagement() throws Exception {
181+
FileBackedOutputStream out = new FileBackedOutputStream(/* fileThreshold= */ 10);
182+
ByteSource source = out.asByteSource();
183+
184+
byte[] chunk1 = newPreFilledByteArray(8); // Below threshold
185+
byte[] chunk2 = newPreFilledByteArray(5); // Crosses threshold
186+
byte[] chunk3 = newPreFilledByteArray(20); // More data to file
187+
188+
out.write(chunk1);
189+
assertThat(out.getFile()).isNull();
190+
191+
out.write(chunk2);
192+
assertThat(out.getFile()).isNotNull();
193+
assertThat(source.read()).isEqualTo(concat(chunk1, chunk2));
194+
195+
out.write(chunk3);
196+
assertThat(source.read()).isEqualTo(concat(chunk1, chunk2, chunk3));
197+
198+
out.reset();
199+
}
164200
}

android/guava/src/com/google/common/io/FileBackedOutputStream.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,21 @@ private void update(int len) throws IOException {
238238
// this is insurance.
239239
temp.deleteOnExit();
240240
}
241+
FileOutputStream transfer = null;
241242
try {
242-
FileOutputStream transfer = new FileOutputStream(temp);
243+
transfer = new FileOutputStream(temp);
243244
transfer.write(memory.getBuffer(), 0, memory.getCount());
244245
transfer.flush();
245246
// We've successfully transferred the data; switch to writing to file
246247
out = transfer;
247248
} catch (IOException e) {
249+
if (transfer != null) {
250+
try {
251+
transfer.close();
252+
} catch (IOException closeException) {
253+
e.addSuppressed(closeException);
254+
}
255+
}
248256
temp.delete();
249257
throw e;
250258
}

guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.common.io;
1818

1919
import static com.google.common.base.StandardSystemProperty.OS_NAME;
20+
import static com.google.common.primitives.Bytes.concat;
2021
import static com.google.common.truth.Truth.assertThat;
2122
import static java.lang.Math.min;
2223
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
@@ -161,4 +162,39 @@ private static boolean isAndroid() {
161162
private static boolean isWindows() {
162163
return OS_NAME.value().startsWith("Windows");
163164
}
165+
166+
/**
167+
* Test that verifies the resource leak fix for <a
168+
* href="https://github.com/google/guava/issues/5756">Issue #5756</a>.
169+
*
170+
* <p>This test covers a scenario where we write a smaller amount of data first, then write a
171+
* large amount that crosses the threshold (transitioning from "not at threshold" to "over the
172+
* threshold"). (We then write some more afterward.) This differs from the existing
173+
* testThreshold() which writes exactly enough bytes to fill the buffer, then immediately writes
174+
* more bytes.
175+
*
176+
* <p>Note: Direct testing of the {@link IOException} scenario during write/flush is challenging
177+
* without mocking. This test verifies that normal operation with threshold crossing still works
178+
* correctly with the fix in place.
179+
*/
180+
public void testThresholdCrossing_resourceManagement() throws Exception {
181+
FileBackedOutputStream out = new FileBackedOutputStream(/* fileThreshold= */ 10);
182+
ByteSource source = out.asByteSource();
183+
184+
byte[] chunk1 = newPreFilledByteArray(8); // Below threshold
185+
byte[] chunk2 = newPreFilledByteArray(5); // Crosses threshold
186+
byte[] chunk3 = newPreFilledByteArray(20); // More data to file
187+
188+
out.write(chunk1);
189+
assertThat(out.getFile()).isNull();
190+
191+
out.write(chunk2);
192+
assertThat(out.getFile()).isNotNull();
193+
assertThat(source.read()).isEqualTo(concat(chunk1, chunk2));
194+
195+
out.write(chunk3);
196+
assertThat(source.read()).isEqualTo(concat(chunk1, chunk2, chunk3));
197+
198+
out.reset();
199+
}
164200
}

guava/src/com/google/common/io/FileBackedOutputStream.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,21 @@ private void update(int len) throws IOException {
238238
// this is insurance.
239239
temp.deleteOnExit();
240240
}
241+
FileOutputStream transfer = null;
241242
try {
242-
FileOutputStream transfer = new FileOutputStream(temp);
243+
transfer = new FileOutputStream(temp);
243244
transfer.write(memory.getBuffer(), 0, memory.getCount());
244245
transfer.flush();
245246
// We've successfully transferred the data; switch to writing to file
246247
out = transfer;
247248
} catch (IOException e) {
249+
if (transfer != null) {
250+
try {
251+
transfer.close();
252+
} catch (IOException closeException) {
253+
e.addSuppressed(closeException);
254+
}
255+
}
248256
temp.delete();
249257
throw e;
250258
}

0 commit comments

Comments
 (0)