1 /*
2  ** Copyright 2023, The Android Open Source Project
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 
17 #include "MultifileBlobCache.h"
18 
19 #include <android-base/properties.h>
20 #include <android-base/test_utils.h>
21 #include <fcntl.h>
22 #include <gtest/gtest.h>
23 #include <stdio.h>
24 
25 #include <fstream>
26 #include <memory>
27 
28 using namespace std::literals;
29 
30 namespace android {
31 
32 template <typename T>
33 using sp = std::shared_ptr<T>;
34 
35 constexpr size_t kMaxKeySize = 2 * 1024;
36 constexpr size_t kMaxValueSize = 6 * 1024;
37 constexpr size_t kMaxTotalSize = 32 * 1024;
38 constexpr size_t kMaxTotalEntries = 64;
39 
40 class MultifileBlobCacheTest : public ::testing::Test {
41 protected:
SetUp()42     virtual void SetUp() {
43         clearProperties();
44         mTempFile.reset(new TemporaryFile());
45         mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize,
46                                           kMaxTotalEntries, &mTempFile->path[0]));
47     }
48 
TearDown()49     virtual void TearDown() {
50         clearProperties();
51         mMBC.reset();
52     }
53 
54     int getFileDescriptorCount();
55     std::vector<std::string> getCacheEntries();
56 
57     void clearProperties();
58 
59     std::unique_ptr<TemporaryFile> mTempFile;
60     std::unique_ptr<MultifileBlobCache> mMBC;
61 };
62 
clearProperties()63 void MultifileBlobCacheTest::clearProperties() {
64     // Clear any debug properties used in the tests
65     base::SetProperty("debug.egl.blobcache.cache_version", "");
66     base::WaitForProperty("debug.egl.blobcache.cache_version", "");
67 
68     base::SetProperty("debug.egl.blobcache.build_id", "");
69     base::WaitForProperty("debug.egl.blobcache.build_id", "");
70 }
71 
TEST_F(MultifileBlobCacheTest,CacheSingleValueSucceeds)72 TEST_F(MultifileBlobCacheTest, CacheSingleValueSucceeds) {
73     unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
74     mMBC->set("abcd", 4, "efgh", 4);
75     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
76     ASSERT_EQ('e', buf[0]);
77     ASSERT_EQ('f', buf[1]);
78     ASSERT_EQ('g', buf[2]);
79     ASSERT_EQ('h', buf[3]);
80 }
81 
TEST_F(MultifileBlobCacheTest,CacheTwoValuesSucceeds)82 TEST_F(MultifileBlobCacheTest, CacheTwoValuesSucceeds) {
83     unsigned char buf[2] = {0xee, 0xee};
84     mMBC->set("ab", 2, "cd", 2);
85     mMBC->set("ef", 2, "gh", 2);
86     ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
87     ASSERT_EQ('c', buf[0]);
88     ASSERT_EQ('d', buf[1]);
89     ASSERT_EQ(size_t(2), mMBC->get("ef", 2, buf, 2));
90     ASSERT_EQ('g', buf[0]);
91     ASSERT_EQ('h', buf[1]);
92 }
93 
TEST_F(MultifileBlobCacheTest,GetSetTwiceSucceeds)94 TEST_F(MultifileBlobCacheTest, GetSetTwiceSucceeds) {
95     unsigned char buf[2] = {0xee, 0xee};
96     mMBC->set("ab", 2, "cd", 2);
97     ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
98     ASSERT_EQ('c', buf[0]);
99     ASSERT_EQ('d', buf[1]);
100     // Use the same key, but different value
101     mMBC->set("ab", 2, "ef", 2);
102     ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
103     ASSERT_EQ('e', buf[0]);
104     ASSERT_EQ('f', buf[1]);
105 }
106 
TEST_F(MultifileBlobCacheTest,GetOnlyWritesInsideBounds)107 TEST_F(MultifileBlobCacheTest, GetOnlyWritesInsideBounds) {
108     unsigned char buf[6] = {0xee, 0xee, 0xee, 0xee, 0xee, 0xee};
109     mMBC->set("abcd", 4, "efgh", 4);
110     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf + 1, 4));
111     ASSERT_EQ(0xee, buf[0]);
112     ASSERT_EQ('e', buf[1]);
113     ASSERT_EQ('f', buf[2]);
114     ASSERT_EQ('g', buf[3]);
115     ASSERT_EQ('h', buf[4]);
116     ASSERT_EQ(0xee, buf[5]);
117 }
118 
TEST_F(MultifileBlobCacheTest,GetOnlyWritesIfBufferIsLargeEnough)119 TEST_F(MultifileBlobCacheTest, GetOnlyWritesIfBufferIsLargeEnough) {
120     unsigned char buf[3] = {0xee, 0xee, 0xee};
121     mMBC->set("abcd", 4, "efgh", 4);
122     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 3));
123     ASSERT_EQ(0xee, buf[0]);
124     ASSERT_EQ(0xee, buf[1]);
125     ASSERT_EQ(0xee, buf[2]);
126 }
127 
TEST_F(MultifileBlobCacheTest,GetDoesntAccessNullBuffer)128 TEST_F(MultifileBlobCacheTest, GetDoesntAccessNullBuffer) {
129     mMBC->set("abcd", 4, "efgh", 4);
130     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, nullptr, 0));
131 }
132 
TEST_F(MultifileBlobCacheTest,MultipleSetsCacheLatestValue)133 TEST_F(MultifileBlobCacheTest, MultipleSetsCacheLatestValue) {
134     unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
135     mMBC->set("abcd", 4, "efgh", 4);
136     mMBC->set("abcd", 4, "ijkl", 4);
137     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
138     ASSERT_EQ('i', buf[0]);
139     ASSERT_EQ('j', buf[1]);
140     ASSERT_EQ('k', buf[2]);
141     ASSERT_EQ('l', buf[3]);
142 }
143 
TEST_F(MultifileBlobCacheTest,SecondSetKeepsFirstValueIfTooLarge)144 TEST_F(MultifileBlobCacheTest, SecondSetKeepsFirstValueIfTooLarge) {
145     unsigned char buf[kMaxValueSize + 1] = {0xee, 0xee, 0xee, 0xee};
146     mMBC->set("abcd", 4, "efgh", 4);
147     mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
148     ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
149     ASSERT_EQ('e', buf[0]);
150     ASSERT_EQ('f', buf[1]);
151     ASSERT_EQ('g', buf[2]);
152     ASSERT_EQ('h', buf[3]);
153 }
154 
TEST_F(MultifileBlobCacheTest,DoesntCacheIfKeyIsTooBig)155 TEST_F(MultifileBlobCacheTest, DoesntCacheIfKeyIsTooBig) {
156     char key[kMaxKeySize + 1];
157     unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
158     for (int i = 0; i < kMaxKeySize + 1; i++) {
159         key[i] = 'a';
160     }
161     mMBC->set(key, kMaxKeySize + 1, "bbbb", 4);
162     ASSERT_EQ(size_t(0), mMBC->get(key, kMaxKeySize + 1, buf, 4));
163     ASSERT_EQ(0xee, buf[0]);
164     ASSERT_EQ(0xee, buf[1]);
165     ASSERT_EQ(0xee, buf[2]);
166     ASSERT_EQ(0xee, buf[3]);
167 }
168 
TEST_F(MultifileBlobCacheTest,DoesntCacheIfValueIsTooBig)169 TEST_F(MultifileBlobCacheTest, DoesntCacheIfValueIsTooBig) {
170     char buf[kMaxValueSize + 1];
171     for (int i = 0; i < kMaxValueSize + 1; i++) {
172         buf[i] = 'b';
173     }
174     mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
175     for (int i = 0; i < kMaxValueSize + 1; i++) {
176         buf[i] = 0xee;
177     }
178     ASSERT_EQ(size_t(0), mMBC->get("abcd", 4, buf, kMaxValueSize + 1));
179     for (int i = 0; i < kMaxValueSize + 1; i++) {
180         SCOPED_TRACE(i);
181         ASSERT_EQ(0xee, buf[i]);
182     }
183 }
184 
TEST_F(MultifileBlobCacheTest,CacheMaxKeySizeSucceeds)185 TEST_F(MultifileBlobCacheTest, CacheMaxKeySizeSucceeds) {
186     char key[kMaxKeySize];
187     unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
188     for (int i = 0; i < kMaxKeySize; i++) {
189         key[i] = 'a';
190     }
191     mMBC->set(key, kMaxKeySize, "wxyz", 4);
192     ASSERT_EQ(size_t(4), mMBC->get(key, kMaxKeySize, buf, 4));
193     ASSERT_EQ('w', buf[0]);
194     ASSERT_EQ('x', buf[1]);
195     ASSERT_EQ('y', buf[2]);
196     ASSERT_EQ('z', buf[3]);
197 }
198 
TEST_F(MultifileBlobCacheTest,CacheMaxValueSizeSucceeds)199 TEST_F(MultifileBlobCacheTest, CacheMaxValueSizeSucceeds) {
200     char buf[kMaxValueSize];
201     for (int i = 0; i < kMaxValueSize; i++) {
202         buf[i] = 'b';
203     }
204     mMBC->set("abcd", 4, buf, kMaxValueSize);
205     for (int i = 0; i < kMaxValueSize; i++) {
206         buf[i] = 0xee;
207     }
208     mMBC->get("abcd", 4, buf, kMaxValueSize);
209     for (int i = 0; i < kMaxValueSize; i++) {
210         SCOPED_TRACE(i);
211         ASSERT_EQ('b', buf[i]);
212     }
213 }
214 
TEST_F(MultifileBlobCacheTest,CacheMaxKeyAndValueSizeSucceeds)215 TEST_F(MultifileBlobCacheTest, CacheMaxKeyAndValueSizeSucceeds) {
216     char key[kMaxKeySize];
217     for (int i = 0; i < kMaxKeySize; i++) {
218         key[i] = 'a';
219     }
220     char buf[kMaxValueSize];
221     for (int i = 0; i < kMaxValueSize; i++) {
222         buf[i] = 'b';
223     }
224     mMBC->set(key, kMaxKeySize, buf, kMaxValueSize);
225     for (int i = 0; i < kMaxValueSize; i++) {
226         buf[i] = 0xee;
227     }
228     mMBC->get(key, kMaxKeySize, buf, kMaxValueSize);
229     for (int i = 0; i < kMaxValueSize; i++) {
230         SCOPED_TRACE(i);
231         ASSERT_EQ('b', buf[i]);
232     }
233 }
234 
TEST_F(MultifileBlobCacheTest,CacheMaxEntrySucceeds)235 TEST_F(MultifileBlobCacheTest, CacheMaxEntrySucceeds) {
236     // Fill the cache with max entries
237     int i = 0;
238     for (i = 0; i < kMaxTotalEntries; i++) {
239         mMBC->set(std::to_string(i).c_str(), sizeof(i), std::to_string(i).c_str(), sizeof(i));
240     }
241 
242     // Ensure it is full
243     ASSERT_EQ(mMBC->getTotalEntries(), kMaxTotalEntries);
244 
245     // Add another entry
246     mMBC->set(std::to_string(i).c_str(), sizeof(i), std::to_string(i).c_str(), sizeof(i));
247 
248     // Ensure total entries is cut in half + 1
249     ASSERT_EQ(mMBC->getTotalEntries(), kMaxTotalEntries / 2 + 1);
250 }
251 
TEST_F(MultifileBlobCacheTest,CacheMinKeyAndValueSizeSucceeds)252 TEST_F(MultifileBlobCacheTest, CacheMinKeyAndValueSizeSucceeds) {
253     unsigned char buf[1] = {0xee};
254     mMBC->set("x", 1, "y", 1);
255     ASSERT_EQ(size_t(1), mMBC->get("x", 1, buf, 1));
256     ASSERT_EQ('y', buf[0]);
257 }
258 
getFileDescriptorCount()259 int MultifileBlobCacheTest::getFileDescriptorCount() {
260     DIR* directory = opendir("/proc/self/fd");
261 
262     int fileCount = 0;
263     struct dirent* entry;
264     while ((entry = readdir(directory)) != NULL) {
265         fileCount++;
266         // printf("File: %s\n", entry->d_name);
267     }
268 
269     closedir(directory);
270     return fileCount;
271 }
272 
TEST_F(MultifileBlobCacheTest,EnsureFileDescriptorsClosed)273 TEST_F(MultifileBlobCacheTest, EnsureFileDescriptorsClosed) {
274     // Populate the cache with a bunch of entries
275     for (int i = 0; i < kMaxTotalEntries; i++) {
276         // printf("Caching: %i", i);
277 
278         // Use the index as the key and value
279         mMBC->set(&i, sizeof(i), &i, sizeof(i));
280 
281         int result = 0;
282         ASSERT_EQ(sizeof(i), mMBC->get(&i, sizeof(i), &result, sizeof(result)));
283         ASSERT_EQ(i, result);
284     }
285 
286     // Ensure we don't have a bunch of open fds
287     ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
288 
289     // Close the cache so everything writes out
290     mMBC->finish();
291     mMBC.reset();
292 
293     // Now open it again and ensure we still don't have a bunch of open fds
294     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
295                                       &mTempFile->path[0]));
296 
297     // Check after initialization
298     ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
299 
300     for (int i = 0; i < kMaxTotalEntries; i++) {
301         int result = 0;
302         ASSERT_EQ(sizeof(i), mMBC->get(&i, sizeof(i), &result, sizeof(result)));
303         ASSERT_EQ(i, result);
304     }
305 
306     // And again after we've actually used it
307     ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
308 }
309 
getCacheEntries()310 std::vector<std::string> MultifileBlobCacheTest::getCacheEntries() {
311     std::string cachePath = &mTempFile->path[0];
312     std::string multifileDirName = cachePath + ".multifile";
313     std::vector<std::string> cacheEntries;
314 
315     struct stat info;
316     if (stat(multifileDirName.c_str(), &info) == 0) {
317         // We have a multifile dir. Skip the status file and return the only entry.
318         DIR* dir;
319         struct dirent* entry;
320         if ((dir = opendir(multifileDirName.c_str())) != nullptr) {
321             while ((entry = readdir(dir)) != nullptr) {
322                 if (entry->d_name == "."s || entry->d_name == ".."s) {
323                     continue;
324                 }
325                 if (strcmp(entry->d_name, kMultifileBlobCacheStatusFile) == 0) {
326                     continue;
327                 }
328                 cacheEntries.push_back(multifileDirName + "/" + entry->d_name);
329             }
330         } else {
331             printf("Unable to open %s, error: %s\n", multifileDirName.c_str(),
332                    std::strerror(errno));
333         }
334     } else {
335         printf("Unable to stat %s, error: %s\n", multifileDirName.c_str(), std::strerror(errno));
336     }
337 
338     return cacheEntries;
339 }
340 
TEST_F(MultifileBlobCacheTest,CacheContainsStatus)341 TEST_F(MultifileBlobCacheTest, CacheContainsStatus) {
342     struct stat info;
343     std::stringstream statusFile;
344     statusFile << &mTempFile->path[0] << ".multifile/" << kMultifileBlobCacheStatusFile;
345 
346     // After INIT, cache should have a status
347     ASSERT_TRUE(stat(statusFile.str().c_str(), &info) == 0);
348 
349     // Set one entry
350     mMBC->set("abcd", 4, "efgh", 4);
351 
352     // Close the cache so everything writes out
353     mMBC->finish();
354     mMBC.reset();
355 
356     // Ensure status lives after closing the cache
357     ASSERT_TRUE(stat(statusFile.str().c_str(), &info) == 0);
358 
359     // Open the cache again
360     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
361                                       &mTempFile->path[0]));
362 
363     // Ensure we still have a status
364     ASSERT_TRUE(stat(statusFile.str().c_str(), &info) == 0);
365 }
366 
367 // Verify missing cache status file causes cache the be cleared
TEST_F(MultifileBlobCacheTest,MissingCacheStatusClears)368 TEST_F(MultifileBlobCacheTest, MissingCacheStatusClears) {
369     // Set one entry
370     mMBC->set("abcd", 4, "efgh", 4);
371 
372     // Close the cache so everything writes out
373     mMBC->finish();
374     mMBC.reset();
375 
376     // Ensure there is one cache entry
377     ASSERT_EQ(getCacheEntries().size(), 1);
378 
379     // Delete the status file
380     std::stringstream statusFile;
381     statusFile << &mTempFile->path[0] << ".multifile/" << kMultifileBlobCacheStatusFile;
382     remove(statusFile.str().c_str());
383 
384     // Open the cache again and ensure no cache hits
385     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
386                                       &mTempFile->path[0]));
387 
388     // Ensure we have no entries
389     ASSERT_EQ(getCacheEntries().size(), 0);
390 }
391 
392 // Verify modified cache status file BEGIN causes cache to be cleared
TEST_F(MultifileBlobCacheTest,ModifiedCacheStatusBeginClears)393 TEST_F(MultifileBlobCacheTest, ModifiedCacheStatusBeginClears) {
394     // Set one entry
395     mMBC->set("abcd", 4, "efgh", 4);
396 
397     // Close the cache so everything writes out
398     mMBC->finish();
399     mMBC.reset();
400 
401     // Ensure there is one cache entry
402     ASSERT_EQ(getCacheEntries().size(), 1);
403 
404     // Modify the status file
405     std::stringstream statusFile;
406     statusFile << &mTempFile->path[0] << ".multifile/" << kMultifileBlobCacheStatusFile;
407 
408     // Stomp on the beginning of the cache file
409     const char* stomp = "BADF00D";
410     std::fstream fs(statusFile.str());
411     fs.seekp(0, std::ios_base::beg);
412     fs.write(stomp, strlen(stomp));
413     fs.flush();
414     fs.close();
415 
416     // Open the cache again and ensure no cache hits
417     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
418                                       &mTempFile->path[0]));
419 
420     // Ensure we have no entries
421     ASSERT_EQ(getCacheEntries().size(), 0);
422 }
423 
424 // Verify modified cache status file END causes cache to be cleared
TEST_F(MultifileBlobCacheTest,ModifiedCacheStatusEndClears)425 TEST_F(MultifileBlobCacheTest, ModifiedCacheStatusEndClears) {
426     // Set one entry
427     mMBC->set("abcd", 4, "efgh", 4);
428 
429     // Close the cache so everything writes out
430     mMBC->finish();
431     mMBC.reset();
432 
433     // Ensure there is one cache entry
434     ASSERT_EQ(getCacheEntries().size(), 1);
435 
436     // Modify the status file
437     std::stringstream statusFile;
438     statusFile << &mTempFile->path[0] << ".multifile/" << kMultifileBlobCacheStatusFile;
439 
440     // Stomp on the END of the cache status file, modifying its contents
441     const char* stomp = "BADF00D";
442     std::fstream fs(statusFile.str());
443     fs.seekp(-strlen(stomp), std::ios_base::end);
444     fs.write(stomp, strlen(stomp));
445     fs.flush();
446     fs.close();
447 
448     // Open the cache again and ensure no cache hits
449     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
450                                       &mTempFile->path[0]));
451 
452     // Ensure we have no entries
453     ASSERT_EQ(getCacheEntries().size(), 0);
454 }
455 
456 // Verify mismatched cacheVersion causes cache to be cleared
TEST_F(MultifileBlobCacheTest,MismatchedCacheVersionClears)457 TEST_F(MultifileBlobCacheTest, MismatchedCacheVersionClears) {
458     // Set one entry
459     mMBC->set("abcd", 4, "efgh", 4);
460 
461     // Close the cache so everything writes out
462     mMBC->finish();
463     mMBC.reset();
464 
465     // Ensure there is one cache entry
466     ASSERT_EQ(getCacheEntries().size(), 1);
467 
468     // Set a debug cacheVersion
469     std::string newCacheVersion = std::to_string(kMultifileBlobCacheVersion + 1);
470     ASSERT_TRUE(base::SetProperty("debug.egl.blobcache.cache_version", newCacheVersion.c_str()));
471     ASSERT_TRUE(
472             base::WaitForProperty("debug.egl.blobcache.cache_version", newCacheVersion.c_str()));
473 
474     // Open the cache again and ensure no cache hits
475     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
476                                       &mTempFile->path[0]));
477 
478     // Ensure we have no entries
479     ASSERT_EQ(getCacheEntries().size(), 0);
480 }
481 
482 // Verify mismatched buildId causes cache to be cleared
TEST_F(MultifileBlobCacheTest,MismatchedBuildIdClears)483 TEST_F(MultifileBlobCacheTest, MismatchedBuildIdClears) {
484     // Set one entry
485     mMBC->set("abcd", 4, "efgh", 4);
486 
487     // Close the cache so everything writes out
488     mMBC->finish();
489     mMBC.reset();
490 
491     // Ensure there is one cache entry
492     ASSERT_EQ(getCacheEntries().size(), 1);
493 
494     // Set a debug buildId
495     base::SetProperty("debug.egl.blobcache.build_id", "foo");
496     base::WaitForProperty("debug.egl.blobcache.build_id", "foo");
497 
498     // Open the cache again and ensure no cache hits
499     mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
500                                       &mTempFile->path[0]));
501 
502     // Ensure we have no entries
503     ASSERT_EQ(getCacheEntries().size(), 0);
504 }
505 
506 } // namespace android
507