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.content.Context; 19 import android.net.Uri; 20 import android.text.TextUtils; 21 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 22 import com.google.common.base.Splitter; 23 import com.google.common.io.BaseEncoding; 24 import com.google.errorprone.annotations.CanIgnoreReturnValue; 25 import java.util.List; 26 27 /** Helper class for "blobstore" URIs. */ 28 public final class BlobUri { 29 // Uri path constants 30 public static final String SCHEME = "blobstore"; 31 private static final String LEASE_URI_SUFFIX = ".lease"; 32 private static final String CHECKSUM_SEPARATOR = "."; 33 private static final String ALL_LEASES_PATH = "*" + LEASE_URI_SUFFIX; 34 private static final int PATH_SIZE = 1; 35 // Uri query constants 36 private static final int QUERY_PARAMETERS = 1; // A single query element called expiryDateSecs 37 private static final String EXPIRY_DATE_QUERY_KEY = "expiryDateSecs"; 38 // MalformedException message strings 39 private static final String EXPECTED_BLOB_URI_PATH = "<non_empty_checksum>"; 40 private static final String EXPECTED_LEASE_URI_PATH = "<non_empty_checksum>.lease"; 41 private static final String EXPECTED_LEASE_URI_QUERY = "expiryDateSecs=<expiryDateSecs>"; 42 43 private static final Splitter SPLITTER = Splitter.on(CHECKSUM_SEPARATOR); 44 45 /** Returns a "blobstore" scheme URI. */ builder(Context context)46 public static Builder builder(Context context) { 47 return new Builder(context); 48 } 49 BlobUri()50 private BlobUri() {} 51 validateUri(Uri uri)52 static void validateUri(Uri uri) throws MalformedUriException { 53 validatePath(uri); 54 validateQuery(uri); 55 } 56 57 /** 58 * Validates the path of the "blobstore" scheme URI. 59 * 60 * <p>Theare only two permitted paths: 61 * 62 * <ul> 63 * <li><non_empty_checksum> 64 * <li><non_empty_checksum>.lease 65 * </ul> 66 */ validatePath(Uri uri)67 private static void validatePath(Uri uri) throws MalformedUriException { 68 List<String> pathSegments = uri.getPathSegments(); 69 if (pathSegments.size() != PATH_SIZE || !hasValidChecksumExtension(pathSegments.get(0))) { 70 throw new MalformedUriException( 71 String.format( 72 "The uri is malformed, expected %s or %s but found %s", 73 EXPECTED_BLOB_URI_PATH, EXPECTED_LEASE_URI_PATH, uri.getPath())); 74 } 75 } 76 hasValidChecksumExtension(String path)77 private static boolean hasValidChecksumExtension(String path) { 78 return SPLITTER.splitToList(path).size() == 1 79 || (isLeaseUri(path) && !TextUtils.equals(path, LEASE_URI_SUFFIX)); 80 } 81 82 /** Returns true if the path is of type "<checksum>.lease". */ isLeaseUri(String path)83 static boolean isLeaseUri(String path) { 84 return path.endsWith(LEASE_URI_SUFFIX); 85 } 86 87 /** Returns true if the path matches "*.lease". */ isAllLeasesUri(String path)88 static boolean isAllLeasesUri(String path) { 89 if (path.startsWith("/")) { 90 path = path.substring(1); 91 } 92 return TextUtils.equals(path, ALL_LEASES_PATH); 93 } 94 95 /** 96 * If available, validates the query part of the "blobstore" scheme URI. 97 * 98 * <p>There is one permitted query parameter: expiryDateSecs=<expiryDateSecs>. 99 */ validateQuery(Uri uri)100 private static void validateQuery(Uri uri) throws MalformedUriException { 101 if (TextUtils.isEmpty(uri.getQuery())) { 102 return; 103 } 104 if (uri.getQueryParameterNames().size() != QUERY_PARAMETERS 105 || uri.getQueryParameter(EXPIRY_DATE_QUERY_KEY) == null) { 106 throw new MalformedUriException( 107 String.format( 108 "The uri query is malformed, expected %s but found query %s", 109 EXPECTED_LEASE_URI_QUERY, uri.getQuery())); 110 } 111 } 112 113 /** 114 * Returns the checksum bytes encoded in the {@code path}. 115 * 116 * <p>To decode the bytes from the path, it uses the same encoding used by {@code // 117 * com.google.android.libraries.mobiledatadownload.internal.downloader.FileValidator}. 118 */ getChecksum(String path)119 static byte[] getChecksum(String path) { 120 if (path.startsWith("/")) { 121 path = path.substring(1); 122 } 123 return BaseEncoding.base16().lowerCase().decode(SPLITTER.splitToList(path).get(0)); 124 } 125 126 /* Parses the {@code query} and returns the encoded {@code expiryDateSecs}. */ getExpiryDateSecs(Uri uri)127 static long getExpiryDateSecs(Uri uri) throws MalformedUriException { 128 String query = uri.getQuery(); 129 if (TextUtils.isEmpty(query)) { 130 throw new MalformedUriException( 131 String.format("The uri query is null or empty, expected %s", EXPECTED_LEASE_URI_QUERY)); 132 } 133 String expiryDateSecsString = uri.getQueryParameter(EXPIRY_DATE_QUERY_KEY); 134 if (expiryDateSecsString == null) { 135 throw new MalformedUriException( 136 String.format( 137 "The uri query is malformed, expected %s but found %s", 138 EXPECTED_LEASE_URI_QUERY, query)); 139 } 140 long expiryDateSecs = Long.parseLong(expiryDateSecsString); 141 return expiryDateSecs; 142 } 143 144 /** A builder for "blobstore" scheme Uris. */ 145 public static class Builder { 146 private String path = ""; 147 private String packageName = ""; 148 private long expiryDateSecs; 149 Builder(Context context)150 private Builder(Context context) { 151 // TODO(b/149260496): remove/change meaning to packageName 152 this.packageName = context.getPackageName(); 153 } 154 155 @CanIgnoreReturnValue setBlobParameters(String checksum)156 public Builder setBlobParameters(String checksum) { 157 path = checksum; 158 return this; 159 } 160 161 @CanIgnoreReturnValue setLeaseParameters(String checksum, long expiryDateSecs)162 public Builder setLeaseParameters(String checksum, long expiryDateSecs) { 163 path = checksum + LEASE_URI_SUFFIX; 164 this.expiryDateSecs = expiryDateSecs; 165 return this; 166 } 167 168 @CanIgnoreReturnValue setAllLeasesParameters()169 public Builder setAllLeasesParameters() { 170 path = ALL_LEASES_PATH; 171 return this; 172 } 173 build()174 public Uri build() throws MalformedUriException { 175 Uri.Builder uriBuilder = new Uri.Builder().scheme(SCHEME).authority(packageName).path(path); 176 if (isLeaseUri(path) && !isAllLeasesUri(path)) { 177 uriBuilder.appendQueryParameter(EXPIRY_DATE_QUERY_KEY, String.valueOf(expiryDateSecs)); 178 } 179 Uri uri = uriBuilder.build(); 180 validateUri(uri); 181 return uri; 182 } 183 } 184 } 185