Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

bug: memory access out of bounds on web platform #611

Open
FabianTauriello opened this issue Jan 2, 2025 · 3 comments
Open

bug: memory access out of bounds on web platform #611

FabianTauriello opened this issue Jan 2, 2025 · 3 comments
Labels
bug/fix Something isn't working needs: triage

Comments

@FabianTauriello
Copy link

Plugin version:
6.0.2

Platform(s):
Web

Current behavior:
I think the bug occurs when I call the .open() function on a SQLiteDBConnection.

Expected behavior:
No error.

Steps to reproduce:

  • Install necessary dependencies.
  • Create a db connection
  • Open the connection

Related code:

Here is a class I use for managing the database.

import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection, DBSQLiteValues } from "@capacitor-community/sqlite";
import { SqlStatements } from "../lib/sqlStatements";
import { Utils } from "../lib/utils";
import { AppContent } from "../lib/types";
import {
  TABLE_BPJ_JOURNAL,
  TABLE_CPD,
  TABLE_EVENTS,
  TABLE_HOME_SLIDES,
  TABLE_MEMBER_BENEFITS,
  TABLE_MEMBER_DISCOUNTS,
  TABLE_MISC,
  TABLE_NEWS,
  TABLE_PI_JOURNAL,
  TABLE_POLLS,
} from "../lib/constants";
import { AppContentSchema } from "../lib/typeSchemas";
import { PreferencesService } from "./preferencesService";

const DATABASE_NAME = "cop_db";
const DATABASE_READ_ONLY = false;
const DATABASE_LATEST_SCHEMA_VERSION = 2;

class LocalDatabaseService {
  #sqliteConnection = new SQLiteConnection(CapacitorSQLite);

  async initDatabase(savedDatabaseSchemaVersion: number) {
    try {
      // Perform initial table creation IF the tables don't already exist
      const dbConnection = await this.open();
      await dbConnection.execute(SqlStatements.setupTables);

      await this.updateDatabaseSchema(dbConnection, savedDatabaseSchemaVersion);
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    } finally {
      await this.close();
    }
  }

  // update database schema if required
  async updateDatabaseSchema(dbConnection: SQLiteDBConnection, savedDatabaseSchemaVersion: number): Promise<void> {
    if (savedDatabaseSchemaVersion < DATABASE_LATEST_SCHEMA_VERSION) {
      await dbConnection.execute("ALTER TABLE news_articles ADD COLUMN appExclusive INTEGER;");
      // add upgrade expressions here...

      await PreferencesService.saveDatabaseSchemaVersion(DATABASE_LATEST_SCHEMA_VERSION);
    }
  }

