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 android.net.Uri; 19 import android.text.TextUtils; 20 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 21 import com.google.android.libraries.mobiledatadownload.file.spi.Backend; 22 import java.io.ByteArrayInputStream; 23 import java.io.ByteArrayOutputStream; 24 import java.io.FileNotFoundException; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.OutputStream; 28 import java.nio.ByteBuffer; 29 import java.util.HashMap; 30 import java.util.Map; 31 32 /** 33 * A {@link Backend} whose files exist only in memory. Files are never cached to disk and exist only 34 * for the duration of the backend itself. It has the following limitations: 35 * 36 * <ul> 37 * <li>The backend is flat; directory operations are unsupported 38 * <li>{@link Backend#openForAppend} is unsupported 39 * <li>Native filesytem features such as FD's and syncing are unsupported 40 * </ul> 41 * 42 * <p>See <internal> for further documentation. 43 */ 44 // TODO(b/111694348): consider adding directory support 45 public final class MemoryBackend implements Backend { 46 47 // Uri scheme the backend is registered under (package-private for use in {@link MemoryUri}) 48 static final String URI_SCHEME = "memory"; 49 50 private final Map<Uri, ByteBuffer> files = new HashMap<>(); 51 52 @Override name()53 public String name() { 54 return URI_SCHEME; 55 } 56 57 @Override openForRead(Uri uri)58 public InputStream openForRead(Uri uri) throws IOException { 59 validate(uri); 60 ByteBuffer buf = files.get(uri); 61 if (buf == null) { 62 throw new FileNotFoundException(String.format("%s (No such file)", uri)); 63 } 64 ByteBuffer slice = buf.slice(); // Points to the same data but has an independent read position 65 return new ByteArrayInputStream(slice.array(), slice.arrayOffset(), slice.limit()); 66 } 67 68 @Override openForWrite(Uri uri)69 public OutputStream openForWrite(Uri uri) throws IOException { 70 validate(uri); 71 // Note that this is atomic in the sense that the data isn't flushed until closing the stream 72 return new ByteArrayOutputStream() { 73 @Override 74 public void close() throws IOException { 75 files.put(uri, ByteBuffer.wrap(this.buf, 0, this.count)); 76 } 77 }; 78 } 79 80 @Override 81 public void deleteFile(Uri uri) throws IOException { 82 validate(uri); 83 if (files.remove(uri) == null) { 84 throw new FileNotFoundException(String.format("%s does not exist", uri)); 85 } 86 } 87 88 @Override 89 public void rename(Uri from, Uri to) throws IOException { 90 validate(from); 91 validate(to); 92 ByteBuffer buf = files.remove(from); 93 if (buf == null) { 94 throw new IOException(String.format("%s could not be renamed to %s", from, to)); 95 } 96 files.put(to, buf); 97 } 98 99 @Override 100 public boolean exists(Uri uri) throws IOException { 101 validate(uri); 102 return files.containsKey(uri); 103 } 104 105 @Override 106 public long fileSize(Uri uri) throws IOException { 107 validate(uri); 108 ByteBuffer buf = files.get(uri); 109 return buf != null ? buf.limit() : 0; 110 } 111 112 /** Validates that {@code uri} is a non-empty memory: Uri with no authority or query. */ 113 private static void validate(Uri uri) throws MalformedUriException { 114 if (!URI_SCHEME.equals(uri.getScheme())) { 115 throw new MalformedUriException("Expected scheme to be " + URI_SCHEME); 116 } 117 if (TextUtils.isEmpty(uri.getSchemeSpecificPart())) { 118 throw new MalformedUriException("Expected a non-empty file identifier"); 119 } 120 if (!TextUtils.isEmpty(uri.getAuthority())) { 121 throw new MalformedUriException("Did not expect an authority"); 122 } 123 if (!TextUtils.isEmpty(uri.getQuery())) { 124 throw new MalformedUriException("Did not expect a query"); 125 } 126 } 127 } 128