1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.file.backends; 17 18 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 19 import static com.google.android.libraries.mobiledatadownload.file.common.internal.Charsets.UTF_8; 20 import static com.google.common.truth.Truth.assertThat; 21 import static org.junit.Assert.assertThrows; 22 23 import android.app.blob.BlobStoreManager; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.os.ParcelFileDescriptor; 27 import android.util.Log; 28 import android.util.Pair; 29 import androidx.test.core.app.ApplicationProvider; 30 import androidx.test.uiautomator.UiDevice; 31 import com.google.android.libraries.mobiledatadownload.file.common.LimitExceededException; 32 import com.google.common.io.BaseEncoding; 33 import com.google.common.io.ByteStreams; 34 import java.io.Closeable; 35 import java.io.FileInputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.security.MessageDigest; 40 import java.util.Random; 41 import org.junit.After; 42 import org.junit.Before; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 import org.junit.runners.JUnit4; 46 47 @RunWith(JUnit4.class) 48 public class BlobStoreBackendTest { 49 public static final String TAG = "BlobStoreBackendTest"; 50 51 private Context context; 52 private BlobStoreBackend backend; 53 private BlobStoreManager blobStoreManager; 54 55 @Before setUpStorage()56 public final void setUpStorage() { 57 context = ApplicationProvider.getApplicationContext(); 58 backend = new BlobStoreBackend(context); 59 blobStoreManager = (BlobStoreManager) context.getSystemService(Context.BLOB_STORE_SERVICE); 60 } 61 62 @After tearDown()63 public void tearDown() throws Exception { 64 // Commands to clean up the blob storage. 65 runShellCmd("cmd blob_store clear-all-sessions"); 66 runShellCmd("cmd blob_store clear-all-blobs"); 67 context.getFilesDir().delete(); 68 } 69 70 @Test nativeReadAfterWrite_succeeds()71 public void nativeReadAfterWrite_succeeds() throws Exception { 72 byte[] content = "nativeReadAfterWrite_succeeds".getBytes(UTF_8); 73 String checksum = computeDigest(content); 74 Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build(); 75 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 76 77 try (OutputStream out = backend.openForWrite(uri)) { 78 assertThat(out).isNotNull(); 79 out.write(content); 80 } 81 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases); 82 83 Uri uriForRead = BlobUri.builder(context).setBlobParameters(checksum).build(); 84 Pair<Uri, Closeable> closeableUri = backend.openForNativeRead(uriForRead); 85 assertThat(closeableUri.second).isNotNull(); 86 int nativeFd = FileDescriptorUri.getFd(closeableUri.first); 87 try (InputStream in = 88 new FileInputStream(ParcelFileDescriptor.fromFd(nativeFd).getFileDescriptor()); 89 Closeable c = closeableUri.second) { 90 assertThat(ByteStreams.toByteArray(in)).isEqualTo(content); 91 } 92 } 93 94 @Test readAfterWrite_succeeds()95 public void readAfterWrite_succeeds() throws Exception { 96 byte[] content = "readAfterWrite_succeeds".getBytes(UTF_8); 97 String checksum = computeDigest(content); 98 Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build(); 99 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 100 101 try (OutputStream out = backend.openForWrite(uri)) { 102 assertThat(out).isNotNull(); 103 out.write(content); 104 } 105 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases); 106 107 Uri uriForRead = BlobUri.builder(context).setBlobParameters(checksum).build(); 108 try (InputStream in = backend.openForRead(uriForRead)) { 109 assertThat(in).isNotNull(); 110 assertThat(ByteStreams.toByteArray(in)).isEqualTo(content); 111 } 112 } 113 114 @Test exists()115 public void exists() throws Exception { 116 byte[] content = "exists".getBytes(UTF_8); 117 String checksum = computeDigest(content); 118 Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build(); 119 120 assertThat(backend.exists(uri)).isFalse(); 121 122 try (OutputStream out = backend.openForWrite(uri)) { 123 assertThat(out).isNotNull(); 124 out.write(content); 125 } 126 127 assertThat(backend.exists(uri)).isTrue(); 128 } 129 130 @Test writeLease_succeeds()131 public void writeLease_succeeds() throws Exception { 132 byte[] content = "writeLease_succeeds".getBytes(UTF_8); 133 String checksum = computeDigest(content); 134 Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 135 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 136 try (OutputStream out = backend.openForWrite(blobUri)) { 137 assertThat(out).isNotNull(); 138 out.write(content); 139 } 140 141 Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 142 143 OutputStream out = backend.openForWrite(leaseUri); 144 assertThat(out).isNull(); 145 146 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1); 147 } 148 149 @Test writeLeaseNonExistentFile_shouldThrow()150 public void writeLeaseNonExistentFile_shouldThrow() throws Exception { 151 byte[] content = "writeLeaseNonExistentFile_shouldThrow".getBytes(UTF_8); 152 String checksum = computeDigest(content); 153 154 Uri uri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 155 156 assertThrows(SecurityException.class, () -> backend.openForWrite(uri)); 157 } 158 159 @Test delete_succeeds()160 public void delete_succeeds() throws Exception { 161 byte[] content = "delete_succeeds".getBytes(UTF_8); 162 String checksum = computeDigest(content); 163 Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 164 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 165 166 try (OutputStream out = backend.openForWrite(blobUri)) { 167 assertThat(out).isNotNull(); 168 out.write(content); 169 } 170 171 Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 172 try (OutputStream out = backend.openForWrite(leaseUri)) { 173 assertThat(out).isNull(); 174 } 175 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1); 176 177 backend.deleteFile(leaseUri); 178 179 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases); 180 } 181 182 @Test releaseAllLeases_succeeds()183 public void releaseAllLeases_succeeds() throws Exception { 184 // Write and acquire lease on first file 185 byte[] content = "releaseAllLeases_succeeds_1".getBytes(UTF_8); 186 String checksum = computeDigest(content); 187 Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 188 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 189 try (OutputStream out = backend.openForWrite(blobUri)) { 190 assertThat(out).isNotNull(); 191 out.write(content); 192 } 193 Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 194 try (OutputStream out = backend.openForWrite(leaseUri)) { 195 assertThat(out).isNull(); 196 } 197 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1); 198 199 // Write and acquire lease on second file 200 content = "releaseAllLeases_succeeds_2".getBytes(UTF_8); 201 checksum = computeDigest(content); 202 blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 203 try (OutputStream out = backend.openForWrite(blobUri)) { 204 assertThat(out).isNotNull(); 205 out.write(content); 206 } 207 leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 208 try (OutputStream out = backend.openForWrite(leaseUri)) { 209 assertThat(out).isNull(); 210 } 211 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 2); 212 213 // Release all leases 214 Uri allLeases = BlobUri.builder(context).setAllLeasesParameters().build(); 215 216 backend.deleteFile(allLeases); 217 218 assertThat(blobStoreManager.getLeasedBlobs()).isEmpty(); 219 } 220 221 @Test deleteNonExistentFile_shouldThrow()222 public void deleteNonExistentFile_shouldThrow() throws Exception { 223 BlobStoreBackend backend = new BlobStoreBackend(context); 224 byte[] content = "deleteNonExistentFile_shouldThrow".getBytes(UTF_8); 225 String checksum = computeDigest(content); 226 227 Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build(); 228 229 assertThrows(IOException.class, () -> backend.deleteFile(uri)); 230 } 231 computeDigest(byte[] byteContent)232 private static String computeDigest(byte[] byteContent) throws Exception { 233 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 234 if (messageDigest == null) { 235 return ""; 236 } 237 return BaseEncoding.base16().lowerCase().encode(messageDigest.digest(byteContent)); 238 } 239 240 @Test writeLeaseExceedsLimit_shouldThrow()241 public void writeLeaseExceedsLimit_shouldThrow() throws Exception { 242 long initialRemainingQuota = blobStoreManager.getRemainingLeaseQuotaBytes(); 243 int numOfLeases = blobStoreManager.getLeasedBlobs().size(); 244 int numberOfFiles = 20; 245 int singleFileSize = (int) initialRemainingQuota / numberOfFiles; 246 long expectedRemainingQuota = initialRemainingQuota - singleFileSize * numberOfFiles; 247 248 // Create small files rather than one big file to avoid OutOfMemoryError 249 for (int i = 0; i < numberOfFiles; i++) { 250 byte[] content = generateRandomBytes(singleFileSize); 251 String checksum = computeDigest(content); 252 Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 253 254 try (OutputStream out = backend.openForWrite(blobUri)) { 255 assertThat(out).isNotNull(); 256 out.write(content); 257 } 258 Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 259 OutputStream out = backend.openForWrite(leaseUri); 260 assertThat(out).isNull(); 261 } 262 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + numberOfFiles); 263 assertThat(blobStoreManager.getRemainingLeaseQuotaBytes()).isEqualTo(expectedRemainingQuota); 264 265 // Write one more file bigger than available quota. Acquiring the lease on it will throw 266 // LimitExceededException. 267 byte[] content = generateRandomBytes((int) expectedRemainingQuota + 1); 268 String checksum = computeDigest(content); 269 Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build(); 270 try (OutputStream out = backend.openForWrite(blobUri)) { 271 assertThat(out).isNotNull(); 272 out.write(content); 273 } 274 Uri exceedingLeaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build(); 275 276 assertThrows(LimitExceededException.class, () -> backend.openForWrite(exceedingLeaseUri)); 277 278 assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + numberOfFiles); 279 assertThat(blobStoreManager.getRemainingLeaseQuotaBytes()).isEqualTo(expectedRemainingQuota); 280 } 281 generateRandomBytes(int length)282 private static byte[] generateRandomBytes(int length) { 283 byte[] content = new byte[length]; 284 new Random().nextBytes(content); 285 return content; 286 } 287 runShellCmd(String cmd)288 private static String runShellCmd(String cmd) throws IOException { 289 final UiDevice uiDevice = UiDevice.getInstance(getInstrumentation()); 290 final String result = uiDevice.executeShellCommand(cmd).trim(); 291 Log.i(TAG, "Output of '" + cmd + "': '" + result + "'"); 292 return result; 293 } 294 } 295