  async open(): Promise<SQLiteDBConnection> {
    try {
      let dbConnection: SQLiteDBConnection;
      const connectionConsistency = (await this.#sqliteConnection.checkConnectionsConsistency()).result; // have no idea what "connection consistency" is but the library's example souce code had it so here we are
      const isConnected = (await this.#sqliteConnection.isConnection(DATABASE_NAME, DATABASE_READ_ONLY)).result;

      // Check if already connected just in case it wasn't closed properly
      if (connectionConsistency && isConnected) {
        console.log("retrieving existing db connection...");
        dbConnection = await this.#sqliteConnection.retrieveConnection(DATABASE_NAME, DATABASE_READ_ONLY);
      } else {
        console.log("creating new db connection...");
        dbConnection = await this.#sqliteConnection.createConnection(
          DATABASE_NAME,
          false,
          "no-encryption",
          1,
          DATABASE_READ_ONLY
        );
      }

      await dbConnection.open();

      return dbConnection;
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    }
  }

  async close(): Promise<void> {
    try {
      const isConnected = (await this.#sqliteConnection.isConnection(DATABASE_NAME, DATABASE_READ_ONLY)).result;
      if (isConnected) {
        console.log("closing db connection...");
        await this.#sqliteConnection.closeConnection(DATABASE_NAME, DATABASE_READ_ONLY);
      }
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    }
  }

  async updateAllTables(appContent: AppContent): Promise<void> {
    try {
      const dbConnection = await this.open();

      await this.updateTable(dbConnection, TABLE_EVENTS, appContent.events);
      await this.updateTable(dbConnection, TABLE_CPD, appContent.cpdVideos);
      await this.updateTable(dbConnection, TABLE_HOME_SLIDES, appContent.homeCarouselSlides);
      await this.updateTable(dbConnection, TABLE_NEWS, appContent.newsArticles);
      await this.updateTable(dbConnection, TABLE_MEMBER_BENEFITS, appContent.memberBenefits);
      await this.updateTable(dbConnection, TABLE_MEMBER_DISCOUNTS, appContent.memberDiscounts);
      await this.updateTable(dbConnection, TABLE_BPJ_JOURNAL, appContent.paramedicInsightEntries);
      await this.updateTable(dbConnection, TABLE_PI_JOURNAL, appContent.bpjEntries);
      await this.updateTable(dbConnection, TABLE_MISC, appContent.miscellaneousItems);
      await this.updateTable(dbConnection, TABLE_POLLS, appContent.polls);
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    } finally {
      await this.close();
    }
  }

  async dropTables(): Promise<void> {
    try {
      const dbConnection = await this.open();
      await dbConnection.query(`DROP TABLE ${TABLE_EVENTS};`);
      await dbConnection.query(`DROP TABLE ${TABLE_CPD};`);
      await dbConnection.query(`DROP TABLE ${TABLE_HOME_SLIDES};`);
      await dbConnection.query(`DROP TABLE ${TABLE_NEWS};`);
      await dbConnection.query(`DROP TABLE ${TABLE_MEMBER_BENEFITS};`);
      await dbConnection.query(`DROP TABLE ${TABLE_MEMBER_DISCOUNTS};`);
      await dbConnection.query(`DROP TABLE ${TABLE_BPJ_JOURNAL};`);
      await dbConnection.query(`DROP TABLE ${TABLE_PI_JOURNAL};`);
      await dbConnection.query(`DROP TABLE ${TABLE_MISC};`);
      await dbConnection.query(`DROP TABLE ${TABLE_POLLS};`);
    } catch (error) {
      console.log("Failed to drop tables", error);
    } finally {
      await this.close();
    }
  }

  async updateTable(dbConnection: SQLiteDBConnection, tableName: string, items: any[]): Promise<void> {
    try {
      for (const item of items) {
        const keys = Object.keys(item);
        const values = Object.values(item).map((it) => {
          if (it === "" || it === undefined || it === null) {
            // This value should only be set for non-boolean types (e.g. if a description field is not set in Airtable),
            // because all booleans are given values in the mapServerDataToCustomTypes() function.
            // In the local SQLite database, boolean types use INTEGER, and everything else uses TEXT, so this should be fine
            return "";
          }

          if (typeof it === "object") {
            return JSON.stringify(it);
          }

          return it;
        });

        // Construct the INSERT statement dynamically based on the keys and values
        const insertQuery = `INSERT OR REPLACE INTO ${tableName} (${keys.join(", ")}) VALUES (${keys
          .map(() => "?")
          .join(", ")});`;

        await dbConnection.query(insertQuery, values);
      }

      // await Utils.delay(2000);
      // throw "custom error from updateDatabase";
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    }
  }

  async query(sql: string, values: string[]): Promise<DBSQLiteValues> {
    try {
      const dbConnection = await this.open();
      return await dbConnection.query(sql, values);
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    } finally {
      await this.close();
    }
  }

  async updateBookmarkStatusForVideo(newValue: boolean, recordId: string): Promise<void> {
    try {
      const dbConnection = await this.open();
      const statement = `UPDATE ${TABLE_CPD} SET isBookmarked = ? WHERE id = ?;`;
      await dbConnection.query(statement, [newValue, recordId]);
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    } finally {
      await this.close();
    }
  }

  /**
   * Retrieves all App Content records from the local database, checks that results are not empty, and that they are mapped to the correct types.
   * @returns
   */
  async getAppContent(): Promise<AppContent> {
    try {
      const dbConnection = await this.open();
      const events = (await dbConnection.query(`SELECT * FROM ${TABLE_EVENTS};`, [])).values ?? [];
      const cpdVideos = (await dbConnection.query(`SELECT * FROM ${TABLE_CPD};`, [])).values ?? [];
      const homeCarouselSlides = (await dbConnection.query(`SELECT * FROM ${TABLE_HOME_SLIDES};`, [])).values ?? [];
      const newsArticles = (await dbConnection.query(`SELECT * FROM ${TABLE_NEWS};`, [])).values ?? [];
      const memberBenefits = (await dbConnection.query(`SELECT * FROM ${TABLE_MEMBER_BENEFITS};`, [])).values ?? [];
      const memberDiscounts = (await dbConnection.query(`SELECT * FROM ${TABLE_MEMBER_DISCOUNTS};`, [])).values ?? [];
      const paramedicInsightEntries = (await dbConnection.query(`SELECT * FROM ${TABLE_PI_JOURNAL};`, [])).values ?? [];
      const bpjEntries = (await dbConnection.query(`SELECT * FROM ${TABLE_BPJ_JOURNAL};`, [])).values ?? [];
      const miscellaneousItems = (await dbConnection.query(`SELECT * FROM ${TABLE_MISC};`, [])).values ?? [];
      const polls = (await dbConnection.query(`SELECT * FROM ${TABLE_POLLS};`, [])).values ?? [];

      // Parse data, converting JSON values and boolean values to the correct types
      events.forEach((e) => {
        e.isRegistered = e.isRegistered === 1;
      });
      cpdVideos.forEach((vid) => {
        vid.presenters = JSON.parse(vid.presenters);
        vid.smallScreenshot = JSON.parse(vid.smallScreenshot);
        vid.largeScreenshot = JSON.parse(vid.largeScreenshot);
        vid.isBookmarked = vid.isRegistered === 1;
        vid.isFeatured = vid.isRegistered === 1;
        vid.isSample = vid.isRegistered === 1;
        vid.seen = vid.seen === 1;
      });
      homeCarouselSlides.forEach((slide) => {
        slide.active = slide.active === 1;
        slide.guestOnly = slide.guestOnly === 1;
      });
      polls.forEach((poll) => {
        poll.question = JSON.parse(poll.question);
        poll.responses = JSON.parse(poll.responses);
        poll.active = poll.active === 1;
      });

      const appContent = {
        events,
        cpdVideos,
        homeCarouselSlides,
        newsArticles,
        memberBenefits,
        memberDiscounts,
        paramedicInsightEntries,
        bpjEntries,
        miscellaneousItems,
        polls,
      };

      // Validate data against schema. Will throw an error if validation fails
      AppContentSchema.parse(appContent);

      return appContent;
    } catch (error) {
      throw Utils.extractErrorMessage(error);
    } finally {
      await this.close();
    }
  }
}

// Export the only instance of LocalDatabaseService, acting as a 'singleton' for the app.
export default new LocalDatabaseService();

Other information:
This package was working fine on web until I recently updated Capacitor to v6.

Capacitor doctor:

💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 6.2.0
  @capacitor/core: 6.2.0
  @capacitor/android: 6.2.0
  @capacitor/ios: 6.2.0

Installed Dependencies:

  @capacitor/cli: 6.2.0
  @capacitor/android: 6.2.0
  @capacitor/core: 6.2.0
  @capacitor/ios: 6.2.0

[success] iOS looking great! 👌
[success] Android looking great! 👌
@FabianTauriello FabianTauriello added bug/fix Something isn't working needs: triage labels Jan 2, 2025
@FabianTauriello
Copy link
Author

FabianTauriello commented Jan 2, 2025

Oops I forgot to include the error message from the console:

Uncaught (in promise) RuntimeError: memory access out of bounds
    at sql-wasm.wasm:0x7d517
    at f2._sqlite3_open (jeep-sqlite_dist_components_jeep-sqlite.js?v=ee19357e:4256:63)
    at Sc (jeep-sqlite_dist_components_jeep-sqlite.js?v=ee19357e:3580:14)
    at jeep-sqlite_dist_components_jeep-sqlite.js?v=ee19357e:4265:73
    at new e2 (jeep-sqlite_dist_components_jeep-sqlite.js?v=ee19357e:2119:28)
    at jeep-sqlite_dist_components_jeep-sqlite.js?v=ee19357e:6907:24

@deokudev
Copy link

deokudev commented Feb 6, 2025

Execute the following command(if you use angular):
copyfiles -u 3 node_modules/sql.js/dist/sql-wasm.wasm src/assets

@FabianTauriello
Copy link
Author

Execute the following command(if you use angular): copyfiles -u 3 node_modules/sql.js/dist/sql-wasm.wasm src/assets

I'm using React :(

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
bug/fix Something isn't working needs: triage
Projects
None yet
Development

No branches or pull requests

2 participants