• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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