diff --git a/src/ProjectFileIO.cpp b/src/ProjectFileIO.cpp index aacde4131..8d6bfdce0 100644 --- a/src/ProjectFileIO.cpp +++ b/src/ProjectFileIO.cpp @@ -501,28 +501,26 @@ int ProjectFileIO::Exec(const char *query, ExecCB callback, wxString *result) return rc; } -wxString ProjectFileIO::GetValue(const char *sql) +bool ProjectFileIO::GetValue(const char *sql, wxString &result) { - auto getresult = [&](wxString *result, int cols, char **vals, char **names) + auto getresult = [](wxString *result, int cols, char **vals, char **names) { if (cols == 1 && vals[0]) { - result->append(vals[0]); + result->assign(vals[0]); return SQLITE_OK; } return SQLITE_ABORT; }; - wxString value; - - int rc = Exec(sql, getresult, &value); + int rc = Exec(sql, getresult, &result); if (rc != SQLITE_OK) { - // Message already captured + return false; } - return value; + return true; } bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer) @@ -549,6 +547,13 @@ bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer) } rc = sqlite3_step(stmt); + + // A row wasn't found...not an error + if (rc == SQLITE_DONE) + { + return true; + } + if (rc != SQLITE_ROW) { SetDBError( @@ -561,6 +566,7 @@ bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer) const void *blob = sqlite3_column_blob(stmt, 0); int size = sqlite3_column_bytes(stmt, 0); + buffer.Clear(); buffer.AppendData(blob, size); return true; @@ -572,43 +578,40 @@ bool ProjectFileIO::CheckVersion() int rc; // Install our schema if this is an empty DB - long count = -1; - GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';").ToLong(&count); - if (count == -1) + wxString result; + if (!GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';", result)) { return false; } // If the return count is zero, then there are no tables defined, so this // must be a new project file. - if (count == 0) + if (wxStrtol(result, nullptr, 10) == 0) { return InstallSchema(); } // Check for our application ID - long appid = -1; - GetValue("PRAGMA application_ID;").ToLong(&appid); - if (appid == -1) + if (!GetValue("PRAGMA application_ID;", result)) { return false; } // It's a database that SQLite recognizes, but it's not one of ours - if (appid != ProjectFileID) + if (wxStrtoul(result, nullptr, 10) != ProjectFileID) { SetError(XO("This is not an Audacity project file")); return false; } // Get the project file version - long version = -1; - GetValue("PRAGMA user_version;").ToLong(&version); - if (version == -1) + if (!GetValue("PRAGMA user_version;", result)) { return false; } + long version = wxStrtol(result, nullptr, 10); + // Project file version is higher than ours. We will refuse to // process it since we can't trust anything about it. if (version > ProjectFileVersion) @@ -658,6 +661,53 @@ bool ProjectFileIO::UpgradeSchema() return true; } +void ProjectFileIO::LoadedBlock(int64_t sbid) +{ + if (sbid > mHighestBlockID) + { + mHighestBlockID = sbid; + } +} + +bool ProjectFileIO::CheckForOrphans() +{ + auto db = DB(); + + char sql[256]; + sqlite3_snprintf(sizeof(sql), + sql, + "DELETE FROM sampleblocks WHERE blockid > %lld", + mHighestBlockID); + + char *errmsg = nullptr; + + int rc = sqlite3_exec(db, sql, nullptr, nullptr, &errmsg); + + if (errmsg) + { + wxLogDebug(wxT("Failed to delete orphaned blocks: %s"), errmsg); + sqlite3_free(errmsg); + } + + if (rc != SQLITE_OK) + { + SetDBError( + XO("Failed to delete orphaned blocks") + ); + + return false; + } + + int changes = sqlite3_changes(db); + if (changes > 0) + { + wxLogInfo(XO("Total orphan blocks deleted %d").Translation(), changes); + mRecovered = true; + } + + return true; +} + sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath) { auto db = DB(); @@ -1150,15 +1200,22 @@ bool ProjectFileIO::LoadProject(const FilePath &fileName) // Get the autosave doc, if any bool wasAutosave = false; wxMemoryBuffer buffer; - if (GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer)) + if (!GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer)) + { + // Error already set + return false; + } + + if (buffer.GetDataLen() > 0) { doc = AutoSaveFile::Decode(buffer); wasAutosave = true; } // Otherwise, get the project doc - else + else if (!GetValue("SELECT doc FROM project;", doc)) { - doc = GetValue("SELECT doc FROM project;"); + // Error already set + return false; } if (doc.empty()) @@ -1168,6 +1225,9 @@ bool ProjectFileIO::LoadProject(const FilePath &fileName) XMLFileReader xmlFile; + // Reset the highest blockid + mHighestBlockID = 0; + success = xmlFile.ParseString(this, doc); if (!success) { @@ -1180,6 +1240,16 @@ bool ProjectFileIO::LoadProject(const FilePath &fileName) // Remember if it was recovered or not mRecovered = wasAutosave; + + // Check for orphans blocks...set mRecovered if any deleted + if (mHighestBlockID > 0) + { + if (!CheckForOrphans()) + { + return false; + } + } + if (mRecovered) { mModified = true; @@ -1188,9 +1258,13 @@ bool ProjectFileIO::LoadProject(const FilePath &fileName) // A previously saved project will have a document in the project table, so // we use that knowledge to determine if this file is an unsaved/temporary // file or not - long count = 0; - GetValue("SELECT Count(*) FROM project;").ToLong(&count); - mTemporary = (count != 1); + wxString result; + if (!GetValue("SELECT Count(*) FROM project;", result)) + { + return false; + } + + mTemporary = (wxStrtol(result, nullptr, 10) != 1); SetFileName(fileName); diff --git a/src/ProjectFileIO.h b/src/ProjectFileIO.h index 16f2772c1..92e0ebc1a 100644 --- a/src/ProjectFileIO.h +++ b/src/ProjectFileIO.h @@ -98,6 +98,8 @@ public: void Bypass(bool bypass); bool ShouldBypass(); + void LoadedBlock(int64_t blockID); + private: // XMLTagHandler callback methods bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; @@ -139,13 +141,16 @@ private: bool TransactionCommit(const wxString &name); bool TransactionRollback(const wxString &name); - wxString GetValue(const char *sql); + bool GetValue(const char *sql, wxString &value); bool GetBlob(const char *sql, wxMemoryBuffer &buffer); bool CheckVersion(); bool InstallSchema(); bool UpgradeSchema(); + // Checks for orphan blocks. This will go away at a future date + bool CheckForOrphans(); + // Return a database connection if successful, which caller must close sqlite3 *CopyTo(const FilePath &destpath); @@ -177,6 +182,9 @@ private: TranslatableString mLastError; TranslatableString mLibraryError; + // Track the highest blockid while loading a project + int64_t mHighestBlockID; + friend SqliteSampleBlock; }; diff --git a/src/SqliteSampleBlock.cpp b/src/SqliteSampleBlock.cpp index e28272c60..04b4e0e46 100644 --- a/src/SqliteSampleBlock.cpp +++ b/src/SqliteSampleBlock.cpp @@ -60,7 +60,7 @@ public: void SaveXML(XMLWriter &xmlFile) override; private: - bool Load(SampleBlockID sbid); + void Load(SampleBlockID sbid); bool GetSummary(float *dest, size_t frameoffset, size_t numframes, @@ -186,6 +186,11 @@ SampleBlockPtr SqliteSampleBlockFactory::DoCreateFromXML( { // This may throw sb->Load((SampleBlockID) nValue); + + // Tell the IO manager the blockid we just loaded so it can track + // the highest one encountered. + mpIO->LoadedBlock(nValue); + found++; } else if (wxStrcmp(attr, wxT("samplecount")) == 0) @@ -518,7 +523,7 @@ size_t SqliteSampleBlock::GetBlob(void *dest, return srcbytes; } -bool SqliteSampleBlock::Load(SampleBlockID sbid) +void SqliteSampleBlock::Load(SampleBlockID sbid) { auto db = mIO.DB();