1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "content/browser/dom_storage/dom_storage_database.h" 6 7 #include "base/file_util.h" 8 #include "base/files/file_path.h" 9 #include "base/files/scoped_temp_dir.h" 10 #include "base/path_service.h" 11 #include "base/strings/utf_string_conversions.h" 12 #include "sql/statement.h" 13 #include "sql/test/scoped_error_ignorer.h" 14 #include "testing/gtest/include/gtest/gtest.h" 15 #include "third_party/sqlite/sqlite3.h" 16 17 using base::ASCIIToUTF16; 18 19 namespace content { 20 21 void CreateV1Table(sql::Connection* db) { 22 ASSERT_TRUE(db->is_open()); 23 ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable")); 24 ASSERT_TRUE(db->Execute( 25 "CREATE TABLE ItemTable (" 26 "key TEXT UNIQUE ON CONFLICT REPLACE, " 27 "value TEXT NOT NULL ON CONFLICT FAIL)")); 28 } 29 30 void CreateV2Table(sql::Connection* db) { 31 ASSERT_TRUE(db->is_open()); 32 ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable")); 33 ASSERT_TRUE(db->Execute( 34 "CREATE TABLE ItemTable (" 35 "key TEXT UNIQUE ON CONFLICT REPLACE, " 36 "value BLOB NOT NULL ON CONFLICT FAIL)")); 37 } 38 39 void CreateInvalidKeyColumnTable(sql::Connection* db) { 40 // Create a table with the key type as FLOAT - this is "invalid" 41 // as far as the DOM Storage db is concerned. 42 ASSERT_TRUE(db->is_open()); 43 ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable")); 44 ASSERT_TRUE(db->Execute( 45 "CREATE TABLE IF NOT EXISTS ItemTable (" 46 "key FLOAT UNIQUE ON CONFLICT REPLACE, " 47 "value BLOB NOT NULL ON CONFLICT FAIL)")); 48 } 49 void CreateInvalidValueColumnTable(sql::Connection* db) { 50 // Create a table with the value type as FLOAT - this is "invalid" 51 // as far as the DOM Storage db is concerned. 52 ASSERT_TRUE(db->is_open()); 53 ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable")); 54 ASSERT_TRUE(db->Execute( 55 "CREATE TABLE IF NOT EXISTS ItemTable (" 56 "key TEXT UNIQUE ON CONFLICT REPLACE, " 57 "value FLOAT NOT NULL ON CONFLICT FAIL)")); 58 } 59 60 void InsertDataV1(sql::Connection* db, 61 const base::string16& key, 62 const base::string16& value) { 63 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, 64 "INSERT INTO ItemTable VALUES (?,?)")); 65 statement.BindString16(0, key); 66 statement.BindString16(1, value); 67 ASSERT_TRUE(statement.is_valid()); 68 statement.Run(); 69 } 70 71 void CheckValuesMatch(DOMStorageDatabase* db, 72 const DOMStorageValuesMap& expected) { 73 DOMStorageValuesMap values_read; 74 db->ReadAllValues(&values_read); 75 EXPECT_EQ(expected.size(), values_read.size()); 76 77 DOMStorageValuesMap::const_iterator it = values_read.begin(); 78 for (; it != values_read.end(); ++it) { 79 base::string16 key = it->first; 80 base::NullableString16 value = it->second; 81 base::NullableString16 expected_value = expected.find(key)->second; 82 EXPECT_EQ(expected_value.string(), value.string()); 83 EXPECT_EQ(expected_value.is_null(), value.is_null()); 84 } 85 } 86 87 void CreateMapWithValues(DOMStorageValuesMap* values) { 88 base::string16 kCannedKeys[] = { 89 ASCIIToUTF16("test"), 90 ASCIIToUTF16("company"), 91 ASCIIToUTF16("date"), 92 ASCIIToUTF16("empty") 93 }; 94 base::NullableString16 kCannedValues[] = { 95 base::NullableString16(ASCIIToUTF16("123"), false), 96 base::NullableString16(ASCIIToUTF16("Google"), false), 97 base::NullableString16(ASCIIToUTF16("18-01-2012"), false), 98 base::NullableString16(base::string16(), false) 99 }; 100 for (unsigned i = 0; i < sizeof(kCannedKeys) / sizeof(kCannedKeys[0]); i++) 101 (*values)[kCannedKeys[i]] = kCannedValues[i]; 102 } 103 104 TEST(DOMStorageDatabaseTest, SimpleOpenAndClose) { 105 DOMStorageDatabase db; 106 EXPECT_FALSE(db.IsOpen()); 107 ASSERT_TRUE(db.LazyOpen(true)); 108 EXPECT_TRUE(db.IsOpen()); 109 EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion()); 110 db.Close(); 111 EXPECT_FALSE(db.IsOpen()); 112 } 113 114 TEST(DOMStorageDatabaseTest, CloseEmptyDatabaseDeletesFile) { 115 base::ScopedTempDir temp_dir; 116 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 117 base::FilePath file_name = 118 temp_dir.path().AppendASCII("TestDOMStorageDatabase.db"); 119 DOMStorageValuesMap storage; 120 CreateMapWithValues(&storage); 121 122 // First test the case that explicitly clearing the database will 123 // trigger its deletion from disk. 124 { 125 DOMStorageDatabase db(file_name); 126 EXPECT_EQ(file_name, db.file_path()); 127 ASSERT_TRUE(db.CommitChanges(false, storage)); 128 } 129 EXPECT_TRUE(base::PathExists(file_name)); 130 131 { 132 // Check that reading an existing db with data in it 133 // keeps the DB on disk on close. 134 DOMStorageDatabase db(file_name); 135 DOMStorageValuesMap values; 136 db.ReadAllValues(&values); 137 EXPECT_EQ(storage.size(), values.size()); 138 } 139 140 EXPECT_TRUE(base::PathExists(file_name)); 141 storage.clear(); 142 143 { 144 DOMStorageDatabase db(file_name); 145 ASSERT_TRUE(db.CommitChanges(true, storage)); 146 } 147 EXPECT_FALSE(base::PathExists(file_name)); 148 149 // Now ensure that a series of updates and removals whose net effect 150 // is an empty database also triggers deletion. 151 CreateMapWithValues(&storage); 152 { 153 DOMStorageDatabase db(file_name); 154 ASSERT_TRUE(db.CommitChanges(false, storage)); 155 } 156 157 EXPECT_TRUE(base::PathExists(file_name)); 158 159 { 160 DOMStorageDatabase db(file_name); 161 ASSERT_TRUE(db.CommitChanges(false, storage)); 162 DOMStorageValuesMap::iterator it = storage.begin(); 163 for (; it != storage.end(); ++it) 164 it->second = base::NullableString16(); 165 ASSERT_TRUE(db.CommitChanges(false, storage)); 166 } 167 EXPECT_FALSE(base::PathExists(file_name)); 168 } 169 170 TEST(DOMStorageDatabaseTest, TestLazyOpenIsLazy) { 171 // This test needs to operate with a file on disk to ensure that we will 172 // open a file that already exists when only invoking ReadAllValues. 173 base::ScopedTempDir temp_dir; 174 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 175 base::FilePath file_name = 176 temp_dir.path().AppendASCII("TestDOMStorageDatabase.db"); 177 178 DOMStorageDatabase db(file_name); 179 EXPECT_FALSE(db.IsOpen()); 180 DOMStorageValuesMap values; 181 db.ReadAllValues(&values); 182 // Reading an empty db should not open the database. 183 EXPECT_FALSE(db.IsOpen()); 184 185 values[ASCIIToUTF16("key")] = 186 base::NullableString16(ASCIIToUTF16("value"), false); 187 db.CommitChanges(false, values); 188 // Writing content should open the database. 189 EXPECT_TRUE(db.IsOpen()); 190 191 db.Close(); 192 ASSERT_FALSE(db.IsOpen()); 193 194 // Reading from an existing database should open the database. 195 CheckValuesMatch(&db, values); 196 EXPECT_TRUE(db.IsOpen()); 197 } 198 199 TEST(DOMStorageDatabaseTest, TestDetectSchemaVersion) { 200 DOMStorageDatabase db; 201 db.db_.reset(new sql::Connection()); 202 ASSERT_TRUE(db.db_->OpenInMemory()); 203 204 CreateInvalidValueColumnTable(db.db_.get()); 205 EXPECT_EQ(DOMStorageDatabase::INVALID, db.DetectSchemaVersion()); 206 207 CreateInvalidKeyColumnTable(db.db_.get()); 208 EXPECT_EQ(DOMStorageDatabase::INVALID, db.DetectSchemaVersion()); 209 210 CreateV1Table(db.db_.get()); 211 EXPECT_EQ(DOMStorageDatabase::V1, db.DetectSchemaVersion()); 212 213 CreateV2Table(db.db_.get()); 214 EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion()); 215 } 216 217 TEST(DOMStorageDatabaseTest, TestLazyOpenUpgradesDatabase) { 218 // This test needs to operate with a file on disk so that we 219 // can create a table at version 1 and then close it again 220 // so that LazyOpen sees there is work to do (LazyOpen will return 221 // early if the database is already open). 222 base::ScopedTempDir temp_dir; 223 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 224 base::FilePath file_name = 225 temp_dir.path().AppendASCII("TestDOMStorageDatabase.db"); 226 227 DOMStorageDatabase db(file_name); 228 db.db_.reset(new sql::Connection()); 229 ASSERT_TRUE(db.db_->Open(file_name)); 230 CreateV1Table(db.db_.get()); 231 db.Close(); 232 233 EXPECT_TRUE(db.LazyOpen(true)); 234 EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion()); 235 } 236 237 TEST(DOMStorageDatabaseTest, SimpleWriteAndReadBack) { 238 DOMStorageDatabase db; 239 240 DOMStorageValuesMap storage; 241 CreateMapWithValues(&storage); 242 243 EXPECT_TRUE(db.CommitChanges(false, storage)); 244 CheckValuesMatch(&db, storage); 245 } 246 247 TEST(DOMStorageDatabaseTest, WriteWithClear) { 248 DOMStorageDatabase db; 249 250 DOMStorageValuesMap storage; 251 CreateMapWithValues(&storage); 252 253 ASSERT_TRUE(db.CommitChanges(false, storage)); 254 CheckValuesMatch(&db, storage); 255 256 // Insert some values, clearing the database first. 257 storage.clear(); 258 storage[ASCIIToUTF16("another_key")] = 259 base::NullableString16(ASCIIToUTF16("test"), false); 260 ASSERT_TRUE(db.CommitChanges(true, storage)); 261 CheckValuesMatch(&db, storage); 262 263 // Now clear the values without inserting any new ones. 264 storage.clear(); 265 ASSERT_TRUE(db.CommitChanges(true, storage)); 266 CheckValuesMatch(&db, storage); 267 } 268 269 TEST(DOMStorageDatabaseTest, UpgradeFromV1ToV2WithData) { 270 const base::string16 kCannedKey = ASCIIToUTF16("foo"); 271 const base::NullableString16 kCannedValue(ASCIIToUTF16("bar"), false); 272 DOMStorageValuesMap expected; 273 expected[kCannedKey] = kCannedValue; 274 275 DOMStorageDatabase db; 276 db.db_.reset(new sql::Connection()); 277 ASSERT_TRUE(db.db_->OpenInMemory()); 278 CreateV1Table(db.db_.get()); 279 InsertDataV1(db.db_.get(), kCannedKey, kCannedValue.string()); 280 281 ASSERT_TRUE(db.UpgradeVersion1To2()); 282 283 EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion()); 284 285 CheckValuesMatch(&db, expected); 286 } 287 288 TEST(DOMStorageDatabaseTest, TestSimpleRemoveOneValue) { 289 DOMStorageDatabase db; 290 291 ASSERT_TRUE(db.LazyOpen(true)); 292 const base::string16 kCannedKey = ASCIIToUTF16("test"); 293 const base::NullableString16 kCannedValue(ASCIIToUTF16("data"), false); 294 DOMStorageValuesMap expected; 295 expected[kCannedKey] = kCannedValue; 296 297 // First write some data into the database. 298 ASSERT_TRUE(db.CommitChanges(false, expected)); 299 CheckValuesMatch(&db, expected); 300 301 DOMStorageValuesMap values; 302 // A null string in the map should mean that that key gets 303 // removed. 304 values[kCannedKey] = base::NullableString16(); 305 EXPECT_TRUE(db.CommitChanges(false, values)); 306 307 expected.clear(); 308 CheckValuesMatch(&db, expected); 309 } 310 311 TEST(DOMStorageDatabaseTest, TestCanOpenAndReadWebCoreDatabase) { 312 base::FilePath webcore_database; 313 PathService::Get(base::DIR_SOURCE_ROOT, &webcore_database); 314 webcore_database = webcore_database.AppendASCII("webkit"); 315 webcore_database = webcore_database.AppendASCII("data"); 316 webcore_database = webcore_database.AppendASCII("dom_storage"); 317 webcore_database = 318 webcore_database.AppendASCII("webcore_test_database.localstorage"); 319 320 ASSERT_TRUE(base::PathExists(webcore_database)); 321 322 DOMStorageDatabase db(webcore_database); 323 DOMStorageValuesMap values; 324 db.ReadAllValues(&values); 325 EXPECT_TRUE(db.IsOpen()); 326 EXPECT_EQ(2u, values.size()); 327 328 DOMStorageValuesMap::const_iterator it = 329 values.find(ASCIIToUTF16("value")); 330 EXPECT_TRUE(it != values.end()); 331 EXPECT_EQ(ASCIIToUTF16("I am in local storage!"), it->second.string()); 332 333 it = values.find(ASCIIToUTF16("timestamp")); 334 EXPECT_TRUE(it != values.end()); 335 EXPECT_EQ(ASCIIToUTF16("1326738338841"), it->second.string()); 336 337 it = values.find(ASCIIToUTF16("not_there")); 338 EXPECT_TRUE(it == values.end()); 339 } 340 341 TEST(DOMStorageDatabaseTest, TestCanOpenFileThatIsNotADatabase) { 342 // Write into the temporary file first. 343 base::ScopedTempDir temp_dir; 344 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 345 base::FilePath file_name = 346 temp_dir.path().AppendASCII("TestDOMStorageDatabase.db"); 347 348 const char kData[] = "I am not a database."; 349 base::WriteFile(file_name, kData, strlen(kData)); 350 351 { 352 sql::ScopedErrorIgnorer ignore_errors; 353 ignore_errors.IgnoreError(SQLITE_IOERR_SHORT_READ); 354 355 // Try and open the file. As it's not a database, we should end up deleting 356 // it and creating a new, valid file, so everything should actually 357 // succeed. 358 DOMStorageDatabase db(file_name); 359 DOMStorageValuesMap values; 360 CreateMapWithValues(&values); 361 EXPECT_TRUE(db.CommitChanges(true, values)); 362 EXPECT_TRUE(db.CommitChanges(false, values)); 363 EXPECT_TRUE(db.IsOpen()); 364 365 CheckValuesMatch(&db, values); 366 367 ASSERT_TRUE(ignore_errors.CheckIgnoredErrors()); 368 } 369 370 { 371 sql::ScopedErrorIgnorer ignore_errors; 372 ignore_errors.IgnoreError(SQLITE_CANTOPEN); 373 374 // Try to open a directory, we should fail gracefully and not attempt 375 // to delete it. 376 DOMStorageDatabase db(temp_dir.path()); 377 DOMStorageValuesMap values; 378 CreateMapWithValues(&values); 379 EXPECT_FALSE(db.CommitChanges(true, values)); 380 EXPECT_FALSE(db.CommitChanges(false, values)); 381 EXPECT_FALSE(db.IsOpen()); 382 383 values.clear(); 384 385 db.ReadAllValues(&values); 386 EXPECT_EQ(0u, values.size()); 387 EXPECT_FALSE(db.IsOpen()); 388 389 EXPECT_TRUE(base::PathExists(temp_dir.path())); 390 391 ASSERT_TRUE(ignore_errors.CheckIgnoredErrors()); 392 } 393 } 394 395 } // namespace content 396