PathPartTable.cpp (17562B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "PathPartTable.h" 5 #include <winget/SQLiteStatementBuilder.h> 6 7 8 namespace AppInstaller::Repository::Microsoft::Schema::V1_0 9 { 10 using namespace std::string_view_literals; 11 static constexpr std::string_view s_PathPartTable_Table_Name = "pathparts"sv; 12 static constexpr std::string_view s_PathPartTable_PrimaryKeyIndex_Name = "pathparts_pkindex"sv; 13 static constexpr std::string_view s_PathPartTable_ParentIndex_Name = "pathparts_parentidx"sv; 14 static constexpr std::string_view s_PathPartTable_ParentValue_Name = "parent"sv; 15 static constexpr std::string_view s_PathPartTable_PartValue_Name = "pathpart"sv; 16 17 namespace 18 { 19 // Attempts to select a path part given the input. 20 // Returns an no value if none exists, or the rowid of the part if it is found. 21 std::optional<SQLite::rowid_t> SelectPathPart(SQLite::Connection& connection, std::optional<SQLite::rowid_t> parent, std::string_view part) 22 { 23 SQLite::Builder::StatementBuilder builder; 24 builder.Select(SQLite::RowIDName).From(s_PathPartTable_Table_Name). 25 Where(s_PathPartTable_ParentValue_Name).Equals(parent).And(s_PathPartTable_PartValue_Name).Equals(part); 26 27 SQLite::Statement select = builder.Prepare(connection); 28 29 if (select.Step()) 30 { 31 return select.GetColumn<SQLite::rowid_t>(0); 32 } 33 else 34 { 35 return {}; 36 } 37 } 38 39 // Inserts the given path part into the table, returning the rowid of the inserted row. 40 SQLite::rowid_t InsertPathPart(SQLite::Connection& connection, std::optional<SQLite::rowid_t> parent, std::string_view part) 41 { 42 THROW_HR_IF(E_INVALIDARG, part.empty()); 43 44 SQLite::Builder::StatementBuilder builder; 45 builder.InsertInto(s_PathPartTable_Table_Name).Columns({ s_PathPartTable_ParentValue_Name, s_PathPartTable_PartValue_Name }).Values(parent, part); 46 47 builder.Execute(connection); 48 49 return connection.GetLastInsertRowID(); 50 } 51 52 // Inserts the no path part into the table. 53 SQLite::rowid_t InsertNoPathPart(SQLite::Connection& connection) 54 { 55 SQLite::Builder::StatementBuilder builder; 56 builder. 57 InsertInto(s_PathPartTable_Table_Name). 58 Columns({ SQLite::RowIDName, s_PathPartTable_ParentValue_Name, s_PathPartTable_PartValue_Name }). 59 Values(PathPartTable::NoPathId, std::optional<SQLite::rowid_t>{}, std::string_view{}); 60 61 builder.Execute(connection); 62 63 return connection.GetLastInsertRowID(); 64 } 65 66 // Gets the parent of a given part by id. 67 // This should only be called when the part must exist, as it will throw if not found. 68 std::optional<SQLite::rowid_t> GetParentById(SQLite::Connection& connection, SQLite::rowid_t id) 69 { 70 SQLite::Builder::StatementBuilder builder; 71 builder.Select(s_PathPartTable_ParentValue_Name).From(s_PathPartTable_Table_Name).Where(SQLite::RowIDName).Equals(id); 72 73 SQLite::Statement select = builder.Prepare(connection); 74 75 THROW_HR_IF(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED, !select.Step()); 76 77 if (!select.GetColumnIsNull(0)) 78 { 79 return select.GetColumn<SQLite::rowid_t>(0); 80 } 81 else 82 { 83 return {}; 84 } 85 } 86 87 // Determines if any part references this one as their parent. 88 bool IsLeafPart(SQLite::Connection& connection, SQLite::rowid_t id) 89 { 90 SQLite::Builder::StatementBuilder builder; 91 builder.Select(SQLite::Builder::RowCount).From(s_PathPartTable_Table_Name).Where(s_PathPartTable_ParentValue_Name).Equals(id); 92 93 SQLite::Statement select = builder.Prepare(connection); 94 95 THROW_HR_IF(E_UNEXPECTED, !select.Step()); 96 97 // No rows with this as a parent means it is a leaf. 98 return (select.GetColumn<int>(0) == 0); 99 } 100 101 // Removes the given part by id. 102 void RemovePartById(SQLite::Connection& connection, SQLite::rowid_t id) 103 { 104 SQLite::Builder::StatementBuilder builder; 105 builder.DeleteFrom(s_PathPartTable_Table_Name).Where(SQLite::RowIDName).Equals(id); 106 107 builder.Execute(connection); 108 } 109 } 110 111 // Starting in V1.1, all code should be going this route of creating named indices rather than using primary or unique keys on columns. 112 // The resulting database will function the same, but give us control to drop the indices to reduce space. 113 void PathPartTable::Create(SQLite::Connection& connection) 114 { 115 using namespace SQLite::Builder; 116 117 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createPathParts_v1_1"); 118 119 StatementBuilder createTableBuilder; 120 createTableBuilder.CreateTable(s_PathPartTable_Table_Name).Columns({ 121 IntegerPrimaryKey(), 122 ColumnBuilder(s_PathPartTable_ParentValue_Name, Type::Int64), 123 ColumnBuilder(s_PathPartTable_PartValue_Name, Type::Text).NotNull() 124 }); 125 126 createTableBuilder.Execute(connection); 127 128 StatementBuilder createPKIndexBuilder; 129 createPKIndexBuilder.CreateUniqueIndex(s_PathPartTable_PrimaryKeyIndex_Name).On(s_PathPartTable_Table_Name).Columns({ s_PathPartTable_PartValue_Name, s_PathPartTable_ParentValue_Name }); 130 createPKIndexBuilder.Execute(connection); 131 132 StatementBuilder createIndexBuilder; 133 createIndexBuilder.CreateIndex(s_PathPartTable_ParentIndex_Name).On(s_PathPartTable_Table_Name).Columns(s_PathPartTable_ParentValue_Name); 134 createIndexBuilder.Execute(connection); 135 136 savepoint.Commit(); 137 } 138 139 void PathPartTable::Create_deprecated(SQLite::Connection& connection) 140 { 141 using namespace SQLite::Builder; 142 143 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createPathParts_v1_0"); 144 145 StatementBuilder createTableBuilder; 146 createTableBuilder.CreateTable(s_PathPartTable_Table_Name).Columns({ 147 ColumnBuilder(s_PathPartTable_ParentValue_Name, Type::Int64), 148 ColumnBuilder(s_PathPartTable_PartValue_Name, Type::Text).NotNull(), 149 PrimaryKeyBuilder({ s_PathPartTable_PartValue_Name, s_PathPartTable_ParentValue_Name }) 150 }); 151 152 createTableBuilder.Execute(connection); 153 154 StatementBuilder createIndexBuilder; 155 createIndexBuilder.CreateIndex(s_PathPartTable_ParentIndex_Name).On(s_PathPartTable_Table_Name).Columns(s_PathPartTable_ParentValue_Name); 156 157 createIndexBuilder.Execute(connection); 158 159 savepoint.Commit(); 160 } 161 162 void PathPartTable::Drop(SQLite::Connection& connection) 163 { 164 SQLite::Builder::StatementBuilder dropTableBuilder; 165 dropTableBuilder.DropTable(s_PathPartTable_Table_Name); 166 167 dropTableBuilder.Execute(connection); 168 } 169 170 std::string_view PathPartTable::TableName() 171 { 172 return s_PathPartTable_Table_Name; 173 } 174 175 std::string_view PathPartTable::ValueName() 176 { 177 return s_PathPartTable_PartValue_Name; 178 } 179 180 std::tuple<bool, SQLite::rowid_t> EnsurePathExistsInternal(SQLite::Connection& connection, const std::filesystem::path& relativePath, bool createIfNotFound) 181 { 182 THROW_HR_IF(E_INVALIDARG, !relativePath.has_relative_path()); 183 THROW_HR_IF(E_INVALIDARG, relativePath.has_root_path()); 184 THROW_HR_IF(E_INVALIDARG, !relativePath.has_filename()); 185 186 std::unique_ptr<SQLite::Savepoint> savepoint; 187 if (createIfNotFound) 188 { 189 savepoint = std::make_unique<SQLite::Savepoint>(SQLite::Savepoint::Create(connection, "ensurepathexists_v1_0")); 190 } 191 192 bool partsAdded = false; 193 194 std::optional<SQLite::rowid_t> parent; 195 for (const auto& part : relativePath) 196 { 197 std::string utf8part = part.u8string(); 198 std::optional<SQLite::rowid_t> current = SelectPathPart(connection, parent, utf8part); 199 200 if (!current) 201 { 202 if (createIfNotFound) 203 { 204 partsAdded = true; 205 current = InsertPathPart(connection, parent, utf8part); 206 } 207 else 208 { 209 // Current part was not found, and we were told not to create. 210 // Return false to indicate that the path does not exist. 211 return {}; 212 } 213 } 214 215 parent = current; 216 } 217 218 if (savepoint) 219 { 220 savepoint->Commit(); 221 } 222 223 // If we get this far, the path exists. 224 // If we were asked to create it, return whether we needed to or it was already present. 225 // If not, then true indicates that it exists. 226 return { (createIfNotFound ? partsAdded : true), parent.value() }; 227 } 228 229 std::tuple<bool, SQLite::rowid_t> PathPartTable::EnsurePathExists(SQLite::Connection& connection, const std::optional<std::filesystem::path>& relativePath, bool createIfNotFound) 230 { 231 if (relativePath) 232 { 233 return EnsurePathExistsInternal(connection, relativePath.value(), createIfNotFound); 234 } 235 236 std::unique_ptr<SQLite::Savepoint> savepoint; 237 if (createIfNotFound) 238 { 239 savepoint = std::make_unique<SQLite::Savepoint>(SQLite::Savepoint::Create(connection, "ensurepathexists_v1_0")); 240 } 241 242 bool partsAdded = false; 243 244 std::optional<SQLite::rowid_t> noPathPart = SelectPathPart(connection, {}, {}); 245 246 if (!noPathPart) 247 { 248 if (createIfNotFound) 249 { 250 partsAdded = true; 251 noPathPart = InsertNoPathPart(connection); 252 } 253 else 254 { 255 // Not found, and we were told not to create. 256 // Return false to indicate that the path does not exist. 257 return {}; 258 } 259 } 260 261 if (savepoint) 262 { 263 savepoint->Commit(); 264 } 265 266 // If we get this far, the path exists. 267 // If we were asked to create it, return whether we needed to or it was already present. 268 // If not, then true indicates that it exists. 269 return { (createIfNotFound ? partsAdded : true), noPathPart.value() }; 270 } 271 272 std::optional<std::string> PathPartTable::GetPathById(const SQLite::Connection& connection, SQLite::rowid_t id) 273 { 274 SQLite::Builder::StatementBuilder builder; 275 builder.Select({ s_PathPartTable_ParentValue_Name, s_PathPartTable_PartValue_Name }). 276 From(s_PathPartTable_Table_Name).Where(SQLite::RowIDName).Equals(SQLite::Builder::Unbound); 277 278 SQLite::Statement select = builder.Prepare(connection); 279 280 SQLite::rowid_t currentPart = id; 281 std::string result; 282 283 while (true) 284 { 285 select.Reset(); 286 select.Bind(1, currentPart); 287 288 if (select.Step()) 289 { 290 std::string partValue = select.GetColumn<std::string>(1); 291 if (result.empty()) 292 { 293 result = partValue; 294 } 295 else 296 { 297 result = partValue + '/' + result; 298 } 299 300 if (select.GetColumnIsNull(0)) 301 { 302 // If the parent of this column is null, then we have reached the relative root 303 break; 304 } 305 else 306 { 307 currentPart = select.GetColumn<SQLite::rowid_t>(0); 308 } 309 } 310 else 311 { 312 if (currentPart == id) 313 { 314 // The given id did not reference an actual path 315 return {}; 316 } 317 else 318 { 319 // We found a broken path 320 AICLI_LOG(Repo, Error, << "Path part references an invalid parent: " << currentPart); 321 THROW_HR(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); 322 } 323 } 324 } 325 326 return result; 327 } 328 329 void PathPartTable::RemovePathById(SQLite::Connection& connection, SQLite::rowid_t id) 330 { 331 // Don't bother removing the pathless id 332 if (id == NoPathId) 333 { 334 return; 335 } 336 337 SQLite::rowid_t currentPartToRemove = id; 338 while (IsLeafPart(connection, currentPartToRemove)) 339 { 340 std::optional<SQLite::rowid_t> parent = GetParentById(connection, currentPartToRemove); 341 RemovePartById(connection, currentPartToRemove); 342 343 // If parent was NULL, this was a root part and we can stop 344 if (!parent) 345 { 346 break; 347 } 348 else 349 { 350 currentPartToRemove = parent.value(); 351 } 352 } 353 } 354 355 void PathPartTable::PrepareForPackaging(SQLite::Connection& connection) 356 { 357 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "pfpPathParts_v1_1"); 358 359 PrepareForPackaging_deprecated(connection); 360 361 SQLite::Builder::StatementBuilder dropPKIndexBuilder; 362 dropPKIndexBuilder.DropIndex(s_PathPartTable_PrimaryKeyIndex_Name); 363 dropPKIndexBuilder.Execute(connection); 364 365 savepoint.Commit(); 366 } 367 368 void PathPartTable::PrepareForPackaging_deprecated(SQLite::Connection& connection) 369 { 370 SQLite::Builder::StatementBuilder dropIndexBuilder; 371 dropIndexBuilder.DropIndex(s_PathPartTable_ParentIndex_Name); 372 dropIndexBuilder.Execute(connection); 373 } 374 375 bool PathPartTable::CheckConsistency(const SQLite::Connection& connection, bool log) 376 { 377 using QCol = SQLite::Builder::QualifiedColumn; 378 379 // Build a select statement to find pathpart rows containing references to parents with nonexistent rowids 380 // Such as: 381 // Select l.rowid, l.parent from pathparts as l left outer join pathparts as r on l.parent = r.rowid where l.parent is not null and r.pathpart is null 382 constexpr std::string_view s_left = "left"sv; 383 constexpr std::string_view s_right = "right"sv; 384 bool result = true; 385 386 { 387 SQLite::Builder::StatementBuilder builder; 388 builder. 389 Select({ QCol(s_left, SQLite::RowIDName), QCol(s_left, s_PathPartTable_ParentValue_Name) }). 390 From(s_PathPartTable_Table_Name).As(s_left). 391 LeftOuterJoin(s_PathPartTable_Table_Name).As(s_right).On(QCol(s_left, s_PathPartTable_ParentValue_Name), QCol(s_right, SQLite::RowIDName)). 392 Where(QCol(s_left, s_PathPartTable_ParentValue_Name)).IsNotNull().And(QCol(s_right, s_PathPartTable_PartValue_Name)).IsNull(); 393 394 SQLite::Statement select = builder.Prepare(connection); 395 396 while (select.Step()) 397 { 398 result = false; 399 400 if (!log) 401 { 402 break; 403 } 404 405 AICLI_LOG(Repo, Info, << " [INVALID] pathparts [" << select.GetColumn<SQLite::rowid_t>(0) << "] refers to " << s_PathPartTable_ParentValue_Name << " [" << select.GetColumn<SQLite::rowid_t>(1) << "]"); 406 } 407 } 408 409 if (!result && !log) 410 { 411 return result; 412 } 413 414 { 415 // Build a select statement to find values that contain an embedded null character 416 // Such as: 417 // Select count(*) from table where instr(value,char(0))>0 418 SQLite::Builder::StatementBuilder builder; 419 builder. 420 Select({ SQLite::RowIDName, s_PathPartTable_PartValue_Name }). 421 From(s_PathPartTable_Table_Name). 422 WhereValueContainsEmbeddedNullCharacter(s_PathPartTable_PartValue_Name); 423 424 SQLite::Statement select = builder.Prepare(connection); 425 426 while (select.Step()) 427 { 428 result = false; 429 430 if (!log) 431 { 432 break; 433 } 434 435 AICLI_LOG(Repo, Info, << " [INVALID] value in table [" << s_PathPartTable_Table_Name << "] at row [" << select.GetColumn<SQLite::rowid_t>(0) << "] contains an embedded null character and starts with [" << select.GetColumn<std::string>(1) << "]"); 436 } 437 } 438 439 return result; 440 } 441 442 bool PathPartTable::IsEmpty(SQLite::Connection& connection) 443 { 444 SQLite::Builder::StatementBuilder builder; 445 builder.Select(SQLite::Builder::RowCount).From(s_PathPartTable_Table_Name); 446 447 SQLite::Statement countStatement = builder.Prepare(connection); 448 449 THROW_HR_IF(E_UNEXPECTED, !countStatement.Step()); 450 451 return (countStatement.GetColumn<int>(0) == 0); 452 } 453 }