diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..89e95af7f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[env] +RSLIB_FTL_ROOT = { value = "ftl/core/l10n.toml", relative = true } +BUILDINFO = { value = "rslib-bridge/buildinfo.txt", relative = true } +BAZEL = "1" diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index 094d247a2..0ee8baf07 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -15,60 +15,64 @@ jobs: timeout-minutes: 80 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - name: Fetch submodules + run: git submodule update --init --recursive - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - # COULD_BE_BETTER: Consider turning this into a GitHub action - help the wider community - # NDK install (unzipping) is really noisy - silence the log spam with grep, while keeping errors - - name: Install NDK (silent) - run: .github/scripts/install_ndk.sh 22.0.7026061 + # COULD_BE_BETTER: Consider turning this into a GitHub action - help the wider community + # NDK install (unzipping) is really noisy - silence the log spam with grep, while keeping errors + - name: Install NDK (silent) + run: .github/scripts/install_ndk.sh 22.0.7026061 - - name: Install linker - run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + - name: Install linker + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Install Python libs + run: | + pip3 install --upgrade protobuf stringcase - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain # assembleAndroidTest + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 - # Our publish workflow (publish_library.yaml) is on Ubuntu and needs javadocs (#57) - - name: Test Javadoc - run: ./gradlew :rsdroid:androidJavadocs -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain # assembleAndroidTest + + # Our publish workflow (publish_library.yaml) is on Ubuntu and needs javadocs (#57) + - name: Test Javadoc + run: ./gradlew :rsdroid:androidJavadocs -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index c565fb0e6..4093b4f52 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -16,72 +16,71 @@ jobs: runs-on: macos-latest timeout-minutes: 80 steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Test NDK - run: echo "NDK set to $ANDROID_NDK_HOME" - - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install & Test Protobuf Compiler - run: | - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleDebug-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleDebug - ${{ runner.os }}-rust-v1 - - - name: Build - run: ./gradlew clean assembleDebug -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Build Instrumented Test APKs - run: ./gradlew rsdroid-instrumented:assembleDebug rsdroid-instrumented:assembleAndroidTest -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Upload APKs as Artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-instrumented - if-no-files-found: error - path: rsdroid-instrumented/build/outputs/apk - + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Test NDK + run: echo "NDK set to $ANDROID_NDK_HOME" + + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install & Test Protobuf Compiler + run: | + pip3 install --upgrade protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleDebug-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleDebug + ${{ runner.os }}-rust-v1 + + - name: Build + run: ./gradlew clean assembleDebug -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Build Instrumented Test APKs + run: ./gradlew rsdroid-instrumented:assembleDebug rsdroid-instrumented:assembleAndroidTest -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Upload APKs as Artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-instrumented + if-no-files-found: error + path: rsdroid-instrumented/build/outputs/apk test: needs: build @@ -93,37 +92,37 @@ jobs: api-level: [21] arch: [x86, x86_64] # arm and arm64 are not supported by reactivecircus/android-emulator-runner steps: - - uses: actions/checkout@v2 - name: Checkout - - # COULD_BE_BETTER: This may not be needed - tiny speed penalty - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Download APKs - uses: actions/download-artifact@v2 - with: - name: rsdroid-instrumented - path: rsdroid-instrumented/build/outputs/apk - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: run tests - uses: reactivecircus/android-emulator-runner@v2 - timeout-minutes: 30 - with: - api-level: ${{ matrix.api-level }} - target: default - arch: ${{ matrix.arch }} - profile: Nexus 6 - script: ./gradlew rsdroid-instrumented:connectedCheck -x rsdroid-instrumented:packageDebugAndroidTest -x rsdroid-instrumented:packageDebug + - uses: actions/checkout@v2 + name: Checkout + + # COULD_BE_BETTER: This may not be needed - tiny speed penalty + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Download APKs + uses: actions/download-artifact@v2 + with: + name: rsdroid-instrumented + path: rsdroid-instrumented/build/outputs/apk + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 30 + with: + api-level: ${{ matrix.api-level }} + target: default + arch: ${{ matrix.arch }} + profile: Nexus 6 + script: ./gradlew rsdroid-instrumented:connectedCheck -x rsdroid-instrumented:packageDebugAndroidTest -x rsdroid-instrumented:packageDebug diff --git a/.github/workflows/publish_library.yml b/.github/workflows/publish_library.yml index 74fbd5f73..63bfaf1f7 100644 --- a/.github/workflows/publish_library.yml +++ b/.github/workflows/publish_library.yml @@ -9,54 +9,58 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Install linker - run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Publish AAR to Maven - env: - ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SONATYPE_NEXUS_USERNAME: david-allison-1 - SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: | - ./gradlew rsdroid:uploadArchives -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: ℹ️Additional Release Instructions (requires human interaction) - run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" \ No newline at end of file + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Install linker + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Python libs + run: | + pip3 install --upgrade protobuf stringcase + + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Publish AAR to Maven + env: + ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_NEXUS_USERNAME: david-allison-1 + SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + run: | + ./gradlew rsdroid:uploadArchives -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: ℹ️Additional Release Instructions (requires human interaction) + run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 40f6b2b3f..721f1b2d0 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -10,80 +10,84 @@ jobs: publish: runs-on: macos-latest steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_robolectric_targets.sh - - - name: Install x86_64-w64-mingw32-gcc - run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v - - - name: Install x86_64-unknown-linux-gnu - run: | - brew tap SergioBenitez/osxct - brew install x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu-gcc -v - - - name: Build JAR - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Check Compiled Libraries - run: > - cd rsdroid-testing/assets && - ../../.github/scripts/check_robolectric_assets.sh - - - name: Upload rsdroid-testing JAR as artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-testing - if-no-files-found: error - path: rsdroid-testing/build/libs - - - name: Publish JAR to Maven - env: - ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SONATYPE_NEXUS_USERNAME: david-allison-1 - SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew rsdroid-testing:uploadArchives -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: ℹ️Additional Release Instructions (requires human interaction) - run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" \ No newline at end of file + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + - name: Install Python libs + run: | + pip3 install --upgrade protobuf stringcase + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_robolectric_targets.sh + + - name: Install x86_64-w64-mingw32-gcc + run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + + - name: Install x86_64-unknown-linux-gnu + run: | + brew tap SergioBenitez/osxct + brew install x86_64-unknown-linux-gnu + x86_64-unknown-linux-gnu-gcc -v + + - name: Build JAR + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Check Compiled Libraries + run: > + cd rsdroid-testing/assets && + ../../.github/scripts/check_robolectric_assets.sh + + - name: Upload rsdroid-testing JAR as artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-testing + if-no-files-found: error + path: rsdroid-testing/build/libs + + - name: Publish JAR to Maven + env: + ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_NEXUS_USERNAME: david-allison-1 + SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew rsdroid-testing:uploadArchives -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: ℹ️Additional Release Instructions (requires human interaction) + run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 2602061bf..db218d373 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -15,105 +15,97 @@ jobs: runs-on: macos-latest timeout-minutes: 80 steps: -# - name: Configure Mac OS environment variables - # gnu tar for cache issue: https://github.com/actions/cache/issues/403 - # error[E0463]: can't find crate for `serde_derive` which `serde` depends on - # --> /Users/runner/work/Anki-Android-Backend/Anki-Android-Backend/anki/rslib/src/decks/schema11.rs:30:9 - - # ::add-path has now been deprectated - # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" - -# run: | -# echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK (r22 - 22.0.7026061) - run: .github/scripts/install_ndk.sh 22.0.7026061 - -# TODO: Needs investigation. This seemed to work before adding gnubin/v1, just with serde broken. -# Now it doesn't seem to be picked up by rust. -# - name: Cache Rust dependencies -# uses: actions/cache@v1.0.1 -# with: -# path: rslib-bridge/target -# key: ${{ runner.OS }}-build-v1-${{ hashFiles('**/Cargo.lock') }} -# restore-keys: | -# ${{ runner.OS }}-build-v1- - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_robolectric_targets.sh - - - name: Install x86_64-w64-mingw32-gcc - run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v - - - name: Install x86_64-unknown-linux-gnu - run: | - brew tap SergioBenitez/osxct - brew install x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu-gcc -v - - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-rsdroid-testing:build-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-rsdroid-testing:build - ${{ runner.os }}-rust-v1 - - - name: Build JAR - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain --warning-mode all - - - name: Check Compiled Libraries - run: > - cd rsdroid-testing/assets && - ../../.github/scripts/check_robolectric_assets.sh - - - - name: Upload rsdroid-testing JAR as artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-testing - if-no-files-found: error - path: rsdroid-testing/build/libs/ - - - name: Upload fluent.proto - uses: actions/upload-artifact@v2 - with: - name: anki-proto - if-no-files-found: error - path: rslib-bridge/anki/proto/ + # - name: Configure Mac OS environment variables + # gnu tar for cache issue: https://github.com/actions/cache/issues/403 + # error[E0463]: can't find crate for `serde_derive` which `serde` depends on + # --> /Users/runner/work/Anki-Android-Backend/Anki-Android-Backend/anki/rslib/src/decks/schema11.rs:30:9 + + # ::add-path has now been deprectated + # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" + + # run: | + # echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK (r22 - 22.0.7026061) + run: .github/scripts/install_ndk.sh 22.0.7026061 + + # TODO: Needs investigation. This seemed to work before adding gnubin/v1, just with serde broken. + # Now it doesn't seem to be picked up by rust. + # - name: Cache Rust dependencies + # uses: actions/cache@v1.0.1 + # with: + # path: rslib-bridge/target + # key: ${{ runner.OS }}-build-v1-${{ hashFiles('**/Cargo.lock') }} + # restore-keys: | + # ${{ runner.OS }}-build-v1- + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_robolectric_targets.sh + + - name: Install x86_64-w64-mingw32-gcc + run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + + - name: Install x86_64-unknown-linux-gnu + run: | + brew tap SergioBenitez/osxct + brew install x86_64-unknown-linux-gnu + x86_64-unknown-linux-gnu-gcc -v + + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-rsdroid-testing:build-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-rsdroid-testing:build + ${{ runner.os }}-rust-v1 + + - name: Build JAR + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain --warning-mode all + + - name: Check Compiled Libraries + run: > + cd rsdroid-testing/assets && + ../../.github/scripts/check_robolectric_assets.sh + + - name: Upload rsdroid-testing JAR as artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-testing + if-no-files-found: error + path: rsdroid-testing/build/libs/ test: needs: build @@ -124,77 +116,67 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: false steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - uses: actions/download-artifact@v2 - name: Download Artifact - with: - name: rsdroid-testing - path: rsdroid-testing/build/libs - - - uses: actions/download-artifact@v2 - name: Download fluent.proto - with: - name: anki-proto - path: rslib-bridge/anki/proto/ - - - name: Ensure fluent.proto exists - if: matrix.os != 'windows-latest' - run: | - if [ ! -f rslib-bridge/anki/proto/fluent.proto ]; then - echo "fluent.proto not generated" - exit 1 - fi - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK (silent) - if: matrix.os != 'windows-latest' - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Install NDK (Windows - silent) - if: matrix.os == 'windows-latest' - run: | - Write-Host "NDK Install Started" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" - - - name: Install Python Setuptools - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install python3-setuptools - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install & Test Protobuf Compiler - if: matrix.os != 'windows-latest' - run: | - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Install & Test Protobuf Compiler (win) - if: matrix.os == 'windows-latest' - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Run Tests - if: matrix.os != 'windows-latest' - run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 - - - name: Run Tests (win) - if: matrix.os == 'windows-latest' - run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - uses: actions/download-artifact@v2 + name: Download Artifact + with: + name: rsdroid-testing + path: rsdroid-testing/build/libs + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK (silent) + if: matrix.os != 'windows-latest' + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Install NDK (Windows - silent) + if: matrix.os == 'windows-latest' + run: | + Write-Host "NDK Install Started" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" + + - name: Install linker + if: matrix.os == 'ubuntu-latest' + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + + - name: Install Python Setuptools + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install python3-setuptools + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install & Test Protobuf Compiler + if: matrix.os != 'windows-latest' + run: | + pip3 install --upgrade protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Install & Test Protobuf Compiler (win) + if: matrix.os == 'windows-latest' + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install --upgrade protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Run Tests + if: matrix.os != 'windows-latest' + run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 + + - name: Run Tests (win) + if: matrix.os == 'windows-latest' + run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 97be6f767..57d79c063 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -15,96 +15,95 @@ jobs: timeout-minutes: 80 runs-on: windows-latest steps: + - uses: actions/checkout@v2 - - uses: actions/checkout@v2 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + # TODO: Ignore lines like we do in the Unix scripts, rather than all of them + - name: Install NDK (silent) + run: | + Write-Host "NDK Install Started" + Write-Host "ANDROID_HOME - $Env:ANDROID_HOME" + Write-Host "ANDROID_SDK_ROOT - $Env:ANDROID_SDK_ROOT" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" - # TODO: Ignore lines like we do in the Unix scripts, rather than all of them - - name: Install NDK (silent) - run: | - Write-Host "NDK Install Started" - Write-Host "ANDROID_HOME - $Env:ANDROID_HOME" - Write-Host "ANDROID_SDK_ROOT - $Env:ANDROID_SDK_ROOT" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" + # bzip2-sys does not build when the value of CC_armv7-linux-androideabi contains a space + # Setting this variable is a pain, as it contains a dash + # It's easier to just move the directory and fix the environment variables to point to the new NDK + # we change Program Files (x86) to "ProgramFiles" + # TODO: Move the setting of environment variables here (permanently) + - name: Move SDK to directory with no spaces + run: | + New-Item -ItemType directory -Path 'C:\ProgramFiles\Android' + Get-ChildItem "$Env:ANDROID_SDK_ROOT" + Move-Item -Path "$Env:ANDROID_SDK_ROOT" -Destination 'C:\ProgramFiles\Android' -ErrorAction Stop -Force + Get-ChildItem 'C:\ProgramFiles\Android\android-sdk' - # bzip2-sys does not build when the value of CC_armv7-linux-androideabi contains a space - # Setting this variable is a pain, as it contains a dash - # It's easier to just move the directory and fix the environment variables to point to the new NDK - # we change Program Files (x86) to "ProgramFiles" - # TODO: Move the setting of environment variables here (permanently) - - name: Move SDK to directory with no spaces - run: | - New-Item -ItemType directory -Path 'C:\ProgramFiles\Android' - Get-ChildItem "$Env:ANDROID_SDK_ROOT" - Move-Item -Path "$Env:ANDROID_SDK_ROOT" -Destination 'C:\ProgramFiles\Android' -ErrorAction Stop -Force - Get-ChildItem 'C:\ProgramFiles\Android\android-sdk' + - name: Debug Env + run: | + $Env:ANDROID_HOME - - name: Debug Env - run: | - $Env:ANDROID_HOME - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - name: Fetch submodules + run: git submodule update --init --recursive - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. - - name: Install Rust Targets - run: | - rustup target add armv7-linux-androideabi - rustup target add i686-linux-android - rustup target add aarch64-linux-android - rustup target add x86_64-linux-android + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. + - name: Install Rust Targets + run: | + rustup target add armv7-linux-androideabi + rustup target add i686-linux-android + rustup target add aarch64-linux-android + rustup target add x86_64-linux-android - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install & Test Protobuf - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py + - name: Install & Test Protobuf + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install --upgrade protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 - # -Dorg.gradle.daemon=false has been removed - # -Dorg.gradle.console=plain has been removed - # env: - # ANDROID_HOME: "C:\\ProgramFiles\\Android\\android-sdk" - # ANDROID_NDK_HOME: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" - # ANDROID_NDK_PATH: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" - # ANDROID_SDK_ROOT: "C:\\ProgramFiles\\Android\\android-sdk" - - name: Build - run: | - $Env:ANDROID_HOME = "C:\ProgramFiles\Android\android-sdk" - $Env:ANDROID_NDK_HOME = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" - $Env:ANDROID_NDK_PATH = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" - $Env:ANDROID_SDK_ROOT = "C:\ProgramFiles\Android\android-sdk" - $env:ANDROID_HOME - ./gradlew clean assembleRelease -DtestBuildType=release # assembleAndroidTest + # -Dorg.gradle.daemon=false has been removed + # -Dorg.gradle.console=plain has been removed + # env: + # ANDROID_HOME: "C:\\ProgramFiles\\Android\\android-sdk" + # ANDROID_NDK_HOME: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" + # ANDROID_NDK_PATH: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" + # ANDROID_SDK_ROOT: "C:\\ProgramFiles\\Android\\android-sdk" + - name: Build + run: | + $Env:ANDROID_HOME = "C:\ProgramFiles\Android\android-sdk" + $Env:ANDROID_NDK_HOME = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" + $Env:ANDROID_NDK_PATH = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" + $Env:ANDROID_SDK_ROOT = "C:\ProgramFiles\Android\android-sdk" + $env:ANDROID_HOME + ./gradlew clean assembleRelease -DtestBuildType=release # assembleAndroidTest diff --git a/.github/workflows/windows_pure_build.yml b/.github/workflows/windows_pure_build.yml index c85bdb1e3..56b08048e 100644 --- a/.github/workflows/windows_pure_build.yml +++ b/.github/workflows/windows_pure_build.yml @@ -11,61 +11,60 @@ jobs: build: runs-on: windows-latest steps: - - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - uses: actions/checkout@v2 - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Fetch submodules + run: git submodule update --init --recursive - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Install NDK (Windows - silent) - if: matrix.os == 'windows-latest' - run: | - Write-Host "NDK Install Started" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.54.0 - override: true - components: rustfmt + - name: Install NDK (Windows - silent) + if: matrix.os == 'windows-latest' + run: | + Write-Host "NDK Install Started" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh - - name: Install & Test Protobuf - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Install & Test Protobuf + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install --upgrade protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release \ No newline at end of file + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 + + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release diff --git a/.gitignore b/.gitignore index 781c28d33..fe10f09c3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ AnkiDroid/ACRA-INSTALLATION .DS_Store .externalNativeBuild .cxx + +# optional symlink to venv +python diff --git a/.gitmodules b/.gitmodules index cefc90f14..feca11298 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,6 @@ [submodule "anki"] path = rslib-bridge/anki url = https://github.com/ankidroid/anki - branch = ankidroid-2-1-34 +[submodule "ftl/core"] + path = ftl/core + url = https://github.com/ankitects/anki-core-i18n.git diff --git a/README.md b/README.md index 77414d348..bf3e498bd 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,22 @@ Adapter allowing AnkiDroid to leverage Anki Desktop's Rust-based business logic ## Why? -* Removes the need to port Anki Desktop's business logic to Java - * 100% compatibility and no bugs - * Rust should provide a speed increase - * An upgrade for AnkiDroid should only require moving to a later commit in a submodule - * Saves massive amount of AnkiDroid developer time & effort - * Allows Anki Desktop to iterate faster - * We can quickly port changes upstream, which will benefit the ecosystem -* Insulates Anki-Android users from the complexity of installing multiple toolchains - * The Rust/Python/cross-compilation toolchain is much more complex than downloading Android Studio - * A separate repository means we keep a low barrier to entry for new contributors +- Removes the need to port Anki Desktop's business logic to Java + - 100% compatibility and no bugs + - Rust should provide a speed increase + - An upgrade for AnkiDroid should only require moving to a later commit in a submodule + - Saves massive amount of AnkiDroid developer time & effort + - Allows Anki Desktop to iterate faster + - We can quickly port changes upstream, which will benefit the ecosystem +- Insulates Anki-Android users from the complexity of installing multiple toolchains + - The Rust/Python/cross-compilation toolchain is much more complex than downloading Android Studio + - A separate repository means we keep a low barrier to entry for new contributors ## How to use it in a project -```gradle - implementation "io.github.david-allison-1:anki-android-backend:0.1.11" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.11" -``` +AnkiDroid uses a pre-built version of this library, and includes it in AnkiDroid/build.gradle. +To build a local version of this library and tell AnkiDroid to use it, please see the instructions +in docs/TESTING.md ## Folders @@ -40,16 +39,14 @@ This is defined as an application to allow instrumented tests to be run against ## Implementation -* Points to a fixed commit of `anki` - * Currently as a fork in `david-allison-1/anki` - * Modified to use a submodule for translations so we have a reproducible build - * Modifications to the library so we do not need to update to database schema 15 for version 1 -* References `backend.proto` and `fluent.proto` which define RPC service calls to the anki backend -* Python script to auto-generate the Java interface/backend to the RPC mechanism. Invoked via gradle. -* Android Library which contains the rust based `.so` under (x86, x86-64, arm, arm64) - * Implements `android.database.sqlite`, redirecting SQL to the rust library - * Exposes RPC calls to Rust via a clean Java interface (`net.ankiweb.rsdroid.RustBackend`) -* Testing library to allow the above to be usable under Robolectric +- Points to a fixed commit of `ankidroid/anki` + - Modifications to the library so we do not need to update to database schema 15 for version 1 +- References `rslib-bridge/anki/proto/anki/*.proto` which define RPC service calls to the anki backend +- Python script to auto-generate the Java interface/backend to the RPC mechanism. Invoked via gradle. +- Android Library which contains the rust based `.so` under (x86, x86-64, arm, arm64) + - Implements `android.database.sqlite`, redirecting SQL to the rust library + - Exposes RPC calls to Rust via a clean Java interface (`net.ankiweb.rsdroid.Backend`) +- Testing library to allow the above to be usable under Robolectric ## Additional Information diff --git a/build-current.sh b/build-current.sh new file mode 100755 index 000000000..971110c22 --- /dev/null +++ b/build-current.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Builds for the current architecture only. See docs/TESTING.md +# + +set -e + +export CURRENT_ONLY=true + +# build simulator/device package +./gradlew assembleRelease + +# build library for Robolectric tests +CARGO_TARGET_DIR=target NO_CROSS=true ./gradlew rsdroid-testing:build diff --git a/build.gradle b/build.gradle index 2e758f923..07d538c9d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,12 @@ buildscript { compileSdkVersion = 31 targetSdkVersion = 30 minSdkVersion = 21 - protobufVersion = "3.19.3" + protobufVersion = "3.21.2" appcompatVersion = "1.3.1" androidxTestJunitVersion = "1.1.3" sqliteVersion = "2.2.0" + kotlin_version = '1.6.21' + version_dokka = "1.6.20" } repositories { @@ -21,7 +23,9 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:4.2.2" - + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:${version_dokka}" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index bc2fa1e99..e4dda9e5b 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -61,11 +61,11 @@ You should open Android Studio and use the Tools --> SDK Manager to download the Install rust via [rustup](https://rustup.rs/) -Configure rust to use version 1.54.0 since current does not work yet [#168](https://github.com/ankidroid/Anki-Android-Backend/issues/168) +Install the Rust version the desktop code uses (later is probably fine too): ```bash -rustup install 1.54.0 -rustup default 1.54.0 +rustup install 1.58.1 +rustup default 1.58.1 ``` #### Android targets @@ -114,12 +114,13 @@ cargo install cross --git https://github.com/rust-embedded/cross --tag v0.2.1 - macOS this should be `brew install python` - linux you may need to make sure `python` exists instead of just `python3`, for example `sudo apt install python-is-python3` -#### Install protobuf package +#### Install python packages ```bash -pip install protobuf +pip install protobuf stringcase ``` + ## Make sure it works Basic commands to build and test things may be taken from the `./github/workflows` scripts. diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index dcac172c0..15f64e8a4 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -1,140 +1,170 @@ # Overview -## Introduction and Basic Flow +Anki-Android-Backend uses Rust, Java and a little Python. Since setting up a Rust environment is somewhat complex, having a separate library encourages drive-by contributions to the main app by keeping a low barrier to entry for Anki-Android. -Anki-Android-Backend uses Rust, Java and a little Python. Since setting up a Rust environment is complex, having a separate library encourages drive-by contributions to the main app by keeping a low barrier to entry for Anki-Android. +This repo is comprised of two main components: -There are two aspects of this library: +- `rslib-bridge` is a small Rust project that gets compiled into a shared library, which Java code can import. Its API consists + of only three different functions: `openBackend()`, `closeBackend()`, and `runMethodRaw()`. When one of these functions is called by + Java code, `rslib-bridge` takes care of converting the Java objects to a native Rust representation, and then passes the call + on to Anki's Rust backend, which gets included in `rslib-bridge` via the `rslib-bridge/anki[/rslib]` submodule. +- `rsdroid` is a Kotlin library that provides a friendly interface to the backend code. The bulk of its code is automatically + generated from the service definitions in `rslib-bridge/anki/proto/anki`. `rsdroid` also provides an adaptor to the Rust + database functionality, so that the Rust backend can be used in place of the standard Android SQLite library. -* `rsdroid.so` - A rust library which contains `anki/rslib` and a JNI bridge (`rslib-bridge`) -* rsdroid.aar - A java library with `rsdroid.so` and handles command processing plus acts as an adapter for database access +## Protocol Buffers -### Protocol Buffers +The Rust backend uses Protocol Buffers to define available methods, +and the structure of data each method takes and receives. The files can +be found in `rslib-bridge/anki/proto/anki`. -Serialisation over the JNI boundary happens mostly via **Protocol Buffers**. +For example: -The source files are stored in `anki/proto`. We perform RPC over JNI, rather than depending on HTTP. - -`fluent.proto` is a special-case, and is generated when the Rust library is built (`anki/rslib/build.rs`) - -Protobuf service generation is handled on the rust side via `anki/rslib/build.rs` and on the java side via `tools/protoc-gen/protoc-gen.py`. - -### Main usages +``` +service StatsService { + rpc GetGraphPreferences(generic.Empty) returns (GraphPreferences); +} +... +message GraphPreferences { + enum Weekday { + SUNDAY = 0; + MONDAY = 1; + FRIDAY = 5; + SATURDAY = 6; + } + Weekday calendar_first_day_of_week = 1; + bool card_counts_separate_inactive = 2; + bool browser_links_supported = 3; + bool future_due_show_backlog = 4; +} +``` -See the components section for additional details +Every method takes a _request_ structure, and returns a _response_ structure. +In the case of GetGraphPreferences, which doesn't need any input arguments, +we use the _Empty_ structure which is a message that contains no fields. -### Command Setup +When Java code wants to invoke a method on the backend, it first creates the +input request, and then encodes it into a ByteArray. It passes the method +id and the input bytes into the backend's `runMethodRaw()`, and the backend +returns another ByteArray which can be decoded into the output message. -![](https://www.planttext.com/api/plantuml/img/ZLLDZzem4BtxLupsi0Af8FNAeThjqWE7TYMazj9MaUDCmKkmhV44YNzVEmci7Q8bkIJnl3VpvYjvyYo9csCjf69Bq5tPiuV68maNS5ff9mt3jl6y9gkhhr8Tq5GHD3mJfMrC9UaC3y_ce1VFfehMHMz-o1psXpzPrtxCyElpvkZgIvXBX1GOhX-IzGc_8-zjvPTlyYHx_PaXqKM-rkMY95tjCDSJpfVa8xV5T924AKD6EQF5HK8q5MMlS4UsSP3cBuI8vOJ5bzighi0wD2-shb6njcOjMPRIuyn9tiz1t116dAn04Kf6M9UmCQ6xHY4ymWxvwcq-wYXjGQyawcvXxv8wAI833yX1WJKd98Q81RRWoBAzuIIT_23UQrQHZPaB4RlQ5VQrPAaDEAiDXvhEh45WVSHvYqa3vF5MWCOtXFthR1IVJKqdCCTdWCX8PcKCdvX7_1DoGzTSnWDaAJWahprdZ3XpQNs21dYltiGiqsupVOBopFZxxzIS1-46IMVm2fMj44AGoT1sakw1eun2NNKKSIMxvPl4j5HqErGOMpIk2aztYI47KD90YV5hMSMfbqgXzs4PwWdTxseyeUi9yAFvVjcZEi2_y1L78ajtyLlqpXgFeFrbcPVVZexFopzcsqtcwLB0E6JnBWOEsz-4U0fluN_o7m00) +Doing this manually for each method would be a pain, so we use code generation +instead. `tools/protoc-gen/protoc-gen.py` reads the Protobuf files, and +automatically generates methods for us into a GeneratedBackend.kt file. Eg: -Source: `docs/sources/sequence_open_collection.plantuml`. Created with [planttext](https://www.planttext.com/) +``` + @Throws(BackendException::class) + fun getGraphPreferencesRaw(input: ByteArray): ByteArray { + return runMethodRaw(service=10, method=2, input); + } -### Process command + @Throws(BackendException::class) + open fun getGraphPreferences(): anki.stats.GraphPreferences { + val builder = anki.generic.Empty.newBuilder(); + val input = builder.build(); + return anki.stats.GraphPreferences.parseFrom( + getGraphPreferencesRaw(input.toByteArray())); + } +``` -![](https://www.planttext.com/api/plantuml/img/bPH1ZzCm48Nl_XMZFQNIjg9mAnBQ1VQ0n7802Gvq5JcnTsis6KVZSKibVZpZ90Kd1R7aK4MUzxqPFpjLLu4rSMmRfMls1CCpUGyGWoNLYSxLhjF8y346ValUcTUwVhHeacY-fYeVqMWwmiKrFhhbDPfKNOxbYudXkFXv_QxjcfFRoIWNolD1izlRMyixRyBgczxhSSn98MjFeN7LiY9d7koqhQolA2IsrmoIZDGo-9JeTGb8fR8Q9tmW7pl8jwbK2WsMhywpsa2m_DxNkhbr6Dc6BpPmiNws0ANEnAF1Fza1_JErWThZtXA3ansmXuuy-pamIMy3zhkjfS4RtxOQJT4nNSBwnILKHxPVxnOlrKIV3B88KyU_SPaiKNcE6w28vOYMYGX5ngfS-mJsUUaGBVs7nQU3ute77cNaBHfRUsCbj2xo5YNpHj9lxbTDohziXmCe3t82MoJBaH1yP16d-z5dl4NvZ2oH_9wMpYQOn3RQ53TjnySVwKBT97enJssMzN2wlNywttxtJqCqkleN0eMx1zwHF-1Pn_dD_EtHpveyzbALGCsu2wNKbGZbl-Kd) +## Backend -Source: `docs/sources/sequence_method_call.plantuml`. Created with [planttext](https://www.planttext.com/) +The main class that AnkiDroid uses is called `Backend`. It includes: -## Components +- all the generated method definitions from `GeneratedBackend.kt` +- a `tr` property that exposes all the backend translations +- helper methods for DB access -### Anki-Android +When `Backend` is instantiated, it uses `NativeMethods.kt:openBackend()` to +create a handle to a backend instance. `Backend.close()` takes care of +also closing the backend instance, closing the collection if open, and +freeing up memory. -There is a small amount of code in the consuming app Anki-Android to use this library +Each backend instance supports a single open collection, so multiple +backend instances need to be created if you need to have multiple collections +open in parallel. Switching between collections does not require multiple +instances - you can close one collection and then open another with the +same backend instance. -#### DroidBackend/RustDroidBackend +`Backend` also wraps the majority of method calls in a mutex, which prevents +any backend call from being made while a transaction is active on another +thread. -Java/Rust interface to the backend. `RustDroidBackend` wraps the implementation of the Rust. +## BackendFactory -* Allows a testable comparison between the Java and the Rust during the conversion - * Allows a pure Java conversion afterwards (if deemed appropriate due to AGPL concerns) -* Encapsulates all access to the Rust, allowing the implementation to later be swapped out within one file. +`BackendFactory.getBackend()` is used by AnkiDroid to get a `Backend` instance +with translations set to the language currently configured by the user. -### Anki-Android-Backend +The returned backend has a boolean legacySchema property, which is +set to `BackendFactory.defaultLegacySchema`. -#### RustBackend (interface) +When set to the default of true, collections are not upgraded on opening, +and they remain in the legacy schema 11 format that AnkiDroid's legacy +code expects. Many backend methods require the schema to be updated, so +when this setting is enabled, the backend can effectively only be used +for DB access, and a few routines that do not require an open collection +to operate. -An interface of the commands allowed to be sent to the Rust. Allows for easy mocking, and method discovery. +When set to false, collections are upgraded to the latest schema on opening, +allowing full use of backend functionality. This setting has not received +much testing yet. To change it, see TESTING.md -##### RustBackend Example +## Error handling -RustBackend is generated by `gen/protoc-gen/protoc-gen.py` and is not checked into source control. +The API `rslib-bridge` exposes is defined in `rsdroid`'s NativeMethods.kt: -```java - Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable com.google.protobuf.ByteString template, boolean fillEmpty); +``` + external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray): Array? + external fun openBackend(data: ByteArray): Array? + external fun closeBackend(backendPointer: Long) ``` -#### RustBackendImpl - -It contains a method per RPC method defined in `backend.proto` - -It is responsible for: - -* Converting parameters from Java types to protobufs -* Executing a command -* Ensuring that the result was not an error -* Deserializing and returning data (if applicable) +When the backend returns data, it needs to be able to return either the data, +or an error message. This is done with nested arrays: `Array?` is +`[valid_data_or_null, error_data_or_null]`. The `Backend` class takes care of +this for us, extracting the error message, decoding it into a protobuf message and +then a native BackendException, and throwing it. The outer array is declared +as nullable to account for rare cases where an array can't be allocated. -##### RustBackendImpl Example +## Usage in AnkiDroid -RustBackendImpl is generated by `gen/protoc-gen/protoc-gen.py` and is not checked into source control. +When a collection is opened with `Storage.collection()`, a `Backend` instance +is created (or reused if provided), and stored in `Collection.backend`. As +`Collection` is initialized, it calls `Storage.openDB(path, backend)` which +creates a `DB` instance that delegates database calls to the provided backend. -```java - public Backend.SearchCardsOut searchCards(@Nullable java.lang.String search, @Nullable Backend.SortOrder order) { - byte[] result = null; - try { - Backend.SearchCardsIn.Builder builder = Backend.SearchCardsIn.newBuilder(); - if (search != null) { builder.setSearch(search); } - if (order != null) { builder.setOrder(order); } - Backend.SearchCardsIn protobuf = builder.build(); +If `defaultLegacySchema` is false, a `CollectionV16` subclass of `Collection` +is returned instead. It contains changes to work with the new backend methods, +such as requesting a list of decks from the backend instead of directly trying +to query them via SQL, eg: - Pointer backendPointer = ensureBackend(); - result = NativeMethods.executeCommand(backendPointer.toJni(), 9, protobuf.toByteArray()); - Backend.SearchCardsOut message = Backend.SearchCardsOut.parseFrom(result); - validateMessage(result, message); - return message; - } catch (InvalidProtocolBufferException ex) { - validateResult(result); - throw BackendException.fromException(ex); +``` + override fun all_names_and_ids(skip_empty_default: Boolean, include_filtered: Boolean): List { + return backend.getDeckNames(skip_empty_default, include_filtered).map { + entry -> + DeckNameId(entry.name, entry.id) } } ``` -#### BackendV1Impl - -BackendV1Impl extends `RustBackendImpl`. - -It is responsible for: - -* Maintaining a pointer to the collection (to be passed into the Rust to identify the collection) -* Accessing methods in the Rust which are not generated from Protobufs - * JSON serialization for database inputs - * Collection opening/closing - -#### NativeMethods - -Method definitions to access `rslib-bridge` - -### rslib-bridge - -#### lib.rs - -All public methods callable from the Java are available here. +V16 corresponds to the schema version that was current when this code was originally +written, and is not technically correct anymore. The DecksV16/ModelsV16/etc classes +also use a few layers of indirection, as they were written at a time when it wasn't +clear whether AnkiDroid would be using the Rust backend or not. -Responsible for: +## Usage in unit tests -* Handling JNI specific concerns (conversions from Java to Rust primitives) -* Calling methods in `anki/rslib` -* Defining the interface callable by `NativeMethods` -* Converting a provided `backendPointer` into a Collection object -* Serialization of outputs and deserialization of inputs -* Handling panics and converting them to errors +AnkiDroid's unit tests run with Robolectric, and to use the backend inside Robolectric, a separate build of rslib-bridge is required. This is handled +by `rsdroid-testing`, which takes care of compiling `rslib-bridge` correctly, +and provides a `RustBackendLoader.kt` file which AnkiDroid's unit tests call +into. ## Database Access We need to use the Rust for database access as: -* We need an open collection to perform most commands in rslib -* An open collection obtains a lock on the database - access can only be made through the Rust. +- We need an open collection to perform most commands in rslib +- An open collection obtains a lock on the database - access can only be made through the Rust. So, we implement `SupportSQLiteOpenHelper.Factory` and related classes. @@ -144,10 +174,10 @@ Anki's rust code does not stream database results, all results are currently obt This is not a significant problem, as: -* Rust is not confined by the Java heap limit -* Most results are small -* In time, we will move most data processing to the Rust, removing the need to deserialize data -* Java has been converted to use protobuf serialization (vs Anki Desktop using JSON), this significantly reduces memory usage. +- Rust is not confined by the Java heap limit +- Most results are small +- In time, we will move most data processing to the Rust, removing the need to deserialize data +- Java has been converted to use protobuf serialization (vs Anki Desktop using JSON), this significantly reduces memory usage. #### LimitOffsetSQLiteCursor diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 000000000..9a95dcfc6 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,79 @@ +# Testing changes with AnkiDroid on an X86_64 sim (Linux) + +It is possible to limit the build to the current architecture, to avoid having +to use Docker or to cross-compile for multiple platforms. + +A similar approach may work on Mac and Windows, but this has only been tested on Linux +so far. + +## Setup + +Make sure you can build AnkiDroid first. + +Install NDK: + +- Download https://developer.android.com/studio#command-tools +- Rename cmdline-tools to $ANDROID_SDK_ROOT/cmdline-tools/latest +- Get ndk version from rslib/build.grade +- .github/scripts/install_ndk.sh 22.0.7026061 + +Install Rust: + +- rustup install 1.58.1 +- rustup target add x86_64-linux-android +- sudo ln -sf /usr/bin/gcc /usr/bin/x86_64-unknown-linux-gnu-gcc + +Install protobuf: + +- Install protobuf with your package manager + +## Optional Python venv + +If you don't want to `pip install protobuf stringcase` globally, +you can do so in a venv, and then symlink the python bin from the +venv into `python` at the top of the project folder: + +``` +$ ln -sf /path/to/venv/bin/python python +``` + +## Install Python packages + +``` +pip install protobuf stringcase +``` + +## Build + +Two files need to be built: + +- A .aar file for emulator/device testing +- A .jar for running unit tests + +``` +export ANDROID_SDK_ROOT=$HOME/Android/Sdk +export PATH=$HOME/Android/Sdk/cmdline-tools/latest/bin/:$PATH +./build-current.sh +``` + +## Modify AnkiDroid to use built library + +Tell gradle to load the compiled .aar and .jar files from disk by editing local.properties +in the AnkiDroid repo, and adding the following line: + +``` +local_backend=true +``` + +If you also want to test out the new schema code paths that make greater use of the backend, +add the following line (be warned, do not use this on a collection you care about yet): + +``` +legacy_schema=false +``` + +Also make sure ext.ankidroid_backend_version in AnkiDroid/build.gradle matches the version +of the backend you're testing. + +After making the change, you should be able to build and run the project on an x86_64 +emulator/device, and run unit tests. diff --git a/docs/easy-testing.md b/docs/easy-testing.md deleted file mode 100644 index 0ebbdcda8..000000000 --- a/docs/easy-testing.md +++ /dev/null @@ -1,120 +0,0 @@ -# Testing changes with AnkiDroid on an X86_64 sim (Linux) - -## Setup - -Make sure you can build AnkiDroid first. - -Install NDK: - -- Download https://developer.android.com/studio#command-tools -- Rename cmdline-tools to $ANDROID_SDK_ROOT/cmdline-tools/latest -- Get ndk version from rslib/build.grade -- .github/scripts/install_ndk.sh 22.0.7026061 - -Install Rust: - -- rustup install 1.54.0 -- rustup target add x86_64-linux-android -- sudo ln -sf /usr/bin/gcc /usr/bin/x86_64--unknown-linux-gnu-gcc - -Install protobuf: - -- Install protobuf with your package manager -- pip install protobuf, or see the venv section below - -## Limit build to x86_64 - -So you don't need to install cross compilers, patch the sources to -only build the x86_64 image for the aar file and jar: - -```diff -diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle -index bc3a401..7c69a5b 100644 ---- a/rsdroid/build.gradle -+++ b/rsdroid/build.gradle -@@ -72,7 +72,7 @@ dependencies { - - } - --preBuild.dependsOn "cargoBuild" -+preBuild.dependsOn "cargoBuildX86_64" - - signing { - def hasPrivate = project.hasProperty('SIGNING_PRIVATE_KEY') -diff --git a/rsdroid-testing/build.gradle b/rsdroid-testing/build.gradle -index 8641b8f..694212e 100644 ---- a/rsdroid-testing/build.gradle -+++ b/rsdroid-testing/build.gradle -@@ -181,8 +181,6 @@ task copyWindowsOutput(type: Copy) { - // TODO: check for cargo - // check for targets: x86_64-apple-darwin, x86_64-pc-windows-gnu, TODO: Linux - --processResources.dependsOn preBuildWindows --processResources.dependsOn copyWindowsOutput - // To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" - // execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu - // "linker `x86_64-unknown-linux-gnu-gcc` not found" -``` - -## Using a custom python venv - -If you don't want to `pip install protobuf` globally, you can -switch to a venv: - -```diff -diff --git a/tools/protoc-gen/protoc-gen.sh b/tools/protoc-gen/protoc-gen.sh -index d4039ec..3ac5d29 100755 ---- a/tools/protoc-gen/protoc-gen.sh -+++ b/tools/protoc-gen/protoc-gen.sh -@@ -1,2 +1,3 @@ - #!/bin/bash --./tools/protoc-gen/protoc-gen.py -+ -+$HOME/Local/python/misc/bin/python3 ./tools/protoc-gen/protoc-gen.py -``` - -## Build - -Two files need to be built: - -- A .aar file for emulator/device testing -- A .jar for running unit tests - -``` -export ANDROID_SDK_ROOT=$HOME/Android/Sdk -export PATH=$HOME/Android/Sdk/cmdline-tools/latest/bin/:$PATH -./gradlew assembleRelease -NO_CROSS=true ./gradlew rsdroid-testing:build -``` - -If your environment is set up to override the default -Rust output location, you must also set unset CARGO_TARGET_DIR. - -## Modify AnkiDroid to use built library - -Tell gradle to load the compiled .aar and .jar files from disk: - -```diff -diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle -index 2a2d94034..b21c7caff 100644 ---- a/AnkiDroid/build.gradle -+++ b/AnkiDroid/build.gradle -@@ -271,10 +271,10 @@ dependencies { - // - switch the commented and uncommented lines below - // - run a gradle sync - -- implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" -- testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" -- // implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.11.jar") -+ // implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" -+ // testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" -+ implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -+ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.11.jar") - - // On Windows, you can use something like - // implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar") -``` - -After making the change, force a gradle sync, and then you should be able to build -and run the project on an x86_64 emulator/device, and run unit tests. diff --git a/docs/source/sequence_open_collection.plantuml b/docs/source/sequence_open_collection.plantuml index 68961940e..a35f8ed11 100644 --- a/docs/source/sequence_open_collection.plantuml +++ b/docs/source/sequence_open_collection.plantuml @@ -37,7 +37,7 @@ BackendUtils -> RustBackend : openAnkiDroidCollection(OpenCollectionIn) rslibbridge -> NativeMethods: byte[] NativeMethods -> RustBackend: byte[] RustBackend -> RustBackend: Check for error - RustBackend -> RustBackend: Response is Backend.Empty.\nReturn void + RustBackend -> RustBackend: Response is Generic.Empty.\nReturn void end RustBackend -> user diff --git a/ftl/core b/ftl/core new file mode 160000 index 000000000..40963d9a2 --- /dev/null +++ b/ftl/core @@ -0,0 +1 @@ +Subproject commit 40963d9a29703c756a47bac8e0e7e5174a286c86 diff --git a/gradle.properties b/gradle.properties index 887c4f4c5..519138e63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ android.useAndroidX=true android.enableJetifier=false GROUP=io.github.david-allison-1 -VERSION_NAME=0.1.11 +VERSION_NAME=0.1.14-anki2.1.54 POM_INCEPTION_YEAR=2020 diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java index 49f457a99..0a8219cc4 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java @@ -21,7 +21,7 @@ import net.ankiweb.rsdroid.ankiutil.DatabaseUtil; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Ignore; import org.junit.Test; @@ -41,8 +41,8 @@ public void testDisposalDoesNotLeak() throws IOException { for (int i = 0; i < 10000; i++) { Timber.d("Iteration %d", i); - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { - SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { + SupportSQLiteDatabase db = AnkiSupportSQLiteDatabase.withRustBackend(BackendFactory.getBackend(getContext())); int count = DatabaseUtil.queryScalar(db, "select count(*) from revlog"); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java index 88c52184a..a36a55677 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java @@ -16,25 +16,21 @@ package net.ankiweb.rsdroid; -import androidx.annotation.VisibleForTesting; +import android.content.Context; -import com.google.protobuf.InvalidProtocolBufferException; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; -import BackendProto.Backend; +import java.util.Arrays; -public class BackendForTesting extends BackendV1Impl { +public class BackendForTesting extends Backend { - BackendForTesting() { - super(); + public BackendForTesting(@NonNull Context context, @NonNull Iterable langs, boolean legacySchema) { + super(context, langs, legacySchema); } - public static BackendForTesting create() { - try { - NativeMethods.ensureSetup(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - return new BackendForTesting(); + public static BackendForTesting create(@NonNull Context context) { + return new BackendForTesting(context, Arrays.asList("en"), true); } @@ -46,48 +42,24 @@ public static BackendForTesting create() { */ @VisibleForTesting public void debugProduceError(ErrorType error) { - byte[] result = null; - try { - Pointer backendPointer = ensureBackend(); - result = NativeMethods.debugProduceError(backendPointer.toJni(), error.toString()); - - // This should fail validate - Backend.Empty message = Backend.Empty.parseFrom(result); - validateMessage(result, message); - - throw new IllegalStateException("An exception should have been thrown"); - - } catch (InvalidProtocolBufferException ex) { - validateResult(result); - throw BackendException.fromException(ex); - } + super.debugProduceError(error.toString()); + throw new IllegalStateException("An exception should have been thrown"); } public enum ErrorType { InvalidInput, TemplateError, - TemplateSaveError, - IOError, + IoError, DbErrorFileTooNew, DbErrorFileTooOld, DbErrorMissingEntity, DbErrorCorrupt, DbErrorLocked, DbErrorOther, - NetworkErrorOffline, - NetworkErrorTimeout, - NetworkErrorProxyAuth, - NetworkErrorOther, - SyncErrorConflict, - SyncErrorServerError, - SyncErrorClientTooOld, + NetworkError, SyncErrorAuthFailed, - SyncErrorServerMessage, - SyncErrorClockIncorrect, SyncErrorOther, - SyncErrorResyncRequired, - SyncErrorDatabaseCheckRequired, JSONError, ProtoError, Interrupted, @@ -95,80 +67,8 @@ public enum ErrorType { CollectionAlreadyOpen, NotFound, Existing, - DeckIsFiltered, + FilteredDeckError, SearchError, FatalError; - - public String toString() { - switch (this) { - case InvalidInput: - return "InvalidInput"; - case TemplateError: - return "TemplateError"; - case TemplateSaveError: - return "TemplateSaveError"; - case IOError: - return "IOError"; - case DbErrorFileTooNew: - return "DbErrorFileTooNew"; - case DbErrorFileTooOld: - return "DbErrorFileTooOld"; - case DbErrorMissingEntity: - return "DbErrorMissingEntity"; - case DbErrorCorrupt: - return "DbErrorCorrupt"; - case DbErrorLocked: - return "DbErrorLocked"; - case DbErrorOther: - return "DbErrorOther"; - case NetworkErrorOffline: - return "NetworkErrorOffline"; - case NetworkErrorTimeout: - return "NetworkErrorTimeout"; - case NetworkErrorProxyAuth: - return "NetworkErrorProxyAuth"; - case NetworkErrorOther: - return "NetworkErrorOther"; - case SyncErrorConflict: - return "SyncErrorConflict"; - case SyncErrorServerError: - return "SyncErrorServerError"; - case SyncErrorClientTooOld: - return "SyncErrorClientTooOld"; - case SyncErrorAuthFailed: - return "SyncErrorAuthFailed"; - case SyncErrorServerMessage: - return "SyncErrorServerMessage"; - case SyncErrorClockIncorrect: - return "SyncErrorClockIncorrect"; - case SyncErrorOther: - return "SyncErrorOther"; - case SyncErrorResyncRequired: - return "SyncErrorResyncRequired"; - case SyncErrorDatabaseCheckRequired: - return "SyncErrorDatabaseCheckRequired"; - case JSONError: - return "JSONError"; - case ProtoError: - return "ProtoError"; - case Interrupted: - return "Interrupted"; - case CollectionNotOpen: - return "CollectionNotOpen"; - case CollectionAlreadyOpen: - return "CollectionAlreadyOpen"; - case NotFound: - return "NotFound"; - case Existing: - return "Existing"; - case DeckIsFiltered: - return "DeckIsFiltered"; - case SearchError: - return "SearchError"; - case FatalError: - return "FatalError"; - default: throw new IllegalStateException("Unknown: " + this); - } - } } } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java index 41c534e78..562570209 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; -import BackendProto.Backend.SchedTimingTodayOut; +import anki.scheduler.SchedTimingTodayResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -49,12 +49,12 @@ public void test() { throw new IllegalArgumentException("do not run on real device yet"); } } - + @Test public void testBackendException() { - BackendV1 backendV1 = getClosedBackend(); + Backend Backend = getClosedBackend(); try { - SchedTimingTodayOut ret = backendV1.schedTimingToday(); + Backend.closeCollection(true); Assert.fail("call should have failed - needs an open collection"); } catch (BackendException ex) { // OK @@ -63,8 +63,8 @@ public void testBackendException() { @Test public void schedTimingTodayCall() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); - SchedTimingTodayOut ret = backendV1.schedTimingToday(); + Backend backend = getBackend("initial_version_2_12_1.anki2"); + SchedTimingTodayResponse ret = backend.schedTimingTodayLegacy(1655258084, 0, 1655258084, 0, 0); int elapsed = ret.getDaysElapsed(); long nextDayAt = ret.getNextDayAt(); } @@ -73,7 +73,7 @@ public void schedTimingTodayCall() { public void collectionIsVersion11AfterOpen() throws JSONException { // This test will be decomissioned, but before we get an upgrade strategy, we need to ensure we're not upgrading the database. - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); JSONArray array = backendV1.fullQuery("select ver from col"); @@ -86,13 +86,13 @@ public void collectionIsVersion11AfterOpen() throws JSONException { @Test public void fullQueryTest() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); JSONArray result = backendV1.fullQuery("select * from col"); } @Test public void columnNamesTest() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); String[] names = backendV1.getColumnNames("select * from col"); assertThat(names, is(new String[] { "id", "crt", "mod", "scm", "ver", "dty", "usn", "ls", "conf", "models", "decks", "dconf", "tags" })); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java index b0230e3d5..d79479fbe 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java @@ -36,7 +36,7 @@ public class BackendMutexTest extends InstrumentedTest { @Test public void ensureDatabaseInTransactionIsLocked() throws JSONException, InterruptedException { - BackendMutex b = (BackendMutex) super.getBackend("initial_version_2_12_1.anki2"); + Backend b = (Backend) super.getBackend("initial_version_2_12_1.anki2"); b.fullQuery("create table test (id int)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java index 275d8498f..8d0f124f7 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java @@ -21,7 +21,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Ignore; import org.junit.Test; @@ -141,8 +141,8 @@ public void ensureSQLIsStreamed() throws IOException { at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1837) */ - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { - SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { + SupportSQLiteDatabase db = AnkiSupportSQLiteDatabase.withRustBackend(backend); db.query("create table tmp (id varchar)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java new file mode 100644 index 000000000..6eaa690a8 --- /dev/null +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java @@ -0,0 +1,34 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +package net.ankiweb.rsdroid; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +public class BackendTranslationsTest extends InstrumentedTest { + + private String withoutIsolation(String s) { + return s.replace("\u2068", "").replace("\u2069", ""); + } + + @Test + public void ensureI18nWorks() { + Backend b = BackendFactory.getBackend(getContext()); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Trash folder: 5 files, 10MB")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10.0)), equalTo("Trash folder: 5 files, 10MB")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, "foo")), equalTo("Trash folder: 5 files, fooMB")); + b = BackendFactory.getBackend(getContext(), Arrays.asList("fr")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Corbeille : 5 fichiers, 10 Mo")); + } +} diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java index 234eab831..c7a57d5c7 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java @@ -16,8 +16,6 @@ package net.ankiweb.rsdroid; -import android.database.sqlite.SQLiteDatabaseCorruptException; - import net.ankiweb.rsdroid.database.NotImplementedException; import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException; import net.ankiweb.rsdroid.exceptions.BackendExistingException; @@ -42,6 +40,10 @@ import static org.junit.Assert.fail; +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + @RunWith(Parameterized.class) public class ExceptionTest { @@ -67,14 +69,6 @@ public static java.util.Collection initParameters() { { BackendForTesting.ErrorType.SearchError, NOT_POSSIBLE }, { BackendForTesting.ErrorType.SyncErrorAuthFailed, BackendSyncException.BackendSyncAuthFailedException.class }, - { BackendForTesting.ErrorType.SyncErrorClientTooOld, BackendSyncException.BackendSyncClientTooOldException.class }, - { BackendForTesting.ErrorType.SyncErrorClockIncorrect, BackendSyncException.BackendSyncClockIncorrectException.class }, - { BackendForTesting.ErrorType.SyncErrorConflict, BackendSyncException.BackendSyncConflictException.class }, - { BackendForTesting.ErrorType.SyncErrorDatabaseCheckRequired, BackendSyncException.BackendSyncDatabaseCheckRequiredException.class }, - { BackendForTesting.ErrorType.SyncErrorResyncRequired, BackendSyncException.BackendSyncResyncRequiredException.class }, - { BackendForTesting.ErrorType.SyncErrorServerMessage, BackendSyncException.BackendSyncServerMessageException.class }, - { BackendForTesting.ErrorType.SyncErrorServerError, BackendSyncException.BackendSyncServerErrorException.class }, - { BackendForTesting.ErrorType.SyncErrorOther, BackendSyncException.class }, { BackendForTesting.ErrorType.DbErrorCorrupt, NOT_POSSIBLE }, @@ -83,26 +77,17 @@ public static java.util.Collection initParameters() { { BackendForTesting.ErrorType.DbErrorFileTooOld, BackendException.BackendDbException.BackendDbFileTooOldException.class}, { BackendForTesting.ErrorType.DbErrorLocked, BackendException.BackendDbException.BackendDbLockedException.class}, { BackendForTesting.ErrorType.DbErrorMissingEntity, BackendException.BackendDbException.BackendDbMissingEntityException.class}, - { BackendForTesting.ErrorType.DbErrorOther, BackendException.BackendDbException.class}, - - - { BackendForTesting.ErrorType.NetworkErrorOffline, BackendNetworkException.BackendNetworkOfflineException.class}, - { BackendForTesting.ErrorType.NetworkErrorProxyAuth, BackendNetworkException.BackendNetworkProxyAuthException.class}, - { BackendForTesting.ErrorType.NetworkErrorTimeout, BackendNetworkException.BackendNetworkTimeoutException.class}, - - { BackendForTesting.ErrorType.NetworkErrorOther, BackendNetworkException.class}, - - { BackendForTesting.ErrorType.DeckIsFiltered, BackendDeckIsFilteredException.class }, + { BackendForTesting.ErrorType.NetworkError, BackendNetworkException.class}, + { BackendForTesting.ErrorType.FilteredDeckError, BackendDeckIsFilteredException.class }, { BackendForTesting.ErrorType.Existing, BackendExistingException.class }, - { BackendForTesting.ErrorType.FatalError, BackendFatalError.class }, + { BackendForTesting.ErrorType.FatalError, BackendException.BackendFatalError.class }, { BackendForTesting.ErrorType.Interrupted, BackendInterruptedException.class}, { BackendForTesting.ErrorType.InvalidInput, BackendInvalidInputException.class}, - { BackendForTesting.ErrorType.IOError, BackendIoException.class}, + { BackendForTesting.ErrorType.IoError, BackendIoException.class}, { BackendForTesting.ErrorType.JSONError, BackendJsonException.class }, { BackendForTesting.ErrorType.ProtoError, BackendProtoException.class }, { BackendForTesting.ErrorType.TemplateError, BackendTemplateException.class}, - { BackendForTesting.ErrorType.TemplateSaveError, BackendTemplateException.BackendTemplateSaveException.class}, { BackendForTesting.ErrorType.NotFound, BackendNotFoundException.class}, }); @@ -111,7 +96,11 @@ public static java.util.Collection initParameters() { @Before public void errorProducesNamedException() { - backend = BackendForTesting.create(); + backend = BackendForTesting.create(getContext()); + } + + protected Context getContext() { + return InstrumentationRegistry.getInstrumentation().getTargetContext(); } @Test diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java index 27ba269eb..fa44d06ea 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java @@ -94,9 +94,9 @@ public void testUpdate() { @CheckResult private RustSupportSQLiteDatabase getDatabase() { try { - BackendV1 backendV1 = getBackend(fileName); + Backend backendV1 = getBackend(fileName); boolean readOnly = false; - return new RustSupportSQLiteDatabase(backendV1, readOnly); + return new RustSupportSQLiteDatabase(backendV1); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java index b473d782c..70ac9f1cb 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java @@ -23,11 +23,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendUtils; -import net.ankiweb.rsdroid.BackendV1; -import net.ankiweb.rsdroid.BackendV1Impl; -import net.ankiweb.rsdroid.NativeMethods; -import net.ankiweb.rsdroid.RustBackendFailedException; +import net.ankiweb.rsdroid.Backend; +import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; import org.jetbrains.annotations.NotNull; import org.junit.After; @@ -36,6 +33,7 @@ import java.util.ArrayList; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertThat; @@ -45,7 +43,7 @@ public class InstrumentedTest { Log.e("InstrumentedTest", "Timber has been disabled."); } - private final List backendList = new ArrayList<>(); + private final List backendList = new ArrayList<>(); protected final static int TEST_PAGE_SIZE = 1000; @@ -56,20 +54,21 @@ public void before() { Timber.uprootAll(); Timber.plant(new Timber.DebugTree()); */ - - try { - NativeMethods.ensureSetup(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - BackendV1Impl.setPageSizeForTesting(TEST_PAGE_SIZE); } @After public void after() { - for (BackendV1 b : backendList) { + for (Backend b : backendList) { if (b != null && b.isOpen()) { - assertThat("All database cursors should be closed", b.debugActiveDatabaseSequenceNumbers(0).getSequenceNumbersList(), empty()); + + List numbers; + try { + numbers = b.getActiveSequenceNumbers(); + } catch (BackendInvalidInputException exc) { + assertThat(exc.getLocalizedMessage(), containsString("CollectionNotOpen")); + continue; + } + assertThat("All database cursors should be closed", numbers, empty()); } } backendList.clear(); @@ -112,24 +111,21 @@ protected Context getContext() { } @NotNull - protected BackendV1 getBackend(String fileName) { + protected Backend getBackend(String fileName) { String path = getAssetFilePath(fileName); return getBackendFromPath(path); } @NotNull - protected BackendV1 getBackendFromPath(String path) { - BackendV1 backendV1 = getClosedBackend(); - BackendUtils.openAnkiDroidCollection(backendV1, path); - this.backendList.add(backendV1); - return backendV1; + protected Backend getBackendFromPath(String path) { + Backend backend = getClosedBackend(); + backend.setPageSize(TEST_PAGE_SIZE); + backend.openCollection(path); + backendList.add(backend); + return backend; } - protected BackendV1 getClosedBackend() { - try { - return BackendFactory.createInstance().getBackend(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } + protected Backend getClosedBackend() { + return BackendFactory.getBackend(getContext()); } } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java index f0cc6e43c..3455d0bcf 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java @@ -18,6 +18,7 @@ import android.database.sqlite.SQLiteDatabaseCorruptException; +import net.ankiweb.rsdroid.BackendException; import net.ankiweb.rsdroid.database.testutils.DatabaseCorruption; import org.junit.runner.RunWith; @@ -35,8 +36,12 @@ public class DatabaseRegularCorruptionTest extends DatabaseCorruption { protected void assertCorruption(Exception setupException) { // Rust: net.ankiweb.rsdroid.BackendException$BackendDbException: DBError { info: "SqliteFailure(Error { code: DatabaseCorrupt, extended_code: 11 }, Some(\"database disk image is malformed\"))", kind: Other } // Java: database disk image is malformed (code 11): , while compiling: PRAGMA journal_mode + +// assertThat(setupException.getClass(), typeCompatibleWith(BackendException.BackendDbException.class)); assertThat(setupException.getClass(), typeCompatibleWith(SQLiteDatabaseCorruptException.class)); + // this mapping to an unrelated exception should be done at a higher level + assertThat(setupException.getLocalizedMessage(), containsString("database disk image is malformed")); assertThat(setupException.getLocalizedMessage(), containsString("11")); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java index 051077eb3..4275e96bf 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java @@ -17,7 +17,7 @@ package net.ankiweb.rsdroid.database; import net.ankiweb.rsdroid.BackendException; -import net.ankiweb.rsdroid.BackendV1; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; import org.json.JSONArray; @@ -36,66 +36,20 @@ public class DowngradeTest extends InstrumentedTest { @Test - public void downgradeWithV16() throws IOException, JSONException { + public void downgradeWithLaterSchema() throws IOException, JSONException { String fileName = "schema_16.anki2"; - String path = getAssetFilePath(fileName); - - // opening the backend fails - assertOpeningFails(path); - - downgrade(path); - - // now it works - try (BackendV1 backendV1 = super.getBackendFromPath(path) ){ + try (Backend backendV1 = super.getBackendFromPath(path) ){ assertSchemaVer(backendV1, 11); } - } - - @Test - public void downgradeWithV17FailsAsTooNew() { - String fileName = "schema_17.anki2"; - - String path = getAssetFilePath(fileName); - - // opening the backend fails - assertOpeningFails(path); - - try { - downgrade(path); - fail(); - } catch (BackendException ex) { - assertThat(ex.getMessage(), containsString("FileTooNew")); - } - } - - - @Test - public void downgradeWithV11Fails() throws JSONException, IOException { - // A downgrade will throw an exception if the file is less than schema 16 - - String fileName = "initial_version_2_12_1.anki2"; - - String path = getAssetFilePath(fileName); - - try (BackendV1 backend = getBackendFromPath(path)) { - assertSchemaVer(backend, 11); - } - - try { - downgrade(path); - fail(); - } catch (Exception e) { - assertThat(e.getMessage(), containsString("FileTooOld")); - assertThat(e.getMessage(), containsString("Schema11")); - } - - try (BackendV1 backend = getBackendFromPath(path)) { - assertSchemaVer(backend, 11); + fileName = "schema_17.anki2"; + path = getAssetFilePath(fileName); + try (Backend backendV1 = super.getBackendFromPath(path) ){ + assertSchemaVer(backendV1, 11); } } - private void assertSchemaVer(BackendV1 backendV1, @SuppressWarnings("SameParameterValue") int expectedVersion) throws JSONException { + private void assertSchemaVer(Backend backendV1, @SuppressWarnings("SameParameterValue") int expectedVersion) throws JSONException { JSONArray array = backendV1.fullQuery("select ver from col"); assertThat(array.length(), is(1)); @@ -104,14 +58,9 @@ private void assertSchemaVer(BackendV1 backendV1, @SuppressWarnings("SameParamet assertThat(subArray.optInt(0, 0), is(expectedVersion)); } - private void downgrade(String collectionPath) { - BackendV1 backendV1 = getClosedBackend(); - backendV1.downgradeBackend(collectionPath); - } - @SuppressWarnings({"unused", "RedundantSuppression"}) private void assertOpeningFails(String path) { - try (BackendV1 unused = super.getBackendFromPath(path)) { + try (Backend unused = super.getBackendFromPath(path)) { fail(); } catch (Exception e) { // ignore diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java index 19cf99927..951243fb8 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java @@ -20,7 +20,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; -import net.ankiweb.rsdroid.BackendV1; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.DatabaseIntegrationTests; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; @@ -40,7 +40,7 @@ public class StreamingProtobufSQLiteCursorTest extends InstrumentedTest { @Test public void testPaging() throws IOException { - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -61,14 +61,14 @@ public void testPaging() throws IOException { } } - private SupportSQLiteDatabase getWritableDatabase(BackendV1 backend) { - return new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + private SupportSQLiteDatabase getWritableDatabase(Backend backend) { + return AnkiSupportSQLiteDatabase.withRustBackend(backend); } @Test public void testBackwards() throws IOException { Timber.w("This is much slower than forwards"); - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -99,7 +99,7 @@ public void testBackwards() throws IOException { @Test public void moveToPositionStart() throws IOException { - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -155,7 +155,7 @@ private void checkValueAtRowEqualsRowNumBackwards(SupportSQLiteDatabase db) { public void testCorruptionIsHandled() throws IOException { int elements = DatabaseIntegrationTests.DB_PAGE_NUM_INT_ELEMENTS; - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -193,7 +193,7 @@ public void smallQueryHasOneCount() throws IOException { int elements = 30; // 465 - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id varchar)"); @@ -219,7 +219,7 @@ public void smallQueryHasOneCount() throws IOException { public void variableLengthStringsReturnDifferentRowCounts() throws IOException { int elements = 50; // 1275 > 1000 - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id varchar)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java index 5e6aa96bd..be1f7575f 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java @@ -19,12 +19,11 @@ import androidx.annotation.NonNull; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.BackendFactory; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.RustBackendFailedException; -import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Before; import org.junit.runners.Parameterized; @@ -40,7 +39,7 @@ public class DatabaseComparison extends InstrumentedTest { @Parameterized.Parameters(name = "{0}") public static java.util.Collection initParameters() { // This does one run with schedVersion injected as 1, and one run as 2 - return Arrays.asList(new Object[][] { { DatabaseType.FRAMEWORK }, { DatabaseType.RUST } }); + return Arrays.asList(new Object[][]{{DatabaseType.FRAMEWORK}, {DatabaseType.RUST_LEGACY}, {DatabaseType.RUST_NEW}}); } @Before @@ -60,23 +59,17 @@ protected boolean handleSetupException(Exception e) { } protected SupportSQLiteDatabase getDatabase() { - SupportSQLiteOpenHelper.Configuration config = SupportSQLiteOpenHelper.Configuration.builder(getContext()) - .callback(new DefaultCallback()) - .name(getDatabasePath()) - .build(); - switch (schedVersion) { - case RUST: - BackendFactory mBackendFactory; - try { - mBackendFactory = BackendFactory.createInstance(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - // This throws on corruption - return new RustV11SQLiteOpenHelperFactory(mBackendFactory).create(config).getWritableDatabase(); + case RUST_LEGACY: + Backend backend = BackendFactory.getBackend(getContext(), Arrays.asList("en"), true); + backend.openCollection(getDatabasePath()); + return AnkiSupportSQLiteDatabase.withRustBackend(backend); + case RUST_NEW: + Backend backend2 = BackendFactory.getBackend(getContext(), Arrays.asList("en"), false); + backend2.openCollection(getDatabasePath()); + return AnkiSupportSQLiteDatabase.withRustBackend(backend2); case FRAMEWORK: - return new FrameworkSQLiteOpenHelperFactory().create(config).getWritableDatabase(); + return AnkiSupportSQLiteDatabase.withFramework(getContext(), getDatabasePath()); } throw new IllegalStateException(); } @@ -85,9 +78,13 @@ protected String getDatabasePath() { // TODO: look into this - null should work try { switch (schedVersion) { - case RUST: return ":memory:"; - case FRAMEWORK: return null; - default: return null; + case RUST_LEGACY: + case RUST_NEW: + return ":memory:"; + case FRAMEWORK: + return null; + default: + return null; } } catch (NullPointerException ex) { throw new IllegalStateException("Class is not annotated with @RunWith(Parameterized.class)", ex); @@ -96,11 +93,11 @@ protected String getDatabasePath() { public enum DatabaseType { FRAMEWORK, - RUST + RUST_LEGACY, + RUST_NEW, } - private static class DefaultCallback extends SupportSQLiteOpenHelper.Callback { public DefaultCallback() { super(1); diff --git a/rsdroid-testing/build.gradle b/rsdroid-testing/build.gradle index 8641b8fca..56cb1b041 100644 --- a/rsdroid-testing/build.gradle +++ b/rsdroid-testing/build.gradle @@ -15,6 +15,7 @@ */ apply plugin: 'java-library' +apply plugin: 'kotlin' apply plugin: 'signing' apply plugin: 'com.vanniktech.maven.publish' @@ -22,6 +23,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // obtaining the OS implementation 'org.apache.commons:commons-exec:1.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } jar { @@ -181,18 +183,22 @@ task copyWindowsOutput(type: Copy) { // TODO: check for cargo // check for targets: x86_64-apple-darwin, x86_64-pc-windows-gnu, TODO: Linux -processResources.dependsOn preBuildWindows -processResources.dependsOn copyWindowsOutput -// To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" -// execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu -// "linker `x86_64-unknown-linux-gnu-gcc` not found" -// sudo ln -s /usr/bin/cc /usr/local/bin/x86_64-unknown-linux-gnu-gcc - -// rustup target add x86_64-pc-windows-gnu --toolchain nightly -// brew install mingw-w64 && x86_64-w64-mingw32-gcc -v -processResources.dependsOn preBuildLinux -processResources.dependsOn copyLinuxOutput +Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" +if (wantAllPlatforms || org.gradle.internal.os.OperatingSystem.current().isWindows()) { + processResources.dependsOn preBuildWindows + processResources.dependsOn copyWindowsOutput +} +if (wantAllPlatforms || org.gradle.internal.os.OperatingSystem.current().isLinux()) { + // rustup target add x86_64-pc-windows-gnu --toolchain nightly + // brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + // To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" + // execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu + // "linker `x86_64-unknown-linux-gnu-gcc` not found" + // sudo ln -s /usr/bin/cc /usr/local/bin/x86_64-unknown-linux-gnu-gcc + processResources.dependsOn preBuildLinux + processResources.dependsOn copyLinuxOutput +} if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { // due to restrictions on downloading the MacOS SDK, we can only build for MacOS on MacOS // > Install a reasonable number of copies of the Apple Software on Apple-branded computers @@ -214,4 +220,22 @@ signing { logger.warn("$message: ${hasPrivate}, ${hasPassword}, ${pk == null || "" == pk}, ${pwd == null || "" == pwd}") } +} +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } } \ No newline at end of file diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java deleted file mode 100644 index 1d4140499..000000000 --- a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.testing; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; - -import org.apache.commons.exec.OS; - -/** - * Loads a librsdroid.so alternative to allow testing of rsdroid under a Robolectric-based environment - */ -public class RustBackendLoader { - - private static boolean alreadyLoaded; - private static final HashMap FILENAME_TO_PATH_CACHE = new HashMap<>(); - - public static boolean PRINT_DEBUG = false; - - /** - * Allows unit testing rsdroid under Robolectric
- * Loads (via {@link Runtime#load(String)}) a librsdroid.so alternative compiled for the current operating system.

- * - * This call is cached and is a no-op if called multiple times. - * - * @throws IllegalStateException OS is not Windows, Linux or macOS - * @throws RuntimeException Failure when extracting library to load - * @throws UnsatisfiedLinkError The library could not be loaded - */ - public static void init() { - if (!alreadyLoaded) { - - // This should help diagnose some issues, - print("loading rsdroid-testing for: " + System.getProperty("os.name")); - - if (OS.isFamilyWindows()) { - load("rsdroid", ".dll"); - } else if (OS.isFamilyMac()) { - load("librsdroid", ".dylib"); - } else if (OS.isFamilyUnix()) { - load("librsdroid", ".so"); - } else { - String osName = System.getProperty("os.name"); - throw new IllegalStateException(String.format("Could not determine OS Type for: '%s'", osName)); - } - - alreadyLoaded = true; - } - } - - private static void print(String message) { - if (PRINT_DEBUG) { - System.out.println(message); - } - } - - /** - * Allows unit testing rsdroid under Robolectric
- * Loads (via {@link Runtime#load(String)}) a librsdroid.so alternative compiled for the current operating system.

- * - * @param filePath A full path to the compiled .dll/.dylib/.so - */ - public static void loadRsdroid(String filePath) { - if (alreadyLoaded) { - return; - } - - loadPath(filePath); - alreadyLoaded = true; - } - - /** - * loads a named file in the jar via {@link Runtime#load(String)} - * - * @param fileName The name of the file in the jar - * @param extension The extension of the file in the jar - * - * @throws UnsatisfiedLinkError The library could not be loaded - * @throws RuntimeException Failure when extracting library to load - */ - private static void load(String fileName, String extension) { - String path; - try { - path = getPathFromResourceStream(fileName, extension); - } catch (IOException e) { - throw new RuntimeException(e); - } - - loadPath(path); - } - - private static void loadPath(String path) { - try { - Runtime.getRuntime().load(path); - } catch (UnsatisfiedLinkError e) { - if (!new File(path).exists()) { - FileNotFoundException exception = new FileNotFoundException("Extracted file was not found. Maybe the temp folder was deleted. Please try again: '" + path + "'"); - throw new RuntimeException(exception); - } - if (e.getMessage() == null || !e.getMessage().contains("already loaded in another classloader")) { - throw e; - } - } - } - - /** - * Extracts a named file from a JAR and saves it to a temp folder - * - * @param fileName The name of the file in the jar - * @param extension The extension of the file in the jar - * @return A path (on disk) to the extracted file from the JAR - * @throws IllegalStateException The named file did not exist in the jar. - * @throws IOException Error copying the file to the filesystem - */ - private static String getPathFromResourceStream(String fileName, String extension) throws IOException { - // TODO: Ensure that this is reasonably handled without too much copying. - // Note: this will leave some data in the temp folder. - String fullFilename = fileName + extension; - - // maintain a cache to the files so we reduce IO activity if a file has already been extracted. - if (FILENAME_TO_PATH_CACHE.containsKey(fullFilename)) { - return FILENAME_TO_PATH_CACHE.get(fullFilename); - } - - String path = File.createTempFile(fileName, extension).getAbsolutePath(); - File targetFile = new File(path); - - // If our temp file already exists, return it - // Likely a logical impossibility due to the implementation of createTempFile - if (targetFile.exists() && targetFile.length() > 0) { - return path; - } - - try (InputStream rsdroid = RustBackendLoader.class.getClassLoader().getResourceAsStream(fullFilename)) { - if (rsdroid == null) { - throw new IllegalStateException("Could not find " + fullFilename); - } - - try (OutputStream outStream = convertToOutputStream(targetFile)) { - byte[] buffer = new byte[8 * 1024]; - int bytesRead; - while ((bytesRead = rsdroid.read(buffer)) != -1) { - outStream.write(buffer, 0, bytesRead); - } - } - } - - FILENAME_TO_PATH_CACHE.put(fullFilename, path); - - return path; - } - - private static OutputStream convertToOutputStream(File targetFile) throws IOException { - OutputStream outStream; - try { - outStream = new FileOutputStream(targetFile); - } catch (Exception e) { - throw new IOException("Could not open output file: {}", e); - } - return outStream; - } -} \ No newline at end of file diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt new file mode 100644 index 000000000..cd06871c4 --- /dev/null +++ b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.testing + +import org.apache.commons.exec.OS +import java.io.* +import java.lang.IllegalStateException +import java.lang.RuntimeException +import java.security.MessageDigest +import java.util.HashMap +import kotlin.Throws + +/** + * Loads a librsdroid.so alternative to allow testing of rsdroid under a Robolectric-based environment + */ +object RustBackendLoader { + private var hasSetUp = false + private val FILENAME_TO_PATH_CACHE = HashMap() + var PRINT_DEBUG = false + + /** + * Allows unit testing rsdroid under Robolectric

+ * Loads (via [Runtime.load]) a librsdroid.so alternative compiled for the current operating system.



+ * + * This call is cached and is a no-op if called multiple times. + * + * Note the @Synchronized label is misleading - see the docs for loadPath() + * + * @throws IllegalStateException OS is not Windows, Linux or macOS + * @throws RuntimeException Failure when extracting library to load + * @throws UnsatisfiedLinkError The library could not be loaded + */ + @JvmStatic + @Synchronized + fun ensureSetup(customPath: String?) { + if (hasSetUp) { + return; + } + if (customPath != null) { + print("loading rsdroid-testing with path $customPath") + loadRsdroid(customPath) + } else { + // This should help diagnose some issues, + print("loading rsdroid-testing for: " + System.getProperty("os.name")) + if (OS.isFamilyWindows()) { + load("rsdroid", ".dll") + } else if (OS.isFamilyMac()) { + load("librsdroid", ".dylib") + } else if (OS.isFamilyUnix()) { + load("librsdroid", ".so") + } else { + val osName = System.getProperty("os.name") + throw IllegalStateException(String.format("Could not determine OS Type for: '%s'", osName)) + } + } + hasSetUp = true + } + + private fun print(message: String) { + if (PRINT_DEBUG) { + println(message) + } + } + + /** + * Allows unit testing rsdroid under Robolectric

+ * Loads (via [Runtime.load]) a librsdroid.so alternative compiled for the current operating system.



+ * + * @param filePath A full path to the compiled .dll/.dylib/.so + */ + private fun loadRsdroid(filePath: String) { + loadPath(filePath) + } + + /** + * loads a named file in the jar via [Runtime.load] + * + * @param fileName The name of the file in the jar + * @param extension The extension of the file in the jar + * + * @throws UnsatisfiedLinkError The library could not be loaded + * @throws RuntimeException Failure when extracting library to load + */ + private fun load(fileName: String, extension: String) { + val path = getPathFromResourceStream(fileName, extension) + loadPath(path) + } + + /** + * Subtle behaviour alert: while the routine that calls this is protected with a + * @Synchronized attribution, the lock it uses is based on the classloader that is + * active at the time. JUnit and Robolectric will alter the classloader for different tests + * (eg some do not use Robolectric's classloader at all, and other tests like BindingAndroidTest + * will use multiple classloader instances due to the use of @Config). This means this code + * is not guaranteed to execute only once, and after the first invocation, an "already loaded" + * error will be thrown by Java, which we have to swallow. + */ + private fun loadPath(path: String) { + try { + Runtime.getRuntime().load(path) + } catch (e: UnsatisfiedLinkError) { + if (!File(path).exists()) { + val exception = FileNotFoundException("Extracted file was not found. Maybe the temp folder was deleted. Please try again: '$path'") + throw RuntimeException(exception) + } + if (e.message == null || !e.message!!.contains("already loaded in another classloader")) { + throw e + } else { + // native library loaded by a different classloader in the same process + } + } + } + + /** + * Extracts a named file from a JAR and saves it to a temp folder + * + * @param fileName The name of the file in the jar + * @param extension The extension of the file in the jar + * @return A path (on disk) to the extracted file from the JAR + * @throws IllegalStateException The named file did not exist in the jar. + * @throws IOException Error copying the file to the filesystem + */ + @Throws(IOException::class) + private fun getPathFromResourceStream(fileName: String, extension: String): String { + // TODO: Ensure that this is reasonably handled without too much copying. + // Note: this will leave some data in the temp folder. + val fullFilename = fileName + extension + + // maintain a cache to the files so we reduce IO activity if a file has already been extracted. + if (FILENAME_TO_PATH_CACHE.containsKey(fullFilename)) { + return FILENAME_TO_PATH_CACHE[fullFilename]!! + } + + val buffer = ByteArray(8 * 1024) + val checksum = withStream(fullFilename) { stream -> + val digest = MessageDigest.getInstance("SHA-1") + var bytesRead: Int + while (stream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + digest.digest().joinToString("") { "%02x".format(it) } + } + val expectedFile = File(System.getProperty("java.io.tmpdir"), "$fileName-$checksum$extension") + if (!expectedFile.exists()) { + val tempFile = File.createTempFile(fileName, extension) + tempFile.outputStream().use { outStream -> + withStream(fullFilename) { inStream -> + var bytesRead: Int + while (inStream.read(buffer).also { bytesRead = it } != -1) { + outStream.write(buffer, 0, bytesRead) + } + } + outStream.flush() + outStream.close() + } + tempFile.renameTo(expectedFile) + } + FILENAME_TO_PATH_CACHE[fullFilename] = expectedFile.path + return expectedFile.absolutePath + } + + private fun withStream(fullFilename: String, func: (InputStream) -> T): T { + return func(RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename)) + } +} \ No newline at end of file diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index bc3a401ce..fd7bb4002 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.library' // required for aar generation to link to from AnkiDroid apply plugin: 'com.google.protobuf' +apply plugin: "kotlin-android" apply plugin: 'signing' apply plugin: 'com.vanniktech.maven.publish' @@ -40,6 +41,12 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + sourceSets { + main { + kotlin.srcDirs += "build/generated/source/fluent" + } + } } // Consider upgrade to DSL: https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block @@ -60,9 +67,10 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar", '*.so']) implementation "androidx.appcompat:appcompat:${rootProject.ext.appcompatVersion}" // Protobuf is part of the ABI, so include it as a compile/api dependency. - api "com.google.protobuf:protobuf-java:${rootProject.ext.protobufVersion}" + api "com.google.protobuf:protobuf-kotlin:${rootProject.ext.protobufVersion}" implementation "androidx.sqlite:sqlite:${rootProject.ext.sqliteVersion}" implementation 'com.jakewharton.timber:timber:5.0.1' + implementation 'androidx.sqlite:sqlite-framework:2.2.0' testImplementation 'junit:junit:4.13.2' testImplementation "org.robolectric:robolectric:4.8.1" @@ -72,7 +80,31 @@ dependencies { } -preBuild.dependsOn "cargoBuild" +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ["-opt-in=kotlin.RequiresOptIn"] + } +} + +task generateTranslations(type: Exec) { + workingDir "$rootDir" + String genPath = System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows') ? 'tools\\genfluent\\genfluent.bat' : 'tools/genfluent/genfluent.sh' + if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) { + commandLine 'cmd', '/c', genPath + } else { + commandLine 'sh', '-c', genPath + } +} + +preBuild.dependsOn "generateTranslations" + +Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" + +if (wantAllPlatforms) { + preBuild.dependsOn "cargoBuild" +} else { + preBuild.dependsOn "cargoBuildX86_64" +} signing { def hasPrivate = project.hasProperty('SIGNING_PRIVATE_KEY') @@ -87,4 +119,4 @@ signing { logger.warn("$message: ${hasPrivate}, ${hasPassword}, ${pk == null || "" == pk}, ${pwd == null || "" == pwd}") } -} \ No newline at end of file +} diff --git a/rsdroid/proto.gradle b/rsdroid/proto.gradle index 8ba0f056e..f988acf30 100644 --- a/rsdroid/proto.gradle +++ b/rsdroid/proto.gradle @@ -1,7 +1,6 @@ import org.gradle.internal.os.OperatingSystem def protobufFolder = new File(rootDir, "rslib-bridge/anki/proto").getAbsolutePath() -def droidProtobufFolder = new File(rootDir, "rslib-bridge/proto").getAbsolutePath() android { if (!new File(protobufFolder).exists()) { @@ -13,7 +12,6 @@ android { main { proto { srcDir protobufFolder - srcDir droidProtobufFolder } } } @@ -35,6 +33,7 @@ protobuf { } generateProtoTasks { all().each { task -> + task.outputs.upToDateWhen { false } task.builtins { java { // Options ref: @@ -45,6 +44,9 @@ protobuf { // which is confusing and of no use to us // option "lite" } + kotlin { + + } } task.plugins { anki { } diff --git a/rsdroid/src/main/AndroidManifest.xml b/rsdroid/src/main/AndroidManifest.xml index a768e28aa..e1b89d9ec 100644 --- a/rsdroid/src/main/AndroidManifest.xml +++ b/rsdroid/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java deleted file mode 100644 index 8dc54daac..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -public class AnkiDroidBackendImpl extends net.ankiweb.rsdroid.AdbackendImpl { - - private final PointerGen mPointerGen; - - public AnkiDroidBackendImpl(PointerGen pointerGen) { - this.mPointerGen = pointerGen; - } - - @Override - public Pointer ensureBackend() { - return mPointerGen.generatePointer(); - } - - @Override - protected byte[] executeCommand(long backendPointer, int command, byte[] args) { - return NativeMethods.executeAnkiDroidCommand(backendPointer, command, args); - } - - public void downgradeBackend(String collectionPath) { - String ret = NativeMethods.downgradeDatabase(collectionPath); - - if (ret != null && ret.length() != 0) { - throw new BackendException(ret); - } - - // otherwise, return - } - - public interface PointerGen { - Pointer generatePointer(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt new file mode 100644 index 000000000..83da1ba3d --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid + +import android.content.Context +import android.os.Looper +import androidx.annotation.CheckResult +import androidx.annotation.VisibleForTesting +import anki.ankidroid.DBResponse +import anki.backend.BackendError +import anki.backend.BackendInit +import anki.backend.GeneratedBackend +import anki.generic.Int64 +import com.google.protobuf.ByteString +import com.google.protobuf.InvalidProtocolBufferException +import net.ankiweb.rsdroid.database.NotImplementedException +import net.ankiweb.rsdroid.database.SQLHandler +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber +import java.io.Closeable +import java.io.File +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +open class Backend(val context: Context, langs: Iterable = listOf("en"), val legacySchema: Boolean = true) : GeneratedBackend(), SQLHandler, Closeable { + // Set on init; unset on .close(). Access via withBackend() + private var backendPointer: Long? = null + private val lock = ReentrantLock() + + val tr: Translations by lazy { + Translations(this) + } + + fun isOpen(): Boolean { + return backendPointer != null + } + + fun openCollection(collectionPath: String) { + val (mediaFolder, mediaDb) = if (legacySchema || collectionPath == ":memory:") { + listOf("", "") + } else { + listOf(collectionPath.replace(".anki2", ".media"), + collectionPath.replace(".anki2", ".media.db")) + } + checkMainThreadOp() + openCollection(collectionPath, mediaFolder, mediaDb, "", legacySchema) + } + + /** Forces a full media check on next sync. Only valid with new backend. */ + fun removeMediaDb(colPath: String) { + val file = File(colPath.replace(".anki2", ".media.db")) + if (file.exists()) { + file.delete() + } + } + + /** + * Open a backend instance, loading the shared library if not already loaded. + */ + init { + checkMainThreadOp() + Timber.d("Opening rust backend with lang=$langs") + NativeMethods.ensureSetup(context) + val input = BackendInit.newBuilder() + .addAllPreferredLangs(langs) + .build() + .toByteArray() + val outBytes = unpackResult(NativeMethods.openBackend(input)) + backendPointer = Int64.parseFrom(outBytes).`val` + } + + /** + * Close the backend, and any open collection. This object can not be used after this. + */ + override fun close() { + checkMainThreadOp() + Timber.d("Closing rust backend") + lock.withLock { + // Must be checked inside lock to avoid race + if (backendPointer != null) { + withBackend { + backendPointer = null + NativeMethods.closeBackend(it) + } + } + } + } + + /** + * Open a collection. There must not already be an open collection. + */ + override fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean) { + try { + super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) + } catch (exc: BackendException.BackendDbException) { + throw exc.toSQLiteException("db open") + } + } + + /** + * Closes an open collection. There must be an open collection. + */ + override fun closeCollection(downgradeToSchema11: Boolean) { + cancelAllProtoQueries() + super.closeCollection(downgradeToSchema11) + } + + /** + * All backend methods (except for backend init/close, and those explicitly + * excluded from the mutex) flow through this. + */ + override fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray { + checkMainThreadOp() + return withBackend { + unpackResult(NativeMethods.runMethodRaw(it, service, method, input)) + } + } + + /** + * Translations, progress fetching, and media sync do not require an outer lock. + */ + override fun runMethodRawNoLock(service: Int, method: Int, input: ByteArray): ByteArray { + if (backendPointer == null) { + throw BackendException("Backend has been closed") + } + return unpackResult(NativeMethods.runMethodRaw(backendPointer!!, service, method, input)) + } + + /** + * Run the provided closure with locked access to the backend. + * The backend maintains its own lock for backend commands, so this extra + * level of locks is only useful for executing begin+sql+commit/rollback commands + * without other commands being interleaved. When AnkiDroid has migrated to more + * of the backend, it can probably remove this and leave the transaction handling + * up to the backend. + */ + private fun withBackend(fn: (ptr: Long) -> T): T { + lock.withLock { + if (backendPointer == null) { + throw BackendException("Backend has been closed") + } + return fn(backendPointer!!) + } + } + + // transactions hold the lock until commit/rollback + + override fun beginTransaction() { + lock.lock() + performTransaction(DbRequestKind.Begin) + } + + override fun commitTransaction() { + try { + performTransaction(DbRequestKind.Commit) + } finally { + lock.unlock() + } + } + + override fun rollbackTransaction() { + try { + performTransaction(DbRequestKind.Rollback) + } finally { + lock.unlock() + } + } + + // other DB methods + + override fun closeDatabase() { + throw NotImplementedException("should close collection, not db") + } + + override fun getPath(): String? { + throw NotImplementedException() + } + + @CheckResult + override fun fullQuery(query: String, bindArgs: Array?): JSONArray { + return try { + fullQueryInternal(query, bindArgs) + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + @Throws(JSONException::class) + private fun fullQueryInternal(sql: String, bindArgs: Array?): JSONArray { + checkMainThreadSQL(sql) + val output = runDbCommand(dbRequestJson(sql, bindArgs)).toStringUtf8() + return JSONArray(output) + } + + override fun insertForId(sql: String, bindArgs: Array?): Long { + checkMainThreadSQL(sql) + return super.insertForId(dbRequestJson(sql, bindArgs)) + } + + override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { + checkMainThreadSQL(sql) + return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).toInt() + } + + /* Begin Protobuf-based database streaming methods (#6) */ + override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { + checkMainThreadSQL(query) + return runDbCommandProto(dbRequestJson(query, bindArgs)) + } + + override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { + return getNextResultPage(sequenceNumber, startIndex) + } + + override fun cancelCurrentProtoQuery(sequenceNumber: Int) { + flushQuery(sequenceNumber) + } + + override fun cancelAllProtoQueries() { + flushAllQueries() + } + + private fun performTransaction(kind: DbRequestKind) { + runDbCommand(dbRequestJson(kind = kind)) + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setPageSize(pageSizeBytes: Long) { + super.setPageSize(pageSizeBytes) + } + + override fun getColumnNames(sql: String): Array { + return getColumnNamesFromQuery(sql).toTypedArray() + } + + private fun checkMainThreadOp(sql: String? = null) { + checkMainThread { + val stackTraceElements = Thread.currentThread().stackTrace + val firstElem = stackTraceElements.filter { + val klass = it.className + for (text in listOf("rsdroid", "libanki", "java.lang", "dalvik", "anki.backend", + "DatabaseChangeDecorator")) { + if (text in klass) { + return@filter false + } + } + true + }.first() + Timber.w("Op on UI thread: %s", firstElem) + sql?.let { + Timber.w("%s", sql) + } + } + } + + + private fun checkMainThreadSQL(query: String) { + checkMainThreadOp(query) + } + + private fun checkMainThread(func: () -> Unit) { + try { + if (Looper.getMainLooper().isCurrentThread) { + func() + } + } catch (exc: NoSuchMethodError) { + // running outside Android, or old API + } + } +} + +/** + * Build a JSON DB request + */ +private fun dbRequestJson(sql: String = "", bindArgs: Array? = null, kind: DbRequestKind = DbRequestKind.Query, firstRowOnly: Boolean = false): ByteString { + val o = JSONObject() + o.put("kind", kind.name.lowercase()) + o.put("sql", sql) + o.put("args", JSONArray((bindArgs ?: arrayOf()).toList())) + o.put("first_row_only", firstRowOnly) + return ByteString.copyFromUtf8(o.toString()) +} + +enum class DbRequestKind { + Query, + Begin, + Commit, + Rollback, +} + +/** + * Unpack success/error tuple from backend, and throw if error. + */ +private fun unpackResult(result: Array?): ByteArray { + if (result == null) { + throw BackendException("null return from backend method") + } + val (successBytes, errorBytes) = result + if (errorBytes != null) { + // convert the error to an exception + val pbError: BackendError = try { + BackendError.parseFrom(errorBytes) + } catch (invalidProtocolBufferException: InvalidProtocolBufferException) { + throw BackendException.fromException(invalidProtocolBufferException) + } + print(pbError) + throw BackendException.fromError(pbError) + } else if (successBytes != null) { + return successBytes + } else { + // should not happen + throw BackendException("both ok & err cases null") + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java deleted file mode 100644 index 9b92b4d1f..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import android.database.sqlite.SQLiteConstraintException; -import android.database.sqlite.SQLiteDatabaseCorruptException; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteFullException; - -import androidx.annotation.Nullable; - -import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException; -import net.ankiweb.rsdroid.exceptions.BackendExistingException; -import net.ankiweb.rsdroid.exceptions.BackendInterruptedException; -import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; -import net.ankiweb.rsdroid.exceptions.BackendIoException; -import net.ankiweb.rsdroid.exceptions.BackendJsonException; -import net.ankiweb.rsdroid.exceptions.BackendNetworkException; -import net.ankiweb.rsdroid.exceptions.BackendNotFoundException; -import net.ankiweb.rsdroid.exceptions.BackendProtoException; -import net.ankiweb.rsdroid.exceptions.BackendSyncException; -import net.ankiweb.rsdroid.exceptions.BackendTemplateException; - -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import BackendProto.Backend; - -public class BackendException extends RuntimeException { - @SuppressWarnings({"unused", "RedundantSuppression"}) - @Nullable - private final Backend.BackendError error; - - public BackendException(Backend.BackendError error) { - super(error.getLocalized()); - this.error = error; - } - - public BackendException(String message) { - super(message); - error = null; - } - - public static BackendException fromError(Backend.BackendError error) { - switch (error.getValueCase()) { - case DB_ERROR: - return BackendDbException.fromDbError(error); - case JSON_ERROR: - return new BackendJsonException(error.getJsonError()); - case SYNC_ERROR: - return BackendSyncException.fromSyncError(error); - case FATAL_ERROR: - // This should have produced a hasFatalError property - throw new BackendFatalError(error.getFatalError()); - case EXISTS: - return new BackendExistingException(error); - case DECK_IS_FILTERED: - return new BackendDeckIsFilteredException(error); - case INTERRUPTED: - return new BackendInterruptedException(error); - case PROTO_ERROR: - return new BackendProtoException(error); - case NOT_FOUND_ERROR: - return new BackendNotFoundException(error); - case INVALID_INPUT: - return BackendInvalidInputException.fromInvalidInputError(error); - case NETWORK_ERROR: - return BackendNetworkException.fromNetworkError(error); - case TEMPLATE_PARSE: - return BackendTemplateException.fromTemplateError(error); - case IO_ERROR: - return new BackendIoException(error); - case VALUE_NOT_SET: - } - - - return new BackendException(error); - } - - public static RuntimeException fromException(Exception ex) { - return new RuntimeException(ex); - } - - - public RuntimeException toSQLiteException(String query) { - String message = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, this.getLocalizedMessage()); - return new SQLiteException(message, this); - } - - public static class BackendDbException extends BackendException { - - public BackendDbException(Backend.BackendError error) { - // This is very simple for now and matches Anki Desktop (error is currently text) - // Later on, we may want to use structured error messages - // DBError { info: "SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some(\"no such table: aa\"))", kind: Other } - super(error); - } - - public static BackendException fromDbError(Backend.BackendError error) { - - String localised = error.getLocalized(); - - if (localised == null) { - return new BackendDbException(error); - } - - if (localised.contains("kind: FileTooNew")) { - return new BackendDbFileTooNewException(error); - } - if (localised.contains("kind: FileTooOld")) { - return new BackendDbFileTooOldException(error); - } - if (localised.contains("kind: MissingEntity")) { - return new BackendDbMissingEntityException(error); - } - if (localised.contains("kind: Other")) { - return new BackendDbException(error); - } - // Anki already open, or media currently syncing. - if (localised.startsWith("Anki already open")) { - return new BackendDbLockedException(error); - } - - return new BackendDbException(error); - } - - @Override - public RuntimeException toSQLiteException(String query) { - String message = this.getLocalizedMessage(); - - if (message == null) { - String outMessage = String.format(Locale.ROOT, "Unknown error while compiling: \"%s\"", query); - throw new SQLiteException(outMessage, this); - } - - if (message.contains("InvalidParameterCount")) { - Matcher p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(this.getMessage()); - if (p.find()) { - int paramCount = Integer.parseInt(p.group(1)); - int index = Integer.parseInt(p.group(2)); - String errorMessage = String.format(Locale.ROOT, "Cannot bind argument at index %d because the index is out of range. The statement has %d parameters.", index, paramCount); - throw new IllegalArgumentException(errorMessage, this); - } - } else if (message.contains("ConstraintViolation")) { - throw new SQLiteConstraintException(message); - } else if (message.contains("DiskFull")) { - throw new SQLiteFullException(message); - } else if (message.contains("DatabaseCorrupt")) { - String outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message); - throw new SQLiteDatabaseCorruptException(outMessage); - } - - String outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message); - throw new SQLiteException(outMessage, this); - } - - public static class BackendDbFileTooNewException extends BackendException { - public BackendDbFileTooNewException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendDbFileTooOldException extends BackendException { - public BackendDbFileTooOldException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendDbLockedException extends BackendException { - public BackendDbLockedException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendDbMissingEntityException extends BackendException { - public BackendDbMissingEntityException(Backend.BackendError error) { - super(error); - } - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt new file mode 100644 index 000000000..eb0f44b74 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid + +import android.database.sqlite.SQLiteConstraintException +import android.database.sqlite.SQLiteDatabaseCorruptException +import android.database.sqlite.SQLiteException +import android.database.sqlite.SQLiteFullException +import anki.backend.BackendError +import net.ankiweb.rsdroid.exceptions.* +import net.ankiweb.rsdroid.exceptions.BackendSyncException.BackendSyncAuthFailedException +import java.util.* +import java.util.regex.Pattern + +open class BackendException : RuntimeException { + private val error: BackendError? + + constructor(error: BackendError) : super(error.localized) { + this.error = error + } + + constructor(message: String?) : super(message) { + error = null + } + + open fun toSQLiteException(query: String): RuntimeException { + val message = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, this.localizedMessage) + return SQLiteException(message, this) + } + + class BackendDbException(error: BackendError) : BackendException(error) { + override fun toSQLiteException(query: String): RuntimeException { + val message = this.localizedMessage + if (message == null) { + val outMessage = String.format(Locale.ROOT, "Unknown error while compiling: \"%s\"", query) + return SQLiteException(outMessage, this) + } + if (message.contains("InvalidParameterCount")) { + val p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(message) + if (p.find()) { + val givenParams = p.group(1)!!.toInt() + val expectedParams = p.group(2)!!.toInt() + val errorMessage = String.format(Locale.ROOT, "Cannot bind argument at index %d because the index is out of range. The statement has %d parameters.", givenParams, expectedParams) + return IllegalArgumentException(errorMessage, this) + } + } else if (message.contains("ConstraintViolation")) { + return SQLiteConstraintException(message) + } else if (message.contains("DiskFull")) { + return SQLiteFullException(message) + } else if (message.contains("DatabaseCorrupt")) { + val outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message) + return SQLiteDatabaseCorruptException(outMessage) + } + val outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message) + return SQLiteException(outMessage, this) + } + + class BackendDbFileTooNewException(error: BackendError) : BackendException(error) + class BackendDbFileTooOldException(error: BackendError) : BackendException(error) + class BackendDbLockedException(error: BackendError) : BackendException(error) + class BackendDbMissingEntityException(error: BackendError) : BackendException(error) + companion object { + fun fromDbError(error: BackendError): BackendException { + val localised = error.localized ?: return BackendDbException(error) + if (localised.contains("kind: FileTooNew")) { + return BackendDbFileTooNewException(error) + } + if (localised.contains("kind: FileTooOld")) { + return BackendDbFileTooOldException(error) + } + if (localised.contains("kind: MissingEntity")) { + return BackendDbMissingEntityException(error) + } + if (localised.contains("kind: Other")) { + return BackendDbException(error) + } + // Anki already open, or media currently syncing. + return if (localised.startsWith("Anki already open")) { + BackendDbLockedException(error) + } else BackendDbException(error) + } + } + } + + class BackendSearchException(error: BackendError) : BackendException(error) + class BackendFatalError(error: BackendError) : BackendException(error) + + companion object { + fun fromError(error: BackendError): BackendException { + when (error.kind!!) { + BackendError.Kind.DB_ERROR -> return BackendDbException.fromDbError(error) + BackendError.Kind.JSON_ERROR -> return BackendJsonException(error) + BackendError.Kind.SYNC_AUTH_ERROR -> return BackendSyncAuthFailedException(error) + BackendError.Kind.SYNC_OTHER_ERROR -> return BackendSyncException(error) + BackendError.Kind.FATAL_ERROR -> return BackendFatalError(error) + BackendError.Kind.EXISTS -> return BackendExistingException(error) + BackendError.Kind.FILTERED_DECK_ERROR -> return BackendDeckIsFilteredException(error) + BackendError.Kind.INTERRUPTED -> return BackendInterruptedException(error) + BackendError.Kind.PROTO_ERROR -> return BackendProtoException(error) + BackendError.Kind.NOT_FOUND_ERROR -> return BackendNotFoundException(error) + BackendError.Kind.INVALID_INPUT -> return BackendInvalidInputException.fromInvalidInputError(error) + BackendError.Kind.NETWORK_ERROR -> return BackendNetworkException(error) + BackendError.Kind.TEMPLATE_PARSE -> return BackendTemplateException.fromTemplateError(error) + BackendError.Kind.IO_ERROR -> return BackendIoException(error) + BackendError.Kind.SEARCH_ERROR -> return BackendSearchException(error) + + BackendError.Kind.UNDO_EMPTY -> return BackendException(error) + BackendError.Kind.CUSTOM_STUDY_ERROR -> return BackendException(error) + BackendError.Kind.IMPORT_ERROR -> return BackendException(error) + BackendError.Kind.DELETED -> return BackendException(error) + BackendError.Kind.CARD_TYPE_ERROR -> return BackendException(error) + BackendError.Kind.UNRECOGNIZED -> return BackendException(error) + } + } + + fun fromException(ex: Exception?): RuntimeException { + return RuntimeException(ex) + } + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java deleted file mode 100644 index 109807759..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -public abstract class BackendFactory { - - private BackendV1 backend; - - // Force users to go through getInstance - for now we need to handle the backend failure - protected BackendFactory() { - - } - - @RustCleanup("Use BackendV[11/Next]Factory") - public static BackendFactory createInstance() throws RustBackendFailedException { - return BackendV11Factory.createInstance(); - } - - public synchronized BackendV1 getBackend() { - if (backend == null) { - backend = new BackendMutex(new BackendV1Impl()); - } - return backend; - } - - public synchronized void closeCollection() { - if (backend == null) { - return; - } - - // we could swallow the exception here, most of the time it will be "collection is already closed" - backend.closeCollection(false); - } - - public abstract SupportSQLiteOpenHelper.Factory getSQLiteOpener(); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt new file mode 100644 index 000000000..7edccfe27 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package net.ankiweb.rsdroid + +import android.content.Context +import java.util.* + +typealias CustomBackendCreator = (context: Context, languages: Iterable, legacySchema: Boolean) -> Backend + +object BackendFactory { + /** + * If enabled, collections are upgraded to the latest schema version on open, and different + * code paths are used to access the collection, eg the major 'col' classes: models, decks, dconf, + * conf, tags are replaced with updated variants. + * + * UNSTABLE: DO NOT USE THIS ON A COLLECTION YOU CARE ABOUT. + */ + @JvmStatic + var defaultLegacySchema: Boolean = true + + /** + * The language(es) the backend uses for translations. + */ + var defaultLanguages: Iterable = listOf("en") + + @JvmStatic + private var backendForTesting: CustomBackendCreator? = null + + @JvmStatic + @JvmOverloads + fun getBackend(context: Context, languages: Iterable? = null, legacySchema: Boolean? = null): Backend { + val langs = languages ?: defaultLanguages + val legacy = legacySchema ?: defaultLegacySchema + return backendForTesting?.invoke(context, langs, legacy) ?: Backend( + context, + langs, + legacy + ) + } + + @JvmStatic + fun setDefaultLanguagesFromLocales(locales: Iterable) { + defaultLanguages = locales.map { localeToBackendCode(it) } + } + + private fun localeToBackendCode(locale: Locale): String { + // TODO: this needs checking that all language codes match the ones + // shown here: https://i18n.ankiweb.net/teams/ + return when (locale.language) { + Locale("heb").language -> "he" + Locale("yue").language -> "zh-TW" + Locale("ind").language -> "id" + Locale("tgl").language -> "tl" + else -> locale.language + } + } + + /** Allows overriding the returned backend for unit tests */ + fun setOverride(creator: CustomBackendCreator?) { + backendForTesting = creator + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java deleted file mode 100644 index 03f159411..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -/** A Java implementation of a rust "panic". - * - * rsdroid-bridge is not yet unwind-safe. If a panic occurs, we may be in an incoherent state - * - * But, we don't want the rust to panic. This causes a native exception, which will kill AnkiDroid - * before ACRA can send an exception report to the crash reporting server. - * - * This error delays the panic in a form that ACRA can catch, log, then crash more gracefully with. - */ -public class BackendFatalError extends Error { - public BackendFatalError(String message) { - super(message); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java deleted file mode 100644 index 879c072dd..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java +++ /dev/null @@ -1,1042 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.annotation.Nullable; - -import com.google.protobuf.ByteString; - -import org.json.JSONArray; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -import BackendProto.AdBackend; -import BackendProto.Backend; -import BackendProto.Sqlite; - -/** - * Ensures that a single thread accesses RustBackend at the same time. - * This is because rslib-bridge currently has no distinction between threads, and handles the state of - * transactions. - */ -public class BackendMutex implements BackendV1 { - // This class exists as the Rust backend uses a single connection for SQLite, rather than a connection pool - // This means that SQL can occur cross-threads. - // There are a few problems with this: - // * When inside a transaction, another thread can add commands, or close the transaction - // * Commands can either be sent from the Java, or from the Rust. - // * We have no knowledge about whether a Rust command will start a transaction - - // We handle this using a mutex and some invariants: - // * If a transaction is held by a thread, have the thread keep the mutex until the transaction is closed - // * Only one Rust command can run at a time - already true as with_col in rust uses a mutex, but we'll lock on the Java side - - private final ReentrantLock lock = new ReentrantLock(); - private final BackendV1 backend; - - public BackendMutex(BackendV1 backend) { - this.backend = backend; - } - - @Override - public void beginTransaction() { - lock.lock(); - backend.beginTransaction(); - } - - @Override - public void commitTransaction() { - try { - backend.commitTransaction(); - } finally { - lock.unlock(); - } - } - - @Override - public void rollbackTransaction() { - try { - backend.rollbackTransaction(); - } finally { - lock.unlock(); - } - } - - @Override - public JSONArray fullQuery(String query, Object... bindArgs) { - try { - lock.lock(); - return backend.fullQuery(query, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public int executeGetRowsAffected(String sql, Object... bindArgs) { - try { - lock.lock(); - return backend.executeGetRowsAffected(sql, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public long insertForId(String sql, Object... bindArgs) { - try { - lock.lock(); - return backend.insertForId(sql, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public String[] getColumnNames(String sql) { - try { - lock.lock(); - return backend.getColumnNames(sql); - } finally { - lock.unlock(); - } - } - - @Override - public void closeDatabase() { - try { - lock.lock(); - backend.closeDatabase(); - } finally { - lock.unlock(); - } - } - - @Override - public String getPath() { - try { - lock.lock(); - return backend.getPath(); - } finally { - lock.unlock(); - } - } - - @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - try { - lock.lock(); - return backend.getNextSlice(startIndex, sequenceNumber); - } finally { - lock.unlock(); - } - } - - @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { - try { - lock.lock(); - return backend.fullQueryProto(query, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - try { - lock.lock(); - backend.cancelCurrentProtoQuery(sequenceNumber); - } finally { - lock.unlock(); - } - } - - @Override - public void cancelAllProtoQueries() { - try { - lock.lock(); - backend.cancelAllProtoQueries(); - } finally { - lock.unlock(); - } - } - - @Override - public void setPageSize(long pageSizeBytes) { - try { - lock.lock(); - backend.setPageSize(pageSizeBytes); - } finally { - lock.unlock(); - } - } - - // RustBackend Implementation - - @Override - public Backend.Progress latestProgress() { - try { - lock.lock(); - return backend.latestProgress(); - } finally { - lock.unlock(); - } - } - - @Override - public void setWantsAbort() { - try { - lock.lock(); - backend.setWantsAbort(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ExtractAVTagsOut extractAVTags(@Nullable String text, boolean questionSide) { - try { - lock.lock(); - return backend.extractAVTags(text, questionSide); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ExtractLatexOut extractLatex(@Nullable String text, boolean svg, boolean expandClozes) { - try { - lock.lock(); - return backend.extractLatex(text, svg, expandClozes); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.EmptyCardsReport getEmptyCards() { - try { - lock.lock(); - return backend.getEmptyCards(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.RenderCardOut renderExistingCard(long cardId, boolean browser) { - try { - lock.lock(); - return backend.renderExistingCard(cardId, browser); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable ByteString template, boolean fillEmpty) { - try { - lock.lock(); - return backend.renderUncommittedCard(note, cardOrd, template, fillEmpty); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String stripAVTags(String args) { - try { - lock.lock(); - return backend.stripAVTags(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SearchCardsOut searchCards(@Nullable String search, @Nullable Backend.SortOrder order) { - try { - lock.lock(); - return backend.searchCards(search, order); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SearchNotesOut searchNotes(@Nullable String search) { - try { - lock.lock(); - return backend.searchNotes(search); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 findAndReplace(List nids, @Nullable String search, @Nullable String replacement, boolean regex, boolean matchCase, @Nullable String fieldName) { - try { - lock.lock(); - return backend.findAndReplace(nids, search, replacement, regex, matchCase, fieldName); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Int32 localMinutesWest(long args) { - try { - lock.lock(); - return backend.localMinutesWest(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setLocalMinutesWest(int args) { - try { - lock.lock(); - backend.setLocalMinutesWest(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SchedTimingTodayOut schedTimingToday() { - try { - lock.lock(); - return backend.schedTimingToday(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String studiedToday(int cards, double seconds) { - try { - lock.lock(); - return backend.studiedToday(cards, seconds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String congratsLearnMessage(float nextDue, int remaining) { - try { - lock.lock(); - return backend.congratsLearnMessage(nextDue, remaining); - } finally { - lock.unlock(); - } - } - - @Override - public void updateStats(long deckId, int newDelta, int reviewDelta, int millisecondDelta) { - try { - lock.lock(); - backend.updateStats(deckId, newDelta, reviewDelta, millisecondDelta); - } finally { - lock.unlock(); - } - } - - @Override - public void extendLimits(long deckId, int newDelta, int reviewDelta) { - try { - lock.lock(); - backend.extendLimits(deckId, newDelta, reviewDelta); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CountsForDeckTodayOut countsForDeckToday(long did) { - try { - lock.lock(); - return backend.countsForDeckToday(did); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String cardStats(long cid) { - try { - lock.lock(); - return backend.cardStats(cid); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.GraphsOut graphs(@Nullable String search, int days) { - try { - lock.lock(); - return backend.graphs(search, days); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CheckMediaOut checkMedia() { - try { - lock.lock(); - return backend.checkMedia(); - } finally { - lock.unlock(); - } - } - - @Override - public void trashMediaFiles(List fnames) { - try { - lock.lock(); - backend.trashMediaFiles(fnames); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String addMediaFile(@Nullable String desiredName, @Nullable ByteString data) { - try { - lock.lock(); - return backend.addMediaFile(desiredName, data); - } finally { - lock.unlock(); - } - } - - @Override - public void emptyTrash() { - try { - lock.lock(); - backend.emptyTrash(); - } finally { - lock.unlock(); - } - } - - @Override - public void restoreTrash() { - try { - lock.lock(); - backend.restoreTrash(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckID addOrUpdateDeckLegacy(@Nullable ByteString deck, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateDeckLegacy(deck, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckTreeNode deckTree(long now, long topDeckId) { - try { - lock.lock(); - return backend.deckTree(now, topDeckId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json deckTreeLegacy() { - try { - lock.lock(); - return backend.deckTreeLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getAllDecksLegacy() { - try { - lock.lock(); - return backend.getAllDecksLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckID getDeckIDByName(String name) { - try { - lock.lock(); - return backend.getDeckIDByName(name); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getDeckLegacy(long did) { - try { - lock.lock(); - return backend.getDeckLegacy(did); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckNames getDeckNames(boolean skipEmptyDefault, boolean includeFiltered) { - try { - lock.lock(); - return backend.getDeckNames(skipEmptyDefault, includeFiltered); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json newDeckLegacy(boolean args) { - try { - lock.lock(); - return backend.newDeckLegacy(args); - } finally { - lock.unlock(); - } - } - - @Override - public void removeDeck(long args) { - try { - lock.lock(); - backend.removeDeck(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckConfigID addOrUpdateDeckConfigLegacy(@Nullable ByteString config, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateDeckConfigLegacy(config, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json allDeckConfigLegacy() { - try { - lock.lock(); - return backend.allDeckConfigLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getDeckConfigLegacy(long dConfId) { - try { - lock.lock(); - return backend.getDeckConfigLegacy(dConfId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json newDeckConfigLegacy() { - try { - lock.lock(); - return backend.newDeckConfigLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public void removeDeckConfig(long dConfId) { - try { - lock.lock(); - backend.removeDeckConfig(dConfId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Card getCard(long cid) { - try { - lock.lock(); - return backend.getCard(cid); - } finally { - lock.unlock(); - } - } - - @Override - public void updateCard(Backend.Card args) { - try { - lock.lock(); - backend.updateCard(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CardID addCard(Backend.Card args) { - try { - lock.lock(); - return backend.addCard(args); - } finally { - lock.unlock(); - } - } - - @Override - public void removeCards(List cardIds) { - try { - lock.lock(); - backend.removeCards(cardIds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Note newNote(long noteTypidId) { - try { - lock.lock(); - return backend.newNote(noteTypidId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteID addNote(@Nullable Backend.Note note, long deckId) { - try { - lock.lock(); - return backend.addNote(note, deckId); - } finally { - lock.unlock(); - } - } - - @Override - public void updateNote(Backend.Note args) { - try { - lock.lock(); - backend.updateNote(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Note getNote(long nid) { - try { - lock.lock(); - return backend.getNote(nid); - } finally { - lock.unlock(); - } - } - - @Override - public void removeNotes(List noteIds, List cardIds) { - try { - lock.lock(); - backend.removeNotes(noteIds, cardIds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 addNoteTags(List nids, @Nullable String tags) { - try { - lock.lock(); - return backend.addNoteTags(nids, tags); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 updateNoteTags(List nids, @Nullable String tags, @Nullable String replacement, boolean regex) { - try { - lock.lock(); - return backend.updateNoteTags(nids, tags, replacement, regex); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ClozeNumbersInNoteOut clozeNumbersInNote(Backend.Note args) { - try { - lock.lock(); - return backend.clozeNumbersInNote(args); - } finally { - lock.unlock(); - } - } - - @Override - public void afterNoteUpdates(List nids, boolean markNotesModified, boolean generateCards) { - try { - lock.lock(); - backend.afterNoteUpdates(nids, markNotesModified, generateCards); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.FieldNamesForNotesOut fieldNamesForNotes(List nids) { - try { - lock.lock(); - return backend.fieldNamesForNotes(nids); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteIsDuplicateOrEmptyOut noteIsDuplicateOrEmpty(Backend.Note args) { - try { - lock.lock(); - return backend.noteIsDuplicateOrEmpty(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeID addOrUpdateNotetype(@Nullable ByteString json, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateNotetype(json, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getStockNotetypeLegacy(@Nullable Backend.StockNoteType kind) { - try { - lock.lock(); - return backend.getStockNotetypeLegacy(kind); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getNotetypeLegacy(long noteTypeId) { - try { - lock.lock(); - return backend.getNotetypeLegacy(noteTypeId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeNames getNotetypeNames() { - try { - lock.lock(); - return backend.getNotetypeNames(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeUseCounts getNotetypeNamesAndCounts() { - try { - lock.lock(); - return backend.getNotetypeNamesAndCounts(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeID getNotetypeIDByName(String name) { - try { - lock.lock(); - return backend.getNotetypeIDByName(name); - } finally { - lock.unlock(); - } - } - - @Override - public void removeNotetype(long noteTypeId) { - try { - lock.lock(); - backend.removeNotetype(noteTypeId); - } finally { - lock.unlock(); - } - } - - @Override - public void openCollection(@Nullable String collectionPath, @Nullable String mediaFolderPath, @Nullable String mediaDbPath, @Nullable String logPath) { - try { - lock.lock(); - backend.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath); - } finally { - lock.unlock(); - } - } - - @Override - public void closeCollection(boolean downgradeToSchema11) { - try { - lock.lock(); - backend.closeCollection(downgradeToSchema11); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CheckDatabaseOut checkDatabase() { - try { - lock.lock(); - return backend.checkDatabase(); - } finally { - lock.unlock(); - } - } - - @Override - public void beforeUpload() { - try { - lock.lock(); - backend.beforeUpload(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String translateString(Backend.TranslateStringIn args) { - try { - lock.lock(); - return backend.translateString(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String formatTimespan(float seconds, @Nullable Backend.FormatTimespanIn.Context context) { - try { - lock.lock(); - return backend.formatTimespan(seconds, context); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json i18nResources() { - try { - lock.lock(); - return backend.i18nResources(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Bool registerTags(@Nullable String tags, boolean preserveUsn, int usn, boolean clearFirst) { - try { - lock.lock(); - return backend.registerTags(tags, preserveUsn, usn, clearFirst); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.AllTagsOut allTags() { - try { - lock.lock(); - return backend.allTags(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getConfigJson(String args) { - try { - lock.lock(); - return backend.getConfigJson(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setConfigJson(@Nullable String key, @Nullable ByteString valueJson) { - try { - lock.lock(); - backend.setConfigJson(key, valueJson); - } finally { - lock.unlock(); - } - } - - @Override - public void removeConfig(String args) { - try { - lock.lock(); - backend.removeConfig(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setAllConfig(Backend.Json args) { - try { - lock.lock(); - backend.setAllConfig(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getAllConfig() { - try { - lock.lock(); - return backend.getAllConfig(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Preferences getPreferences() { - try { - lock.lock(); - return backend.getPreferences(); - } finally { - lock.unlock(); - } - } - - @Override - public void setPreferences(Backend.Preferences args) { - try { - lock.lock(); - backend.setPreferences(args); - } finally { - lock.unlock(); - } - } - - @Override - public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { - try { - lock.lock(); - backend.openAnkiDroidCollection(args); - } finally { - lock.unlock(); - } - } - - @Override - public boolean isOpen() { - try { - lock.lock(); - return backend.isOpen(); - } finally { - lock.unlock(); - } - } - - @Override - public void downgradeBackend(String collectionPath) throws BackendException { - try { - lock.lock(); - backend.downgradeBackend(collectionPath); - } finally { - lock.unlock(); - } - } - - @Override - public void close() throws IOException { - try { - lock.lock(); - backend.close(); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { - try { - lock.lock(); - return backend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { - try { - lock.lock(); - return backend.localMinutesWestLegacy(collectionCreationTime); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { - try { - lock.lock(); - return backend.debugActiveDatabaseSequenceNumbers(backendPtr); - } finally { - lock.unlock(); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java deleted file mode 100644 index 5ae8415bf..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.annotation.NonNull; - -import BackendProto.Backend; - -public class BackendUtils { - /** - * - * @throws android.database.sqlite.SQLiteDatabaseCorruptException If database is corrupt - */ - public static void openAnkiDroidCollection(BackendV1 backendV1, String path) { - backendV1.openAnkiDroidCollection(Backend.OpenCollectionIn.newBuilder().setCollectionPath(path).build()); - } - - @NonNull - public static String getAnkiCommitHash() { - return BuildConfig.ANKI_COMMIT_HASH; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt new file mode 100644 index 000000000..819a9d700 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid + +object BackendUtils { + val ankiCommitHash: String + get() = BuildConfig.ANKI_COMMIT_HASH +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java deleted file mode 100644 index 37fa681bc..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.ankiweb.rsdroid; - -import net.ankiweb.rsdroid.database.SQLHandler; - -import java.io.Closeable; - -import BackendProto.Backend; - -public interface BackendV1 extends SQLHandler, net.ankiweb.rsdroid.RustBackend, net.ankiweb.rsdroid.Adbackend, Closeable { - void openAnkiDroidCollection(Backend.OpenCollectionIn args) throws BackendException; - - boolean isOpen(); - - /** - * Downgrades the collection from Schema 16 to Schema 11 - * @param collectionPath The fully qualified path to collection.anki2 - * @throws BackendException The collection is not schema 16 - * @throws BackendException Collection is already open - */ - void downgradeBackend(String collectionPath) throws BackendException; -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java deleted file mode 100644 index 2f5837014..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory; - -public class BackendV11Factory extends BackendFactory { - - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - public static BackendV11Factory createInstance() throws RustBackendFailedException { - NativeMethods.ensureSetup(); - return new BackendV11Factory(); - } - - public SupportSQLiteOpenHelper.Factory getSQLiteOpener() { - return new RustV11SQLiteOpenHelperFactory(this); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java deleted file mode 100644 index 82b024b86..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.google.protobuf.InvalidProtocolBufferException; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.Closeable; -import java.io.File; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import BackendProto.AdBackend; -import BackendProto.Backend; -import BackendProto.Sqlite; -import timber.log.Timber; - -/** - * Do not use an instance of this class directly - it should be handled via BackendMutex - * All public methods should be accessed via interface - * */ -public class BackendV1Impl extends net.ankiweb.rsdroid.RustBackendImpl implements BackendV1, Closeable { - - /* - Note: java.lang.RuntimeException: com.google.protobuf.InvalidProtocolBufferException: Protocol message end-group tag did not match expected tag. - Implies that ? - */ - - private Pointer backEndPointer = null; - @Nullable - private String collectionPath; - private boolean isDisposed; - private final AnkiDroidBackendImpl ankiDroidBackend; - - // intentionally package private - use BackendFactory - BackendV1Impl() { - ankiDroidBackend = new AnkiDroidBackendImpl(this::ensureBackend); - } - - /** - * Obtains a pointer to the Rust backend. - * This pointer is to a structure containing environment-level data such as the - * optional collection reference, - * - * Java is responsible for disposing of this pointer, or a memory leak will occur. - * - * TODO: This defines the locales, logging and translation files. We do not use these yet. - * */ - @Override - @CheckResult - public Pointer ensureBackend() { - if (isDisposed) { - throw new BackendException("Backend has been closed"); - } - - if (backEndPointer == null) { - boolean isServer = false; - String langs = "en"; - String localeFolderPath = ""; - - Timber.i("Opening rust backend. Server: %b. Langs: '%s', path: %s", isServer, langs, localeFolderPath); - - Backend.BackendInit.Builder builder = Backend.BackendInit.newBuilder() - .setServer(isServer) - .addPreferredLangs(langs) - .setLocaleFolderPath(localeFolderPath); - - long backendPointer = NativeMethods.openBackend(builder.build().toByteArray()); - - backEndPointer = new Pointer(backendPointer); - } - return backEndPointer; - } - - @Override - protected byte[] executeCommand(long backendPointer, int command, byte[] args) { - return NativeMethods.executeCommand(backendPointer, command, args); - } - - @Override - public boolean isOpen() { - return backEndPointer != null; - } - - @Override - public void close() { - Timber.i("Closing rust backend"); - if (backEndPointer != null) { - try { - closeDatabase(); - } catch (BackendException ex) { - // Typically: CollectionNotOpen - Timber.w(ex, "Error while closing rust database"); - } - Timber.d("Executing close backend command"); - NativeMethods.closeBackend(backEndPointer.toJni()); - } - this.isDisposed = true; - backEndPointer = null; - } - - public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { - collectionPath = args.getCollectionPath(); - try { - Pointer backendPointer = ensureBackend(); - Timber.i("Opening Collection: '%s' '%s' '%s' '%s'", args.getCollectionPath(), args.getLogPath(), args.getMediaDbPath(), args.getMediaFolderPath()); - byte[] result = NativeMethods.openCollection(backendPointer.toJni(), args.toByteArray()); - Backend.Empty message = Backend.Empty.parseFrom(result); - validateMessage(result, message); - } catch (BackendException.BackendDbException ex) { - collectionPath = null; - throw ex.toSQLiteException("openAnkiDroidCollection"); - } catch (InvalidProtocolBufferException ex) { - collectionPath = null; - throw BackendException.fromException(ex); - } - } - - @CheckResult - public JSONArray fullQuery(String sql, @Nullable Object... args) { - try { - Timber.i("Rust: SQL query: '%s'", sql); - return fullQueryInternal(sql, args); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - private JSONArray fullQueryInternal(String sql, @Nullable Object[] args) throws JSONException { - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - - o.put("kind", "query"); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - o.put("first_row_only", false); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.fullDatabaseCommand(backend.toJni(), data); - - String json = new String(result); - - try { - return new JSONArray(json); - } catch (Exception e) { - Backend.BackendError pbError; - try { - pbError = Backend.BackendError.parseFrom(result); - } catch (InvalidProtocolBufferException invalidProtocolBufferException) { - throw BackendException.fromException(invalidProtocolBufferException); - } - throw BackendException.fromError(pbError); - } - } - - public long insertForId(String sql, Object[] args) { - try { - Timber.i("Rust: sql insert %s", sql); - - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.sqlInsertForId(backend.toJni(), data); - - Backend.Int64 message = Backend.Int64.parseFrom(result); - validateMessage(result, message); - return message.getVal(); - - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - throw BackendException.fromException(e); - } - } - - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - try { - Timber.i("Rust: executeGetRowsAffected %s", sql); - - List asList = bindArgs == null ? new ArrayList<>() : Arrays.asList(bindArgs); - JSONObject o = new JSONObject(); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.sqlQueryForAffected(backend.toJni(), data); - - Backend.Int32 message = Backend.Int32.parseFrom(result); - validateMessage(result, message); - return message.getVal(); - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - throw BackendException.fromException(e); - } - } - - /* Begin Protobuf-based database streaming methods (#6) */ - - @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... args) { - byte[] result = null; - try { - Timber.d("Rust: fullQueryProto %s", query); - - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - - o.put("kind", "query"); - o.put("sql", query); - o.put("args", new JSONArray(asList)); - o.put("first_row_only", false); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - result = NativeMethods.databaseCommand(backend.toJni(), data); - - Sqlite.DBResponse message = Sqlite.DBResponse.parseFrom(result); - validateMessage(result, message); - return message; - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - validateResult(result); - throw BackendException.fromException(e); - } - } - - @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - byte[] result = null; - try { - Timber.d("Rust: getNextSlice %d", startIndex); - - Pointer backend = ensureBackend(); - result = NativeMethods.databaseGetNextResultPage(backend.toJni(), sequenceNumber, startIndex); - - Sqlite.DBResponse message = Sqlite.DBResponse.parseFrom(result); - validateMessage(result, message); - return message; - } catch (InvalidProtocolBufferException e) { - validateResult(result); - throw BackendException.fromException(e); - } - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - Timber.d("cancelCurrentProtoQuery"); - NativeMethods.cancelCurrentProtoQuery(ensureBackend().toJni(), sequenceNumber); - } - - @Override - public void cancelAllProtoQueries() { - Timber.d("cancelAllProtoQueries"); - NativeMethods.cancelAllProtoQueries(ensureBackend().toJni()); - - } - - /* End protobuf-based database streaming methods */ - - public void beginTransaction() { - // Note: Casing is important here. - performTransaction("begin"); - } - - - public void commitTransaction() { - performTransaction("commit"); - } - - public void rollbackTransaction() { - performTransaction("rollback"); - } - - private void performTransaction(String kind) { - try { - Timber.i("Rust: transaction %s", kind); - - JSONObject o = new JSONObject(); - - o.put("kind", kind); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.fullDatabaseCommand(backend.toJni(), data); - - String asString = new String(result); - - if (!"null".equals(asString)) { - Backend.BackendError ex = Backend.BackendError.parseFrom(result); - throw BackendException.fromError(ex); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - public static void setPageSizeForTesting(long pageSizeInBytes) { - // TODO: Make this nonstatic - NativeMethods.setDbPageSize(pageSizeInBytes); - } - - @Override - public void setPageSize(long pageSizeInBytes) { - NativeMethods.setDbPageSize(pageSizeInBytes); - } - - @Override - public String[] getColumnNames(String sql) { - Timber.i("Rust: getColumnNames %s", sql); - return NativeMethods.getColumnNames(ensureBackend().toJni(), sql); - } - - @Override - public void closeCollection(boolean downgradeToSchema11) { - cancelAllProtoQueries(); - super.closeCollection(downgradeToSchema11); - } - - @Override - public void closeDatabase() { - closeCollection(false); - } - - @Override - public String getPath() { - return collectionPath; - } - - @SuppressWarnings("CharsetObjectCanBeUsed") - private byte[] jsonToBytes(JSONObject o) { - return o.toString().getBytes(Charset.forName("UTF-8")); - } - - @Override - public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { - return ankiDroidBackend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); - } - - @Override - public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { - return ankiDroidBackend.localMinutesWestLegacy(collectionCreationTime); - } - - @Override - @RustCleanup("Architecture - backendPtr param is not required") - public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { - return ankiDroidBackend.debugActiveDatabaseSequenceNumbers(ensureBackend().toJni()); - } - - @Override - public void downgradeBackend(String collectionPath) { - if (!new File(collectionPath).exists()) { - throw new BackendException(collectionPath + " not found"); - } - - ankiDroidBackend.downgradeBackend(collectionPath); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java deleted file mode 100644 index eee7b85b2..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.database.RustVNextSQLiteOpenHelperFactory; - -/** A factory for the latest version of the backend (v16 currently) */ -public class BackendVNextFactory extends BackendFactory { - - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - public static BackendVNextFactory createInstance() throws RustBackendFailedException { - NativeMethods.ensureSetup(); - return new BackendVNextFactory(); - } - - public SupportSQLiteOpenHelper.Factory getSQLiteOpener() { - return new RustVNextSQLiteOpenHelperFactory(this); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java deleted file mode 100644 index 50b6babe3..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java +++ /dev/null @@ -1,119 +0,0 @@ -package net.ankiweb.rsdroid; - -import android.annotation.SuppressLint; -import android.os.Build; - -import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import timber.log.Timber; - -public class NativeMethods { - - private static boolean hasSetUp = false; - private static RustBackendFailedException setupException; - - public static boolean isRoboUnitTest() { - return "robolectric".equals(Build.FINGERPRINT); - } - - - public static synchronized void ensureSetup() throws RustBackendFailedException { - if (hasSetUp) { - if (setupException != null) { - throw setupException; - } - return; - } - try { - System.loadLibrary("rsdroid"); - } catch (UnsatisfiedLinkError e) { - if (!isRoboUnitTest()) { - setupException = new RustBackendFailedException(e); - throw setupException; - } - // In Robolectric, assume setup works (setupException == null) if the library throws. - // As the library is loaded at a later time (or a failure will be quickly found). - } finally { - hasSetUp = true; - } - } - - public static byte[] executeCommand(long backendPointer, final int command, byte[] args) { - Timber.i("ExecuteCommand: %s", net.ankiweb.rsdroid.RustBackendMethods.commandName(command)); - return command(backendPointer, command, args); - } - - @CheckResult - private static native byte[] command(long backendPointer, final int command, byte[] args); - - @CheckResult - static native long openBackend(byte[] data); - - @SuppressLint("CheckResult") - static void execCommand(long backendPointer, final int command, byte[] args) { - command(backendPointer, command, args); - } - - @CheckResult - static native byte[] openCollection(long backendPointer, byte[] data); - - - /** Temporary: perform a database command and obtain the result as a JSON string without streaming. */ - @CheckResult - static native byte[] fullDatabaseCommand(long backendPointer, byte[] data); - - /** Input: JSON serialized request - * @return DbResult object */ - @CheckResult - static native byte[] databaseCommand(long backendPointer, byte[] data); - - /** Returns the next page of results after a databaseCommand. - * @return DbResult object */ - @CheckResult - static native byte[] databaseGetNextResultPage(long backendPointer, int sequenceNumber, long startIndex); - - /** Clears the memory from the current protobuf query. */ - static native int cancelCurrentProtoQuery(long backendPointer, int sequenceNumber); - - /** Clears the memory from the all protobuf queries. */ - static native void cancelAllProtoQueries(long backendPointer); - - /** - * Performs an insert and returns the last inserted row id. - * data: json encoded data - */ - @CheckResult - static native byte[] sqlInsertForId(long backendPointer, byte[] data); - - @CheckResult - static native byte[] sqlQueryForAffected(long backendPointer, byte[] data); - - @Nullable - @CheckResult - static native String[] getColumnNames(long backendPointer, String sql); - - static native long closeBackend(long backendPointer); - - static native byte[] executeAnkiDroidCommand(long backendPointer, int command, byte[] args); - - /** - * Sets the maximum number of bytes that a page of database results should return - * {@link net.ankiweb.rsdroid.database.StreamingProtobufSQLiteCursor} - */ - static native void setDbPageSize(long numberOfBytes); - - /** - * Produces all possible Rust-based errors. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - static native byte[] debugProduceError(long backendPointer, String command); - - /** - * - * @param path The path of the collection to downgrade - * @return Error message, or empty string for success - */ - static native String downgradeDatabase(String path); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt new file mode 100644 index 000000000..7b63d7896 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -0,0 +1,40 @@ +package net.ankiweb.rsdroid + +import android.content.Context +import android.os.Build +import android.system.Os +import androidx.annotation.CheckResult +import java.lang.RuntimeException + +object NativeMethods { + private var hasSetUp = false + val isRoboUnitTest: Boolean + get() = "robolectric" == Build.FINGERPRINT + + @JvmStatic + @Synchronized + fun ensureSetup(context: Context) { + if (hasSetUp) { + return + } + + if (!isRoboUnitTest) { + // Prevent sqlite throwing error 6410 due to the lack of /tmp + val dir = context.cacheDir + Os.setenv("TMPDIR", dir.path, false) + // Then load library + System.loadLibrary("rsdroid") + } else { + // Test harness will load the library for us. + } + + hasSetUp = true + } + + @CheckResult + external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray): Array? + + @CheckResult + external fun openBackend(data: ByteArray): Array? + external fun closeBackend(backendPointer: Long) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java deleted file mode 100644 index d2505209a..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.ankiweb.rsdroid; - -public class Pointer { - private final long pointer; - - - public Pointer(long backendPointer) { - pointer = backendPointer; - } - - - public long toJni() { - return pointer; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java deleted file mode 100644 index 47935ec91..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid; - -@RustV1Cleanup("This exists to force implementers to handle a `rsdroid failed to load` case" + - "as I do not trust our ~16k target devices will all export the appropriate" + - "functions allowing for rsdroid to be loaded." + - "This exists to ensure that there is a valid (working) fallback for V1 of the rust conversion" + - "Once we prove this to be incorrect (or fix the bugs), we could remove this and assume that" + - "rsdroid will always load without issue") -public class RustBackendFailedException extends Exception { - public RustBackendFailedException(Throwable error) { - super(error); - } - - @SuppressWarnings({"unused", "RedundantSuppression"}) - public RustBackendFailedException(String message) { - super(message); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt similarity index 72% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt index 501a45020..38950a4a3 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt @@ -13,21 +13,15 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +package net.ankiweb.rsdroid /** * Specifies that the provided class requires attention during the Rust conversion * These act as TODOs and should be audited before a production release is produced * After the Rust conversion is completed, this class should be deleted. */ -@Repeatable(RustCleanupCollection.class) -@Retention(RetentionPolicy.SOURCE) -public @interface RustCleanup { - /** Context and rationale for the cleanup, and the action which will be taken */ - String value(); -} +@JvmRepeatable(RustCleanupCollection::class) +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) +annotation class RustCleanup( + /** Context and rationale for the cleanup, and the action which will be taken */ + val value: String) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt similarity index 75% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt index 8b68b11d8..6a9f36e0f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt @@ -13,17 +13,11 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +package net.ankiweb.rsdroid /** A collection of RustCleanup attributes. Not to be used directly * Allows multiple instances of @RustCleanup on a class - * See {@link java.lang.annotation.Repeatable}. + * See [java.lang.annotation.Repeatable]. */ -@Retention(RetentionPolicy.SOURCE) -public @interface RustCleanupCollection { - RustCleanup[] value(); -} \ No newline at end of file +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) +annotation class RustCleanupCollection(vararg val value: RustCleanup) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt similarity index 81% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt index 2b5ad24c1..88a2ff5ce 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt @@ -13,11 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; +package net.ankiweb.rsdroid @RustCleanup("To be removed (or converted to RustV2Cleanup)") -public @interface RustV1Cleanup { - /** Context and rationale for the cleanup, and the action which will be taken */ - String value(); -} +annotation class RustV1Cleanup( + /** Context and rationale for the cleanup, and the action which will be taken */ + val value: String) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt new file mode 100644 index 000000000..f06c80b3a --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt @@ -0,0 +1,23 @@ +package net.ankiweb.rsdroid + +import anki.i18n.GeneratedTranslations +import anki.i18n.TranslateArgMap +import anki.i18n.TranslateStringRequest +import kotlin.Int +import kotlin.String +import anki.generic.String as GenericString + +// strip off unicode isolation markers from a translated string +// for testing purposes +fun String.withoutUnicodeIsolation(): String { + return this.replace("\u2068", "").replace("\u2069", "") +} + +class Translations(private val backend: Backend) : GeneratedTranslations { + + override fun translate(module: Int, translation: Int, args: TranslateArgMap): String { + val request = TranslateStringRequest.newBuilder().putAllArgs(args).setModuleIndex(module).setMessageIndex(translation).build() + val output = backend.translateStringRaw(request.toByteArray()) + return GenericString.parseFrom(output).`val` + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java deleted file mode 100644 index b529e6f5c..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import android.content.ContentResolver; -import android.database.CharArrayBuffer; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Bundle; - -import timber.log.Timber; - -/** - * Base class for all database cursors, abstracting database methods to a common interface - * Throwing on non-database-related cursor-methods - * - * This is useful because cursors are an android-specific implementation and not a database-specific - * implementation, and many of the methods are not relevant. - */ -public abstract class AnkiDatabaseCursor implements Cursor { - - @Override - public boolean isFirst() { - return getPosition() == 0; - } - - @Override - public boolean isBeforeFirst() { - return getPosition() < 0; - } - - @Override - public abstract int getCount(); - @Override - public abstract int getPosition(); - - @Override - public abstract int getColumnIndex(String columnName); - - @Override - public abstract int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException; - - @Override - public abstract String getColumnName(int columnIndex); - - @Override - public abstract String[] getColumnNames(); - - @Override - public abstract int getColumnCount(); - - @Override - public abstract String getString(int columnIndex); - - @Override - public abstract short getShort(int columnIndex); - - @Override - public abstract int getInt(int columnIndex); - - @Override - public abstract long getLong(int columnIndex); - - @Override - public abstract float getFloat(int columnIndex); - - @Override - public abstract double getDouble(int columnIndex); - - @Override - public abstract boolean isNull(int columnIndex); - - @Override - public abstract void close(); - - @Override - public abstract boolean isClosed(); - - @Override - public abstract int getType(int columnIndex); - - @Override - public byte[] getBlob(int columnIndex) { - throw new NotImplementedException(); - } - - @Override - public void setNotificationUri(ContentResolver cr, Uri uri) { - throw new NotImplementedException(); - } - - @Override - public void deactivate() { - Timber.w("deactivate - not implemented - throwing"); - throw new NotImplementedException(); - } - - @Override - public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { - throw new NotImplementedException(); - } - - @Override - public boolean requery() { - Timber.w("requery - not implemented - throwing"); - throw new NotImplementedException(); - } - - @Override - public Uri getNotificationUri() { - throw new NotImplementedException(); - } - - @Override - public boolean getWantsAllOnMoveCalls() { - return false; - } - - @Override - public void setExtras(Bundle extras) { - throw new NotImplementedException(); - } - - @Override - public Bundle getExtras() { - throw new NotImplementedException(); - } - - @Override - public Bundle respond(Bundle extras) { - throw new NotImplementedException(); - } - - @Override - public abstract boolean moveToPosition(int position); - - @Override - public void registerContentObserver(ContentObserver observer) { - Timber.w("Not implemented: registerContentObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void unregisterContentObserver(ContentObserver observer) { - Timber.w("Not implemented: unregisterContentObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - Timber.w("Not implemented: registerDataSetObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - Timber.w("Not implemented: unregisterDataSetObserver - shouldn't matter unless requery() is called"); - } - - - @Override - public boolean isLast() { - return getPosition() == getLastPosition(); - } - - @Override - public boolean isAfterLast() { - return getPosition() >= getCount(); - } - - @Override - public boolean move(int offset) { - return moveToPosition(getPosition() + offset); - } - - @Override - public boolean moveToLast() { - int toMoveTo = getLastPosition(); - return moveToPosition(toMoveTo); - } - - @Override - public boolean moveToFirst() { - return moveToPosition(0); - } - - @Override - public boolean moveToNext() { - int toMoveTo = getPosition() + 1; - return moveToPosition(toMoveTo); - } - - @Override - public boolean moveToPrevious() { - int toMoveTo = getPosition() - 1; - return moveToPosition(toMoveTo); - } - - protected int getLastPosition() { - return getCount() - 1; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt new file mode 100644 index 000000000..b29aff135 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.database + +import android.content.ContentResolver +import android.database.CharArrayBuffer +import android.database.ContentObserver +import android.database.Cursor +import android.database.DataSetObserver +import android.net.Uri +import android.os.Bundle +import timber.log.Timber + +/** + * Base class for all database cursors, abstracting database methods to a common interface + * Throwing on non-database-related cursor-methods + * + * This is useful because cursors are an android-specific implementation and not a database-specific + * implementation, and many of the methods are not relevant. + */ +abstract class AnkiDatabaseCursor : Cursor { + override fun isFirst(): Boolean { + return position == 0 + } + + override fun isBeforeFirst(): Boolean { + return position < 0 + } + + abstract override fun getCount(): Int + abstract override fun getPosition(): Int + abstract override fun getColumnIndex(columnName: String): Int + + @Throws(IllegalArgumentException::class) + abstract override fun getColumnIndexOrThrow(columnName: String): Int + abstract override fun getColumnName(columnIndex: Int): String + abstract override fun getColumnNames(): Array + abstract override fun getColumnCount(): Int + abstract override fun getString(columnIndex: Int): String? + abstract override fun getShort(columnIndex: Int): Short + abstract override fun getInt(columnIndex: Int): Int + abstract override fun getLong(columnIndex: Int): Long + abstract override fun getFloat(columnIndex: Int): Float + abstract override fun getDouble(columnIndex: Int): Double + abstract override fun isNull(columnIndex: Int): Boolean + abstract override fun close() + abstract override fun isClosed(): Boolean + abstract override fun getType(columnIndex: Int): Int + override fun getBlob(columnIndex: Int): ByteArray { + throw NotImplementedException() + } + + override fun setNotificationUri(cr: ContentResolver, uri: Uri) { + throw NotImplementedException() + } + + override fun deactivate() { + Timber.w("deactivate - not implemented - throwing") + throw NotImplementedException() + } + + override fun copyStringToBuffer(columnIndex: Int, buffer: CharArrayBuffer) { + throw NotImplementedException() + } + + override fun requery(): Boolean { + Timber.w("requery - not implemented - throwing") + throw NotImplementedException() + } + + override fun getNotificationUri(): Uri { + throw NotImplementedException() + } + + override fun getWantsAllOnMoveCalls(): Boolean { + return false + } + + override fun setExtras(extras: Bundle) { + throw NotImplementedException() + } + + override fun getExtras(): Bundle { + throw NotImplementedException() + } + + override fun respond(extras: Bundle): Bundle { + throw NotImplementedException() + } + + abstract override fun moveToPosition(nextPositionGlobal: Int): Boolean + override fun registerContentObserver(observer: ContentObserver) { + Timber.w("Not implemented: registerContentObserver - shouldn't matter unless requery() is called") + } + + override fun unregisterContentObserver(observer: ContentObserver) { + Timber.w("Not implemented: unregisterContentObserver - shouldn't matter unless requery() is called") + } + + override fun registerDataSetObserver(observer: DataSetObserver) { + Timber.w("Not implemented: registerDataSetObserver - shouldn't matter unless requery() is called") + } + + override fun unregisterDataSetObserver(observer: DataSetObserver) { + Timber.w("Not implemented: unregisterDataSetObserver - shouldn't matter unless requery() is called") + } + + override fun isLast(): Boolean { + return position == lastPosition + } + + override fun isAfterLast(): Boolean { + return position >= count + } + + override fun move(offset: Int): Boolean { + return moveToPosition(position + offset) + } + + override fun moveToLast(): Boolean { + val toMoveTo = lastPosition + return moveToPosition(toMoveTo) + } + + override fun moveToFirst(): Boolean { + return moveToPosition(0) + } + + override fun moveToNext(): Boolean { + val toMoveTo = position + 1 + return moveToPosition(toMoveTo) + } + + override fun moveToPrevious(): Boolean { + val toMoveTo = position - 1 + return moveToPosition(toMoveTo) + } + + protected val lastPosition: Int + get() = count - 1 +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java deleted file mode 100644 index 3716670ff..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import org.json.JSONArray; -import org.json.JSONException; - -public abstract class AnkiJsonDatabaseCursor extends AnkiDatabaseCursor { - - private final Session backend; - protected final String query; - protected final Object[] bindArgs; - protected JSONArray results; - private String[] columnMapping; - - - public AnkiJsonDatabaseCursor(Session backend, String query, Object[] bindArgs) { - this.backend = backend; - this.query = query; - this.bindArgs = bindArgs; - } - - // There's no need to close this cursor. - @Override - public boolean isClosed() { - return true; - } - - - @Override - public int getColumnCount() { - if (results.length() == 0) { - return 0; - } else { - try { - return results.getJSONArray(0).length(); - } catch (JSONException e) { - return 0; - } - } - } - - @Override - public String getString(int columnIndex) { - try { - return getRowAtCurrentPosition().getString(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - @Override - public short getShort(int columnIndex) { - try { - return (short) getRowAtCurrentPosition().getInt(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public int getInt(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0; - } - return rowAtCurrentPosition.getInt(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public long getLong(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0; - } - return rowAtCurrentPosition.getLong(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public float getFloat(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0.0f; - } - return (float) rowAtCurrentPosition.getDouble(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public double getDouble(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0.0f; - } - return rowAtCurrentPosition.getDouble(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean isNull(int columnIndex) { - try { - return getRowAtCurrentPosition().isNull(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public int getColumnIndex(String columnName) { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - return -1; - } - return -1; - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - throw new IllegalArgumentException(String.format("Could not find column '%s'", columnName)); - } - - @Override - public String getColumnName(int columnIndex) { - return getColumnNamesInternal()[columnIndex]; - } - - @Override - public String[] getColumnNames() { - return getColumnNamesInternal(); - } - - private String[] getColumnNamesInternal() { - if (columnMapping == null) { - columnMapping = backend.getColumnNames(query); - if (columnMapping == null) { - throw new IllegalStateException("unable to obtain column mapping"); - } - } - - return columnMapping; - } - - protected abstract JSONArray getRowAtCurrentPosition() throws JSONException; - - protected JSONArray fullQuery(String query, Object[] bindArgs) { - return backend.fullQuery(query, bindArgs); - } - - @Override - public int getType(int columnIndex) { - throw new NotImplementedException(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt new file mode 100644 index 000000000..24ae1b880 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 Ankitects Pty Ltd + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.database + +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import net.ankiweb.rsdroid.Backend + +/** + * Helper routines for constructing Rust-backed and Framework-backed + * SupportSQLiteDatabases. + */ +abstract class AnkiSupportSQLiteDatabase { + companion object { + /** + * Wrap a Rust backend connection (which provides an SQL interface). + * Caller is responsible for opening&closing the database. + */ + @JvmStatic + fun withRustBackend(backend: Backend): SupportSQLiteDatabase { + return RustSupportSQLiteDatabase(backend) + } + + /** + * Open a connection using the Android framework. + * If path is not provided, an in-memory database is opened. + */ + @JvmStatic + @JvmOverloads + fun withFramework(context: Context, path: String?, dbCallback: SupportSQLiteOpenHelper.Callback? = null): SupportSQLiteDatabase { + val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) + .name(path) + .callback(dbCallback ?: DefaultDbCallback(1)) + .build() + return FrameworkSQLiteOpenHelperFactory().create(configuration).writableDatabase + } + } + + open class DefaultDbCallback(version: Int) : SupportSQLiteOpenHelper.Callback(version) { + override fun onCreate(db: SupportSQLiteDatabase) {} + override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {} + override fun onCorruption(db: SupportSQLiteDatabase) {} + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java deleted file mode 100644 index 23b00cd2f..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -public class NotImplementedException extends RuntimeException { - - public NotImplementedException(String message) { - super(message); - } - - public NotImplementedException() { - - } - - /** A method which we should implement */ - public static NotImplementedException todo() { - return new NotImplementedException(); - } -} diff --git a/rsdroid/src/test/java/net/ankiweb/TestUtil.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt similarity index 67% rename from rsdroid/src/test/java/net/ankiweb/TestUtil.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt index d063ddb45..c2444bd50 100644 --- a/rsdroid/src/test/java/net/ankiweb/TestUtil.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt @@ -13,18 +13,16 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.database -package net.ankiweb; +class NotImplementedException : RuntimeException { + constructor(message: String?) : super(message) + constructor() -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.RustBackendFailedException; - -public class TestUtil { - public static BackendFactory getBackendFactory() { - try { - return BackendFactory.createInstance(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); + companion object { + /** A method which we should implement */ + fun todo(): NotImplementedException { + return NotImplementedException() } } -} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java deleted file mode 100644 index cc892e02e..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDoneException; - -import androidx.sqlite.db.SupportSQLiteStatement; - -import java.util.HashMap; -import java.util.Set; - -public class RustSQLiteStatement implements SupportSQLiteStatement { - private final RustSupportSQLiteDatabase database; - private final String sql; - - private final HashMap mBindings = new HashMap<>(); - - public RustSQLiteStatement(RustSupportSQLiteDatabase database, String sql) { - this.database = database; - this.sql = sql; - } - - @Override - public void execute() { - database.query(sql, getBindings()).close(); - } - - @Override - public int executeUpdateDelete() { - return database.executeGetRowsAffected(sql, getBindings()); - } - - @Override - public long executeInsert() { - return database.insertForForId(sql, getBindings()); - } - - @Override - public long simpleQueryForLong() { - try (Cursor query = database.query(sql, getBindings())) { - if (!query.moveToFirst()) { - throw new SQLiteDoneException(); - } - return query.getLong(0); - } - } - - @Override - public String simpleQueryForString() { - try (Cursor query = database.query(sql, getBindings())) { - if (!query.moveToFirst()) { - throw new SQLiteDoneException(); - } - return query.getString(0); - } - } - - @Override - public void bindNull(int index) { - bind(index, null); - } - - @Override - public void bindLong(int index, long value) { - bind(index, value); - } - - @Override - public void bindDouble(int index, double value) { - bind(index, value); - } - - @Override - public void bindString(int index, String value) { - bind(index, value); - } - - @Override - public void bindBlob(int index, byte[] value) { - bind(index, value); - } - - @Override - public void clearBindings() { - mBindings.clear(); - } - - @Override - public void close() { - - } - - - private void bind(int index, Object value) { - mBindings.put(index, value); - } - - Object[] getBindings() { - int max = max(mBindings.keySet()); - - Object[] ret = new Object[max + 1]; - for (int i = 0; i <= max; i++) { - if (mBindings.containsKey(i)) { - ret[i] = mBindings.get(i); - } - } - return ret; - } - - private int max(Set integerSet) { - int max = -1; - for (int i : integerSet) { - max = Math.max(max, i); - } - return max; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt new file mode 100644 index 000000000..53068f529 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.database + +import android.database.sqlite.SQLiteDoneException +import androidx.sqlite.db.SupportSQLiteStatement + +class RustSQLiteStatement(private val database: RustSupportSQLiteDatabase, private val sql: String) : SupportSQLiteStatement { + private val mBindings = HashMap() + override fun execute() { + database.query(sql, bindings).close() + } + + override fun executeUpdateDelete(): Int { + return database.executeGetRowsAffected(sql, bindings) + } + + override fun executeInsert(): Long { + return database.insertForForId(sql, bindings) + } + + override fun simpleQueryForLong(): Long { + database.query(sql, bindings).use { query -> + if (!query.moveToFirst()) { + throw SQLiteDoneException() + } + return query.getLong(0) + } + } + + override fun simpleQueryForString(): String { + database.query(sql, bindings).use { query -> + if (!query.moveToFirst()) { + throw SQLiteDoneException() + } + return query.getString(0) + } + } + + override fun bindNull(index: Int) { + bind(index, null) + } + + override fun bindLong(index: Int, value: Long) { + bind(index, value) + } + + override fun bindDouble(index: Int, value: Double) { + bind(index, value) + } + + override fun bindString(index: Int, value: String) { + bind(index, value) + } + + override fun bindBlob(index: Int, value: ByteArray) { + bind(index, value) + } + + override fun clearBindings() { + mBindings.clear() + } + + override fun close() {} + private fun bind(index: Int, value: Any?) { + mBindings[index] = value + } + + val bindings: Array + get() { + val max = max(mBindings.keys) + val ret = arrayOfNulls(max + 1) + for (i in 0..max) { + if (mBindings.containsKey(i)) { + ret[i] = mBindings[i] + } + } + return ret + } + + private fun max(integerSet: Set): Int { + var max = -1 + for (i in integerSet) { + max = kotlin.math.max(max, i) + } + return max + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java deleted file mode 100644 index 57e15c38d..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - * - * Contains code under the following license - * - * Copyright (C) 2006 The Android Open Source Project - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * from android.database.sqlite.SQLiteStatement - * from update/insert/delete - */ - -package net.ankiweb.rsdroid.database; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteTransactionListener; -import android.os.CancellationSignal; -import android.util.Pair; - -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteQuery; -import androidx.sqlite.db.SupportSQLiteStatement; - -import net.ankiweb.rsdroid.BackendV1; - -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static android.text.TextUtils.isEmpty; - -public class RustSupportSQLiteDatabase implements SupportSQLiteDatabase { - private static final String[] CONFLICT_VALUES = new String[] - {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; - - private final ThreadLocal sessionFactory; - private final boolean isReadOnly; - private boolean isOpen; - - public RustSupportSQLiteDatabase(BackendV1 backend, boolean readOnly) { - if (backend == null) { - throw new IllegalArgumentException("backend was null"); - } - this.sessionFactory = new SessionThreadLocal(backend); - this.isReadOnly = readOnly; - this.isOpen = true; - } - - @Override - public boolean isReadOnly() { - return isReadOnly; - } - - @Override - public boolean isOpen() { - return isOpen; - } - - @Override - public SupportSQLiteStatement compileStatement(String sql) { - return new RustSQLiteStatement(this, sql); - } - - @Override - public void beginTransaction() { - getSession().beginTransaction(); - } - - @Override - public void endTransaction() { - getSession().endTransaction(); - } - - @Override - public void setTransactionSuccessful() { - getSession().setTransactionSuccessful(); - } - - @Override - public boolean inTransaction() { - return getSession().inTransaction(); - } - - @Override - public int getVersion() { - throw NotImplementedException.todo(); - } - - @Override - public void setVersion(int version) { - throw NotImplementedException.todo(); - } - - @Override - public Cursor query(String query) { - return query(query, null); - } - - @Override - public Cursor query(String query, Object[] bindArgs) { - return new StreamingProtobufSQLiteCursor(getSession(), query, bindArgs); - } - - - @Override - public Cursor query(SupportSQLiteQuery query) { - throw NotImplementedException.todo(); - } - - @Override - public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) { - throw NotImplementedException.todo(); - } - - @Override - public long insert(String table, int conflictAlgorithm, ContentValues values) throws SQLException { - StringBuilder sql = new StringBuilder(); - sql.append("INSERT"); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(" INTO "); - sql.append(table); - sql.append('('); - - Object[] bindArgs = null; - int size = (values != null && values.size() > 0) - ? values.size() : 0; - if (size > 0) { - bindArgs = new Object[size]; - int i = 0; - for (Map.Entry entry : values.valueSet()) { - sql.append((i > 0) ? "," : ""); - sql.append(entry.getKey()); - bindArgs[i++] = entry.getValue(); - } - sql.append(')'); - sql.append(" VALUES ("); - for (i = 0; i < size; i++) { - sql.append((i > 0) ? ",?" : "?"); - } - } else { - sql.append((String) null).append(") VALUES (NULL"); - } - sql.append(')'); - - query(sql.toString(), bindArgs).close(); - return 0; - } - - @Override - public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause, Object[] whereArgs) { - // taken from SQLiteDatabase class. - if (values == null || values.size() == 0) { - throw new IllegalArgumentException("Empty values"); - } - StringBuilder sql = new StringBuilder(120); - sql.append("UPDATE "); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(table); - sql.append(" SET "); - - // move all bind args to one array - int setValuesSize = values.size(); - int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); - Object[] bindArgs = new Object[bindArgsSize]; - int i = 0; - for (String colName : values.keySet()) { - sql.append((i > 0) ? "," : ""); - sql.append(colName); - bindArgs[i++] = values.get(colName); - sql.append("=?"); - } - if (whereArgs != null) { - for (i = setValuesSize; i < bindArgsSize; i++) { - bindArgs[i] = whereArgs[i - setValuesSize]; - } - } - if (!isEmpty(whereClause)) { - sql.append(" WHERE "); - sql.append(whereClause); - } - - return this.executeGetRowsAffected(sql.toString(), bindArgs); - } - - @Override - public void execSQL(String sql) throws SQLException { - execSQL(sql, null); - } - - @Override - public void execSQL(String sql, Object[] bindArgs) throws SQLException { - query(sql, bindArgs).close(); - } - - @Override - public boolean needUpgrade(int newVersion) { - // needed for metaDB, but not for Anki DB - throw NotImplementedException.todo(); - } - - @Override - public String getPath() { - return getSession().getPath(); - } - - - @Override - public boolean isWriteAheadLoggingEnabled() { - return false; - } - - @Override - public void disableWriteAheadLogging() { - // Nothing to do - openAnkiDroidCollection does this - } - - @Override - public boolean isDatabaseIntegrityOk() { - Cursor pragma_integrity_check = query("pragma integrity_check"); - if (!pragma_integrity_check.moveToFirst()) { - return false; - } - String value = pragma_integrity_check.getString(0); - return "ok".equals(value); - } - - @Override - public void close() { - isOpen = false; - getSession().closeDatabase(); - } - - /* Not part of interface */ - - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - try { - return getSession().executeGetRowsAffected(sql, bindArgs); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public long insertForForId(String sql, Object[] bindArgs) { - try { - return getSession().insertForId(sql, bindArgs); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** Helper methods */ - - private Session getSession() { - return sessionFactory.get(); - } - - /** Confirmed that the below are not used for our code */ - - - @Override - public int delete(String table, String whereClause, Object[] whereArgs) { - // Complex to implement and not required - throw new NotImplementedException(); - } - - @Override - public boolean isDbLockedByCurrentThread() { - throw new NotImplementedException(); - } - - @Override - public boolean yieldIfContendedSafely() { - throw new NotImplementedException(); - } - - @Override - public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { - throw new NotImplementedException(); - } - - @Override - public void setLocale(Locale locale) { - throw new NotImplementedException(); - } - - @Override - public void setMaxSqlCacheSize(int cacheSize) { - throw new NotImplementedException(); - } - - @Override - public long getMaximumSize() { - throw new NotImplementedException(); - } - - @Override - public long setMaximumSize(long numBytes) { - throw new NotImplementedException(); - } - - @Override - public long getPageSize() { - throw new NotImplementedException(); - } - - @Override - public void setPageSize(long numBytes) { - throw new NotImplementedException(); - } - - - @Override - public void setForeignKeyConstraintsEnabled(boolean enable) { - throw new NotImplementedException(); - } - - - @Override - public boolean enableWriteAheadLogging() { - throw new NotImplementedException(); - } - - @Override - public List> getAttachedDbs() { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionNonExclusive() { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener) { - throw new NotImplementedException(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt new file mode 100644 index 000000000..c7fcf00c1 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * Contains code under the following license + * + * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * from android.database.sqlite.SQLiteStatement + * from update/insert/delete + */ +package net.ankiweb.rsdroid.database + +import android.content.ContentValues +import android.database.Cursor +import android.database.SQLException +import android.database.sqlite.SQLiteTransactionListener +import android.os.CancellationSignal +import android.text.TextUtils +import android.util.Pair +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteStatement +import net.ankiweb.rsdroid.Backend +import java.util.* + +/** + * A wrapper that allows the Rust backend to act like a standard Android + * database. Key differences: + * - the backend must be instructed to open the collection by the caller + * - .close() is a no-op - you should call .close() on the collection (or + * backend directly in testing) instead + * - isOpen and getPath return dummy data + */ +class RustSupportSQLiteDatabase(backend: Backend) : SupportSQLiteDatabase { + private val sessionFactory: ThreadLocal + + override fun isReadOnly(): Boolean { + return false + } + + override fun isOpen(): Boolean { + return true + } + + override fun compileStatement(sql: String): SupportSQLiteStatement { + return RustSQLiteStatement(this, sql) + } + + override fun beginTransaction() { + session.beginTransaction() + } + + override fun endTransaction() { + session.endTransaction() + } + + override fun setTransactionSuccessful() { + session.setTransactionSuccessful() + } + + override fun inTransaction(): Boolean { + return session.inTransaction() + } + + override fun getVersion(): Int { + throw NotImplementedException.todo() + } + + override fun setVersion(version: Int) { + throw NotImplementedException.todo() + } + + override fun query(query: String): Cursor { + return query(query, null) + } + + override fun query(query: String, bindArgs: Array?): Cursor { + return StreamingProtobufSQLiteCursor(session, query, bindArgs) + } + + override fun query(query: SupportSQLiteQuery): Cursor { + throw NotImplementedException.todo() + } + + override fun query(query: SupportSQLiteQuery, cancellationSignal: CancellationSignal): Cursor { + throw NotImplementedException.todo() + } + + @Throws(SQLException::class) + override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long { + val sql = StringBuilder() + sql.append("INSERT") + sql.append(CONFLICT_VALUES[conflictAlgorithm]) + sql.append(" INTO ") + sql.append(table) + sql.append('(') + var bindArgs: Array? = null + val size = values.size() + if (size > 0) { + bindArgs = arrayOfNulls(size) + var i = 0 + for ((key, value) in values.valueSet()) { + sql.append(if (i > 0) "," else "") + sql.append(key) + bindArgs[i++] = value + } + sql.append(')') + sql.append(" VALUES (") + i = 0 + while (i < size) { + sql.append(if (i > 0) ",?" else "?") + i++ + } + } else { + sql.append(null as String?).append(") VALUES (NULL") + } + sql.append(')') + query(sql.toString(), bindArgs).close() + return 0 + } + + override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String?, whereArgs: Array?): Int { + // taken from SQLiteDatabase class. + require(values.size() > 0) { "Empty values" } + val sql = StringBuilder(120) + sql.append("UPDATE ") + sql.append(CONFLICT_VALUES[conflictAlgorithm]) + sql.append(table) + sql.append(" SET ") + + // move all bind args to one array + val setValuesSize = values.size() + val bindArgsSize = if (whereArgs == null) setValuesSize else setValuesSize + whereArgs.size + val bindArgs = arrayOfNulls(bindArgsSize) + var i = 0 + for (colName in values.keySet()) { + sql.append(if (i > 0) "," else "") + sql.append(colName) + bindArgs[i++] = values[colName] + sql.append("=?") + } + if (whereArgs != null) { + i = setValuesSize + while (i < bindArgsSize) { + bindArgs[i] = whereArgs[i - setValuesSize] + i++ + } + } + if (!TextUtils.isEmpty(whereClause)) { + sql.append(" WHERE ") + sql.append(whereClause) + } + return executeGetRowsAffected(sql.toString(), bindArgs) + } + + @Throws(SQLException::class) + override fun execSQL(sql: String) { + execSQL(sql, null) + } + + @Throws(SQLException::class) + override fun execSQL(sql: String, bindArgs: Array?) { + query(sql, bindArgs).close() + } + + override fun needUpgrade(newVersion: Int): Boolean { + // needed for metaDB, but not for Anki DB + throw NotImplementedException.todo() + } + + override fun getPath(): String { + return "" + } + + override fun isWriteAheadLoggingEnabled(): Boolean { + return false + } + + override fun disableWriteAheadLogging() { + // Nothing to do - openAnkiDroidCollection does this + } + + override fun isDatabaseIntegrityOk(): Boolean { + val pragmaIntegrityCheck = query("pragma integrity_check") + if (!pragmaIntegrityCheck.moveToFirst()) { + return false + } + val value = pragmaIntegrityCheck.getString(0) + return "ok" == value + } + + override fun close() { + // no-op + } + + /* Not part of interface */ + fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { + return try { + session.executeGetRowsAffected(sql, bindArgs) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + fun insertForForId(sql: String, bindArgs: Array?): Long { + return try { + session.insertForId(sql, bindArgs) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + /** Helper methods */ + private val session: Session + get() = sessionFactory.get()!! + + /** Confirmed that the below are not used for our code */ + override fun delete(table: String, whereClause: String, whereArgs: Array): Int { + // Complex to implement and not required + throw NotImplementedException() + } + + override fun isDbLockedByCurrentThread(): Boolean { + throw NotImplementedException() + } + + override fun yieldIfContendedSafely(): Boolean { + throw NotImplementedException() + } + + override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean { + throw NotImplementedException() + } + + override fun setLocale(locale: Locale) { + throw NotImplementedException() + } + + override fun setMaxSqlCacheSize(cacheSize: Int) { + throw NotImplementedException() + } + + override fun getMaximumSize(): Long { + throw NotImplementedException() + } + + override fun setMaximumSize(numBytes: Long): Long { + throw NotImplementedException() + } + + override fun getPageSize(): Long { + throw NotImplementedException() + } + + override fun setPageSize(numBytes: Long) { + throw NotImplementedException() + } + + override fun setForeignKeyConstraintsEnabled(enable: Boolean) { + throw NotImplementedException() + } + + override fun enableWriteAheadLogging(): Boolean { + throw NotImplementedException() + } + + override fun getAttachedDbs(): List> { + throw NotImplementedException() + } + + override fun beginTransactionNonExclusive() { + throw NotImplementedException() + } + + override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) { + throw NotImplementedException() + } + + override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLiteTransactionListener) { + throw NotImplementedException() + } + + companion object { + private val CONFLICT_VALUES = arrayOf("", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE ") + } + + init { + sessionFactory = SessionThreadLocal(backend) + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java deleted file mode 100644 index eefc0d888..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendV1; - -public abstract class RustSupportSQLiteOpenHelper implements SupportSQLiteOpenHelper { - @Nullable - protected final Configuration configuration; - @Nullable - protected final BackendV1 backend; - protected BackendFactory backendFactory; - protected SupportSQLiteDatabase database; - - public RustSupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - this.configuration = configuration; - this.backendFactory = backendFactory; - this.backend = null; - } - - public RustSupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - if (!backend.isOpen()) { - throw new IllegalStateException("Backend should be open"); - } - this.backend = backend; - configuration = null; - } - - @Nullable - @Override - public String getDatabaseName() { - if (backend != null) { - return backend.getPath(); - } else if (configuration != null) { - return configuration.name; - } else { - throw new IllegalStateException("Class invalid: no config or backend"); - } - } - - @Override - public void setWriteAheadLoggingEnabled(boolean enabled) { - throw new NotImplementedException(); - } - - @Override - public SupportSQLiteDatabase getWritableDatabase() { - if (database == null) { - this.database = createRustSupportSQLiteDatabase(false); - } - return database; - } - - @Override - public SupportSQLiteDatabase getReadableDatabase() { - throw new NotImplementedException("Not supported by Rust - requires open collection"); - } - - @Override - public void close() { - - } - - protected abstract SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java deleted file mode 100644 index 659c2cf95..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; - -/** - * Implementation of {@link SupportSQLiteOpenHelper.Factory} using the Anki Desktop backend - */ -public class RustV11SQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { - private final BackendFactory backendFactory; - - public RustV11SQLiteOpenHelperFactory(BackendFactory backendFactory) { - this.backendFactory = backendFactory; - } - - @NonNull - @Override - public SupportSQLiteOpenHelper create(@NonNull SupportSQLiteOpenHelper.Configuration configuration) { - return new RustV11SupportSQLiteOpenHelper(configuration, backendFactory); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java deleted file mode 100644 index 179fe2618..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendUtils; -import net.ankiweb.rsdroid.BackendV1; - -import timber.log.Timber; - -public class RustV11SupportSQLiteOpenHelper extends RustSupportSQLiteOpenHelper { - - public RustV11SupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - super(configuration, backendFactory); - } - - public RustV11SupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - super(backend); - } - - @Override - protected SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly) { - Timber.d("createRustSupportSQLiteDatabase"); - if (configuration != null) { - BackendV1 backend = backendFactory.getBackend(); - BackendUtils.openAnkiDroidCollection(backend, configuration.name); - return new RustSupportSQLiteDatabase(backend, readOnly); - } else { - return new RustSupportSQLiteDatabase(backend, readOnly); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java deleted file mode 100644 index 8cb335684..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; - -/** - * Implementation of {@link SupportSQLiteOpenHelper.Factory} using the Anki Desktop backend - */ -public class RustVNextSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { - private final BackendFactory backendFactory; - - public RustVNextSQLiteOpenHelperFactory(BackendFactory backendFactory) { - this.backendFactory = backendFactory; - } - - @NonNull - @Override - public SupportSQLiteOpenHelper create(@NonNull SupportSQLiteOpenHelper.Configuration configuration) { - return new RustVNextSupportSQLiteOpenHelper(configuration, backendFactory); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java deleted file mode 100644 index b854dd3a8..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteDatabase; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendV1; - -import timber.log.Timber; - -public class RustVNextSupportSQLiteOpenHelper extends RustSupportSQLiteOpenHelper { - - public RustVNextSupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - super(configuration, backendFactory); - } - - public RustVNextSupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - super(backend); - } - - @Override - protected SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly) { - Timber.d("createRustSupportSQLiteDatabase"); - if (configuration != null) { - BackendV1 backend = backendFactory.getBackend(); - // openCollection opens and upgrades the collection - backend.openCollection(configuration.name, null, null, null); - return new RustSupportSQLiteDatabase(backend, readOnly); - } else { - return new RustSupportSQLiteDatabase(backend, readOnly); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java deleted file mode 100644 index f0e8c8496..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - -import androidx.annotation.CheckResult; - -import org.json.JSONArray; - -import BackendProto.Sqlite; - -public interface SQLHandler { - @CheckResult - JSONArray fullQuery(String query, Object... bindArgs); - int executeGetRowsAffected(String sql, Object... bindArgs); - long insertForId(String sql, Object... bindArgs); - - void beginTransaction(); - void commitTransaction(); - void rollbackTransaction(); - - @CheckResult - String[] getColumnNames(String sql); - - void closeDatabase(); - - @CheckResult - String getPath(); - - /* Protobuf-related (#6) */ - Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber); - Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs); - - void cancelCurrentProtoQuery(int sequenceNumber); - void cancelAllProtoQueries(); - - /** - * Sets the page size for all future calls to - * {@link SQLHandler#getNextSlice(long, int)} - * and - * {@link SQLHandler#fullQueryProto(String, Object...)} - * - * Default: 2MB - */ - void setPageSize(long pageSizeBytes); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt new file mode 100644 index 000000000..2bd37a50d --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.database + +import androidx.annotation.CheckResult +import anki.ankidroid.DBResponse +import org.json.JSONArray + +interface SQLHandler { + @CheckResult + fun fullQuery(query: String, bindArgs: Array?): JSONArray + fun fullQuery(query: String): JSONArray { + return fullQuery(query, null) + } + + fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int + fun insertForId(sql: String, bindArgs: Array?): Long + fun beginTransaction() + fun commitTransaction() + fun rollbackTransaction() + + @CheckResult + fun getColumnNames(sql: String): Array + fun closeDatabase() + + @CheckResult + fun getPath(): String? + + /* Protobuf-related (#6) */ + fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse + fun fullQueryProto(query: String, bindArgs: Array?): DBResponse + fun cancelCurrentProtoQuery(sequenceNumber: Int) + fun cancelAllProtoQueries() + + /** + * Sets the page size for all future calls to + * [SQLHandler.getNextSlice] + * and + * [SQLHandler.fullQueryProto] + * + * Default: 2MB + */ + fun setPageSize(pageSizeBytes: Long) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java deleted file mode 100644 index 985ba65a6..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - * - * Contains code under the following license - * - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * From android.database.sqlite.SQLiteSession - */ - -package net.ankiweb.rsdroid.database; - -import org.json.JSONArray; - -import java.util.Stack; - -import BackendProto.Sqlite; - -/** Handles transaction state management */ -public class Session implements SQLHandler { - private final SQLHandler backend; - private final Stack sessions = new Stack<>(); - - public Session(SQLHandler backend) { - this.backend = backend; - } - - private boolean mInTransaction() { - return !sessions.empty(); - } - - public void beginTransaction() { - if (!mInTransaction()) { - backend.beginTransaction(); - } - sessions.add(SessionState.initial()); - } - - @Override - public void commitTransaction() { - backend.commitTransaction(); - } - - @Override - public void rollbackTransaction() { - backend.rollbackTransaction(); - } - - @Override - public String[] getColumnNames(String sql) { - return backend.getColumnNames(sql); - } - - @Override - public void closeDatabase() { - backend.closeDatabase(); - } - - @Override - public String getPath() { - return backend.getPath(); - } - - @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - return backend.getNextSlice(startIndex, sequenceNumber); - } - - @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { - return backend.fullQueryProto(query, bindArgs); - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - backend.cancelCurrentProtoQuery(sequenceNumber); - } - - @Override - public void cancelAllProtoQueries() { - backend.cancelAllProtoQueries(); - } - - @Override - public void setPageSize(long pageSizeBytes) { - backend.setPageSize(pageSizeBytes); - } - - - public void setTransactionSuccessful() { - if (!inTransaction()) { - throw new IllegalStateException("must be in a transaction"); - } - sessions.peek().markSuccessful(); - } - - public void endTransaction() { - if (!inTransaction()) { - throw new IllegalStateException("must be in a transaction"); - } - - SessionState currentState = pop(); - - if (sessions.size() != 0) { - if (!currentState.isSuccessful()) { - sessions.peek().markAsFailed(); - } - - return; - } - - // We have a single session - rollback or abort - - if (currentState.isSuccessful()) { - commitTransaction(); - } else { - rollbackTransaction(); - } - } - - private SessionState pop() { - return sessions.pop(); - } - - public boolean inTransaction() { - return mInTransaction(); - } - - public JSONArray fullQuery(String query, Object[] bindArgs) { - return backend.fullQuery(query, bindArgs); - } - - @Override - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - return backend.executeGetRowsAffected(sql, bindArgs); - } - - @Override - public long insertForId(String sql, Object[] bindArgs) { - return backend.insertForId(sql, bindArgs); - } - - public static class SessionState { - private boolean mTransactionMarkedSuccessful; - private boolean mIsFailed; - - public SessionState(boolean success, boolean isFailed) { - mTransactionMarkedSuccessful = success; - mIsFailed = isFailed; - } - - public static SessionState initial() { - return new SessionState(false, false); - } - - public boolean isSuccessful() { - return isMarkedSuccessful() && !isFailed(); - } - - public boolean isMarkedSuccessful() { - return mTransactionMarkedSuccessful; - } - - public boolean isFailed() { - return mIsFailed; - } - - public void markAsFailed() { - mIsFailed = true; - } - - public void markSuccessful() { - mTransactionMarkedSuccessful = true; - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt new file mode 100644 index 000000000..a49ffadbd --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2020 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * Contains code under the following license + * + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * From android.database.sqlite.SQLiteSession + */ +package net.ankiweb.rsdroid.database + +import anki.ankidroid.DBResponse +import org.json.JSONArray +import java.util.* + +/** Handles transaction state management */ +class Session(private val backend: SQLHandler) : SQLHandler { + private val sessions = Stack() + private fun mInTransaction(): Boolean { + return !sessions.empty() + } + + override fun beginTransaction() { + if (!mInTransaction()) { + backend.beginTransaction() + } + sessions.add(SessionState.initial()) + } + + override fun commitTransaction() { + backend.commitTransaction() + } + + override fun rollbackTransaction() { + backend.rollbackTransaction() + } + + override fun getColumnNames(sql: String): Array { + return backend.getColumnNames(sql) + } + + override fun closeDatabase() { + backend.closeDatabase() + } + + override fun getPath(): String? { + return backend.getPath() + } + + override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { + return backend.getNextSlice(startIndex, sequenceNumber) + } + + override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { + return backend.fullQueryProto(query, bindArgs) + } + + override fun cancelCurrentProtoQuery(sequenceNumber: Int) { + backend.cancelCurrentProtoQuery(sequenceNumber) + } + + override fun cancelAllProtoQueries() { + backend.cancelAllProtoQueries() + } + + override fun setPageSize(pageSizeBytes: Long) { + backend.setPageSize(pageSizeBytes) + } + + fun setTransactionSuccessful() { + check(inTransaction()) { "must be in a transaction" } + sessions.peek().markSuccessful() + } + + fun endTransaction() { + check(inTransaction()) { "must be in a transaction" } + val currentState = pop() + if (sessions.size != 0) { + if (!currentState.isSuccessful) { + sessions.peek().markAsFailed() + } + return + } + + // We have a single session - rollback or abort + if (currentState.isSuccessful) { + commitTransaction() + } else { + rollbackTransaction() + } + } + + private fun pop(): SessionState { + return sessions.pop() + } + + fun inTransaction(): Boolean { + return mInTransaction() + } + + override fun fullQuery(query: String, bindArgs: Array?): JSONArray { + return backend.fullQuery(query, bindArgs) + } + + override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { + return backend.executeGetRowsAffected(sql, bindArgs) + } + + override fun insertForId(sql: String, bindArgs: Array?): Long { + return backend.insertForId(sql, bindArgs) + } + + class SessionState(var isMarkedSuccessful: Boolean, var isFailed: Boolean) { + val isSuccessful: Boolean + get() = isMarkedSuccessful && !isFailed + + fun markAsFailed() { + isFailed = true + } + + fun markSuccessful() { + isMarkedSuccessful = true + } + + companion object { + fun initial(): SessionState { + return SessionState(false, false) + } + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt similarity index 79% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt index 198a4ebc1..7c0bc81a6 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt @@ -31,21 +31,10 @@ * * Source: android.database.sqlite.SQLiteDatabase */ +package net.ankiweb.rsdroid.database -package net.ankiweb.rsdroid.database; - -import androidx.annotation.Nullable; - -public class SessionThreadLocal extends ThreadLocal { - private final SQLHandler mBackend; - - public SessionThreadLocal(SQLHandler backend) { - this.mBackend = backend; - } - - @Nullable - @Override - protected Session initialValue() { - return new Session(mBackend); +class SessionThreadLocal(private val mBackend: SQLHandler) : ThreadLocal() { + override fun initialValue(): Session { + return Session(mBackend) } -} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java deleted file mode 100644 index 8de785bd1..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.database; - - -import android.database.CursorIndexOutOfBoundsException; -import android.database.sqlite.SQLiteException; - -import net.ankiweb.rsdroid.BackendException; -import net.ankiweb.rsdroid.utils.StringToDouble; -import net.ankiweb.rsdroid.utils.StringToLong; - -import java.util.Locale; - -import BackendProto.Sqlite; - -public class StreamingProtobufSQLiteCursor extends AnkiDatabaseCursor { - /** - * Rust Implementation: - * - * When we request a query, rust calculates 2MB (default) of results and sends it to us - * - * We keep track of where we are with getSliceStartIndex: the index into the rust collection - * - * The next request should be for index: getSliceStartIndex() + getCurrentSliceRowCount() - */ - - private final SQLHandler backend; - private final String query; - private Sqlite.DBResponse results; - /** The local position in the current slice */ - private int positionInSlice = -1; - private String[] columnMapping; - private boolean isClosed = false; - private final int sequenceNumber; - /** The total number of rows for the query */ - private final int rowCount; - - /**The current index into the collection or rows */ - private int getSliceStartIndex() { - return (int) results.getStartIndex(); - } - - public StreamingProtobufSQLiteCursor(SQLHandler backend, String query, Object[] bindArgs) { - this.backend = backend; - this.query = query; - - try { - results = this.backend.fullQueryProto(this.query, bindArgs); - sequenceNumber = results.getSequenceNumber(); - rowCount = results.getRowCount(); - } catch (BackendException e) { - throw e.toSQLiteException(this.query); - } - } - - private void loadPage(long startingAtIndex) { - try { - long requestedIndex = startingAtIndex == -1 ? 0 : startingAtIndex; - results = backend.getNextSlice(requestedIndex, sequenceNumber); - positionInSlice = startingAtIndex == -1 ? -1 : 0; - if (results.getSequenceNumber() != sequenceNumber) { - throw new IllegalStateException("rsdroid does not currently handle nested cursor-based queries. Please change the code to avoid holding a reference to the query, or implement the functionality in rsdroid"); - } - } catch (BackendException e) { - throw e.toSQLiteException(query); - } - } - - @Override - public int getCount() { - return rowCount; - } - - @Override - public int getPosition() { - return getSliceStartIndex() + positionInSlice; - } - - @Override - public boolean moveToPosition(int nextPositionGlobal) { - int nextPositionLocal = nextPositionGlobal - getSliceStartIndex(); - boolean isInCurrentSlice = nextPositionLocal >= 0 && nextPositionLocal < getCurrentSliceRowCount(); - if (!isInCurrentSlice && getCurrentSliceRowCount() > 0 && getCount() != getCurrentSliceRowCount()) { - // loadPage this resets the position to 0 - loadPage(nextPositionGlobal); - } else { - positionInSlice = nextPositionLocal; - } - // moving to -1 should return false and mutate the position - return positionInSlice >= 0 && getCurrentSliceRowCount() > 0 && positionInSlice < getCurrentSliceRowCount(); - } - - @Override - public int getColumnIndex(String columnName) { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - return -1; - } - return -1; - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - throw new IllegalArgumentException(String.format("Could not find column '%s'", columnName)); - } - - @Override - public String getColumnName(int columnIndex) { - return getColumnNamesInternal()[columnIndex]; - } - - @Override - public String[] getColumnNames() { - return getColumnNamesInternal(); - } - - private String[] getColumnNamesInternal() { - if (columnMapping == null) { - columnMapping = backend.getColumnNames(query); - if (columnMapping == null) { - throw new IllegalStateException("unable to obtain column mapping"); - } - } - - return columnMapping; - } - - @Override - public int getColumnCount() { - if (getCurrentSliceRowCount() == 0) { - return 0; - } else { - return results.getResult().getRows(0).getFieldsCount(); - } - } - - @Override - public String getString(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to string"); - case LONGVALUE: return Long.toString(field.getLongValue()); - case DOUBLEVALUE: return Double.toString(field.getDoubleValue()); - case STRINGVALUE: return field.getStringValue(); - case DATA_NOT_SET: return null; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public long getLong(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to long"); - case LONGVALUE: return field.getLongValue(); - case DOUBLEVALUE: return (long) field.getDoubleValue(); - case STRINGVALUE: return strtoll(field.getStringValue()); - case DATA_NOT_SET: return 0; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public double getDouble(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to double"); - case LONGVALUE: return field.getLongValue(); - case DOUBLEVALUE: return field.getDoubleValue(); - case STRINGVALUE: return strtod(field.getStringValue()); - case DATA_NOT_SET: return 0d; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public short getShort(int columnIndex) { - return (short) getLong(columnIndex); - } - - @Override - public int getInt(int columnIndex) { - return (int) getLong(columnIndex); - } - - @Override - public float getFloat(int columnIndex) { - return (float) getDouble(columnIndex); - } - - private boolean isNull(Sqlite.SqlValue field) { - return field.getDataCase() == Sqlite.SqlValue.DataCase.DATA_NOT_SET; - } - - @Override - public boolean isNull(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); - return isNull(field); - } - - @Override - public void close() { - isClosed = true; - backend.cancelCurrentProtoQuery(sequenceNumber); - } - - @Override - public boolean isClosed() { - return isClosed; - } - - @Override - public int getType(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: return FIELD_TYPE_BLOB; - case LONGVALUE: return FIELD_TYPE_INTEGER; - case DOUBLEVALUE: return FIELD_TYPE_FLOAT; - case STRINGVALUE: return FIELD_TYPE_STRING; - case DATA_NOT_SET: return FIELD_TYPE_NULL; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - protected Sqlite.Row getRowAtCurrentPosition() { - Sqlite.DBResult result = results.getResult(); - int rowCount = getCurrentSliceRowCount(); - if (positionInSlice < 0 || positionInSlice >= rowCount) { - throw new CursorIndexOutOfBoundsException(String.format(Locale.ROOT, "Index %d requested, with a size of %d", positionInSlice, rowCount)); - } - return result.getRows(positionInSlice); - } - - private Sqlite.SqlValue getFieldAtIndex(int columnIndex) { - return getRowAtCurrentPosition().getFields(columnIndex); - } - - protected int getCurrentSliceRowCount() { - return results.getResult().getRowsCount(); - } - - private long strtoll(String stringValue) { - try { - return StringToLong.strtol(stringValue); - } catch (NumberFormatException exception) { - return 0; - } - } - - private double strtod(String stringValue) { - try { - return StringToDouble.strtod(stringValue); - } catch (NumberFormatException exception) { - return 0.0; - } - } -} - diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt new file mode 100644 index 000000000..3dbd6de6f --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.database + +import android.database.Cursor +import android.database.CursorIndexOutOfBoundsException +import android.database.sqlite.SQLiteException +import anki.ankidroid.DBResponse +import anki.ankidroid.Row +import anki.ankidroid.SqlValue +import anki.ankidroid.SqlValue.DataCase +import net.ankiweb.rsdroid.BackendException +import net.ankiweb.rsdroid.utils.StringToDouble +import net.ankiweb.rsdroid.utils.StringToLong +import java.util.* + +open class StreamingProtobufSQLiteCursor( + /** + * Rust Implementation: + * + * When we request a query, rust calculates 2MB (default) of results and sends it to us + * + * We keep track of where we are with getSliceStartIndex: the index into the rust collection + * + * The next request should be for index: getSliceStartIndex() + getCurrentSliceRowCount() + */ + private val backend: SQLHandler, private val query: String, bindArgs: Array?) : AnkiDatabaseCursor() { + private var results: DBResponse? = null + + /** The local position in the current slice */ + private var positionInSlice = -1 + private var columnMapping: Array? = null + private var isClosed = false + private var sequenceNumber = 0 + + /** The total number of rows for the query */ + private var rowCount = 0 + + /**The current index into the collection or rows */ + private val sliceStartIndex: Int + get() = results!!.startIndex.toInt() + + private fun loadPage(startingAtIndex: Long) { + try { + val requestedIndex = if (startingAtIndex == -1L) 0 else startingAtIndex + results = backend.getNextSlice(requestedIndex, sequenceNumber) + positionInSlice = if (startingAtIndex == -1L) -1 else 0 + check(results!!.sequenceNumber == sequenceNumber) { "rsdroid does not currently handle nested cursor-based queries. Please change the code to avoid holding a reference to the query, or implement the functionality in rsdroid" } + } catch (e: BackendException) { + throw e.toSQLiteException(query) + } + } + + override fun getCount(): Int { + return rowCount + } + + override fun getPosition(): Int { + return sliceStartIndex + positionInSlice + } + + override fun moveToPosition(nextPositionGlobal: Int): Boolean { + val nextPositionLocal = nextPositionGlobal - sliceStartIndex + val isInCurrentSlice = nextPositionLocal >= 0 && nextPositionLocal < currentSliceRowCount + if (!isInCurrentSlice && currentSliceRowCount > 0 && count != currentSliceRowCount) { + // loadPage this resets the position to 0 + loadPage(nextPositionGlobal.toLong()) + } else { + positionInSlice = nextPositionLocal + } + // moving to -1 should return false and mutate the position + return positionInSlice >= 0 && currentSliceRowCount > 0 && positionInSlice < currentSliceRowCount + } + + override fun getColumnIndex(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + return -1 + } + return -1 + } + + @Throws(IllegalArgumentException::class) + override fun getColumnIndexOrThrow(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + throw IllegalArgumentException(e) + } + throw IllegalArgumentException(String.format("Could not find column '%s'", columnName)) + } + + override fun getColumnName(columnIndex: Int): String { + return columnNamesInternal[columnIndex] + } + + override fun getColumnNames(): Array { + return columnNamesInternal + } + + private val columnNamesInternal: Array + get() { + if (columnMapping == null) { + columnMapping = backend.getColumnNames(query) + checkNotNull(columnMapping) { "unable to obtain column mapping" } + } + return columnMapping!! + } + + override fun getColumnCount(): Int { + return if (currentSliceRowCount == 0) { + 0 + } else { + results!!.result.getRows(0).fieldsCount + } + } + + override fun getString(columnIndex: Int): String? { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to string") + DataCase.LONGVALUE -> field.longValue.toString() + DataCase.DOUBLEVALUE -> field.doubleValue.toString() + DataCase.STRINGVALUE -> field.stringValue + DataCase.DATA_NOT_SET -> null + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getLong(columnIndex: Int): Long { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to long") + DataCase.LONGVALUE -> field.longValue + DataCase.DOUBLEVALUE -> field.doubleValue.toLong() + DataCase.STRINGVALUE -> strtoll(field.stringValue) + DataCase.DATA_NOT_SET -> 0 + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getDouble(columnIndex: Int): Double { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to double") + DataCase.LONGVALUE -> field.longValue.toDouble() + DataCase.DOUBLEVALUE -> field.doubleValue + DataCase.STRINGVALUE -> strtod(field.stringValue) + DataCase.DATA_NOT_SET -> 0.0 + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getShort(columnIndex: Int): Short { + return getLong(columnIndex).toShort() + } + + override fun getInt(columnIndex: Int): Int { + return getLong(columnIndex).toInt() + } + + override fun getFloat(columnIndex: Int): Float { + return getDouble(columnIndex).toFloat() + } + + private fun isNull(field: SqlValue): Boolean { + return field.dataCase == DataCase.DATA_NOT_SET + } + + override fun isNull(columnIndex: Int): Boolean { + val field = getFieldAtIndex(columnIndex) + return isNull(field) + } + + override fun close() { + isClosed = true + backend.cancelCurrentProtoQuery(sequenceNumber) + } + + override fun isClosed(): Boolean { + return isClosed + } + + override fun getType(columnIndex: Int): Int { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> Cursor.FIELD_TYPE_BLOB + DataCase.LONGVALUE -> Cursor.FIELD_TYPE_INTEGER + DataCase.DOUBLEVALUE -> Cursor.FIELD_TYPE_FLOAT + DataCase.STRINGVALUE -> Cursor.FIELD_TYPE_STRING + DataCase.DATA_NOT_SET -> Cursor.FIELD_TYPE_NULL + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + protected val rowAtCurrentPosition: Row + get() { + val result = results!!.result + val rowCount = currentSliceRowCount + if (positionInSlice < 0 || positionInSlice >= rowCount) { + throw CursorIndexOutOfBoundsException(String.format(Locale.ROOT, "Index %d requested, with a size of %d", positionInSlice, rowCount)) + } + return result.getRows(positionInSlice) + } + + private fun getFieldAtIndex(columnIndex: Int): SqlValue { + return rowAtCurrentPosition.getFields(columnIndex) + } + + protected val currentSliceRowCount: Int + get() = results!!.result.rowsCount + + private fun strtoll(stringValue: String): Long { + return try { + StringToLong.strtol(stringValue) + } catch (exception: NumberFormatException) { + 0 + } + } + + private fun strtod(stringValue: String): Double { + return try { + StringToDouble.strtod(stringValue) + } catch (exception: NumberFormatException) { + 0.0 + } + } + + init { + try { + results = backend.fullQueryProto(query, bindArgs) + sequenceNumber = results!!.sequenceNumber + rowCount = results!!.rowCount + } catch (e: BackendException) { + throw e.toSQLiteException(query) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt similarity index 73% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt index 5295a7e08..2c94620c9 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt @@ -13,15 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendIoException extends BackendException { - public BackendIoException(Backend.BackendError error) { - super(error); - } -} +class BackendDeckIsFilteredException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt similarity index 73% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt index ff6eedda3..a3327430f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt @@ -13,18 +13,12 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException /** * TODO: Document this */ -public class BackendExistingException extends BackendException { - public BackendExistingException(Backend.BackendError error) { - super(error); - } -} +class BackendExistingException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt similarity index 72% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt index 5ebe4d8e5..02f882cbd 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt @@ -13,16 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendProtoException extends BackendException { - public BackendProtoException(Backend.BackendError error) { - super(error); - } -} +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException +class BackendInterruptedException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java deleted file mode 100644 index 3d420ee34..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -/** - * A lot of exceptions get converted to Invalid Input when returned: - * - * CollectionNotOpen - * CollectionAlreadyOpen - * SearchError - */ -public class BackendInvalidInputException extends BackendException { - public BackendInvalidInputException(Backend.BackendError error) { - super(error); - } - - public static BackendInvalidInputException fromInvalidInputError(Backend.BackendError error) { - switch (error.getLocalized()) { - case "CollectionAlreadyOpen": return new BackendCollectionAlreadyOpenException(error); - case "CollectionNotOpen": return new BackendCollectionNotOpenException(error); - // TODO: We can't handle this case as there's no available properties. - // case "SearchError": return new BackendSearchException(error); - } - return new BackendInvalidInputException(error); - } - - public static class BackendCollectionAlreadyOpenException extends BackendInvalidInputException { - public BackendCollectionAlreadyOpenException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendCollectionNotOpenException extends BackendInvalidInputException { - public BackendCollectionNotOpenException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSearchException extends BackendInvalidInputException { - public BackendSearchException(Backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt new file mode 100644 index 000000000..b2f530e0b --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +/** + * A lot of exceptions get converted to Invalid Input when returned: + * + * CollectionNotOpen + * CollectionAlreadyOpen + * SearchError + */ +open class BackendInvalidInputException(error: BackendError?) : BackendException(error!!) { + class BackendCollectionAlreadyOpenException(error: BackendError?) : BackendInvalidInputException(error) + class BackendCollectionNotOpenException(error: BackendError?) : BackendInvalidInputException(error) + companion object { + fun fromInvalidInputError(error: BackendError): BackendInvalidInputException { + when (error.localized) { + "CollectionAlreadyOpen" -> return BackendCollectionAlreadyOpenException(error) + "CollectionNotOpen" -> return BackendCollectionNotOpenException(error) + } + return BackendInvalidInputException(error) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt similarity index 71% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt index 6eb47a3fb..5f6016580 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt @@ -13,15 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendInterruptedException extends BackendException { - public BackendInterruptedException(Backend.BackendError error) { - super(error); - } -} +class BackendIoException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java deleted file mode 100644 index 55845ff30..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendJsonException extends BackendException { - public BackendJsonException(Backend.BackendError error) { - super(error); - } - - public BackendJsonException(String message) { - super(message); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt similarity index 71% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt index 24c1bbe6e..b429be095 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt @@ -13,16 +13,11 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendDeckIsFilteredException extends BackendException { - public BackendDeckIsFilteredException(Backend.BackendError error) { - super(error); - } -} +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException +class BackendJsonException : BackendException { + constructor(error: BackendError?) : super(error!!) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java deleted file mode 100644 index 26127abff..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendNetworkException extends BackendException { - public BackendNetworkException(Backend.BackendError error) { - super(error); - } - - public static BackendNetworkException fromNetworkError(Backend.BackendError error) { - - if (!error.hasNetworkError()) { - return new BackendNetworkException(error); - } - - Backend.NetworkError networkError = error.getNetworkError(); - - switch (networkError.getKind()) { - case OFFLINE: return new BackendNetworkOfflineException(error); - case TIMEOUT: return new BackendNetworkTimeoutException(error); - case PROXY_AUTH: return new BackendNetworkProxyAuthException(error); - case UNRECOGNIZED: - case OTHER: - } - - return new BackendNetworkException(error); - } - - public static class BackendNetworkOfflineException extends BackendNetworkException { - public BackendNetworkOfflineException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendNetworkTimeoutException extends BackendNetworkException { - public BackendNetworkTimeoutException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendNetworkProxyAuthException extends BackendNetworkException { - public BackendNetworkProxyAuthException(Backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt new file mode 100644 index 000000000..c28677992 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +class BackendNetworkException(error: BackendError) : BackendException(error) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt similarity index 73% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt index f26ab729f..3523fb8fa 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt @@ -13,16 +13,10 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -/** An item was not found (example: a deck from backend.get_deck_legacy) */ -public class BackendNotFoundException extends BackendException { - public BackendNotFoundException(Backend.BackendError error) { - super(error); - } -} +/** An item was not found (example: a deck from backend.get_deck_legacy) */ +class BackendNotFoundException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt new file mode 100644 index 000000000..90445a5ca --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +class BackendProtoException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java deleted file mode 100644 index c59d49209..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendSyncException extends BackendException { - - public BackendSyncException(Backend.BackendError error) { - super(error); - } - - - public static BackendSyncException fromSyncError(Backend.BackendError error) { - - switch (error.getSyncError().getKind()) { - case CONFLICT: - throw new BackendSyncConflictException(error); - case AUTH_FAILED: - throw new BackendSyncAuthFailedException(error); - case SERVER_ERROR: - throw new BackendSyncServerErrorException(error); - case UNRECOGNIZED: - throw new BackendSyncUnrecognizedException(error); - case CLIENT_TOO_OLD: - throw new BackendSyncClientTooOldException(error); - case SERVER_MESSAGE: - throw new BackendSyncServerMessageException(error); - case CLOCK_INCORRECT: - throw new BackendSyncClockIncorrectException(error); - case RESYNC_REQUIRED: - throw new BackendSyncResyncRequiredException(error); - case MEDIA_CHECK_REQUIRED: - throw new BackendSyncMediaCheckRequiredException(error); - case DATABASE_CHECK_REQUIRED: - throw new BackendSyncDatabaseCheckRequiredException(error); - case OTHER: - default: - throw new BackendSyncException(error); - } - } - - public static class BackendSyncConflictException extends BackendSyncException { - public BackendSyncConflictException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncAuthFailedException extends BackendSyncException { - public BackendSyncAuthFailedException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncServerErrorException extends BackendSyncException { - public BackendSyncServerErrorException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncUnrecognizedException extends BackendSyncException { - public BackendSyncUnrecognizedException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncClientTooOldException extends BackendSyncException { - public BackendSyncClientTooOldException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncClockIncorrectException extends BackendSyncException { - public BackendSyncClockIncorrectException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncServerMessageException extends BackendSyncException { - public BackendSyncServerMessageException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncResyncRequiredException extends BackendSyncException { - public BackendSyncResyncRequiredException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncMediaCheckRequiredException extends BackendSyncException { - public BackendSyncMediaCheckRequiredException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncDatabaseCheckRequiredException extends BackendSyncException { - public BackendSyncDatabaseCheckRequiredException(Backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt new file mode 100644 index 000000000..43df43cce --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +open class BackendSyncException(error: BackendError?) : BackendException(error!!) { + class BackendSyncAuthFailedException(error: BackendError?) : BackendSyncException(error) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java deleted file mode 100644 index 8f16012da..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2021 David Allison - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; - -public class BackendTemplateException extends BackendException { - public BackendTemplateException(Backend.BackendError error) { - super(error); - } - - public static BackendTemplateException fromTemplateError(Backend.BackendError error) { - - if (error.getLocalized() == null) { - return new BackendTemplateException(error); - } - - if (error.getLocalized().contains("has a problem")) { - return new BackendTemplateSaveException(error); - } - - return new BackendTemplateException(error); - } - - public static class BackendTemplateSaveException extends BackendTemplateException { - public BackendTemplateSaveException(Backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt new file mode 100644 index 000000000..41240f733 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +open class BackendTemplateException(error: BackendError?) : BackendException(error!!) { + class BackendTemplateSaveException(error: BackendError?) : BackendTemplateException(error) + companion object { + fun fromTemplateError(error: BackendError): BackendTemplateException { + if (error.localized == null) { + return BackendTemplateException(error) + } + return if (error.localized.contains("has a problem")) { + BackendTemplateSaveException(error) + } else BackendTemplateException(error) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java index ca6f1ff50..a3f52554b 100644 --- a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java +++ b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java @@ -10,8 +10,9 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import net.ankiweb.rsdroid.testing.RustBackendLoader; import org.junit.Before; @@ -30,7 +31,7 @@ public class CollectionCreationTest { @Before public void loadLibrary() { - RustBackendLoader.init(); + RustBackendLoader.ensureSetup(null); } @Test @@ -38,13 +39,13 @@ public void ensureCollectionCreatedIsValid() { // We use this routine in AnkiDroid to create the collection, therefore we need to ensure // that the database is valid, open, and the values returned match how the Java used to work - BackendFactory backendV1 = TestUtil.getBackendFactory(); - String path = new File(getTargetContext().getFilesDir(), "collection.anki2").getAbsolutePath(); Configuration config = getConfiguration(path); - SupportSQLiteDatabase database = new RustV11SupportSQLiteOpenHelper(config, backendV1).getWritableDatabase(); + Backend backend = BackendFactory.getBackend(getTargetContext()); + backend.openCollection(":memory:"); + SupportSQLiteDatabase database = AnkiSupportSQLiteDatabase.withRustBackend(backend); database.beginTransaction(); try { diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index 184f651a9..672472fe3 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -3,60 +3,99 @@ version = 3 [[package]] -name = "addr2line" -version = "0.13.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" -dependencies = [ - "gimli", -] +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "adler" -version = "0.2.3" +name = "ahash" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] [[package]] name = "aho-corasick" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] +[[package]] +name = "ammonia" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "android_logger" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b7ddf197de32e415d197aa21c1c0cb36e01e4794fd801302280ac7847ee02" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + [[package]] name = "anki" -version = "2.1.34" +version = "0.0.0" dependencies = [ - "askama", - "async-compression", + "ammonia", + "anki_i18n", + "async-trait", "blake3", "bytes", "chrono", "coarsetime", - "failure", + "csv", "flate2", "fluent", - "fluent-syntax", + "fluent-bundle", + "fnv", "futures", "hex", "htmlescape", - "hyper", + "id_tree", "intl-memoizer", - "itertools 0.9.0", + "itertools", "lazy_static", "nom", - "num-format", "num-integer", + "num_cpus", "num_enum", "once_cell", + "pct-str", "pin-project", + "proc-macro-nested", "prost", "prost-build", + "pulldown-cmark", "rand", "regex", + "reqwest", "rusqlite", "scopeguard", "serde", @@ -70,26 +109,46 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "strum", "tempfile", "tokio", + "tokio-util 0.6.10", "unic-langid", + "unic-ucd-category", "unicase", "unicode-normalization", "utime", "zip", + "zstd", +] + +[[package]] +name = "anki_i18n" +version = "0.0.0" +dependencies = [ + "fluent", + "fluent-bundle", + "fluent-syntax", + "inflections", + "intl-memoizer", + "num-format", + "phf", + "serde", + "serde_json", + "unic-langid", ] [[package]] name = "anyhow" -version = "1.0.32" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arc-swap" -version = "0.4.8" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" +checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" [[package]] name = "arrayref" @@ -108,68 +167,25 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" - -[[package]] -name = "askama" -version = "0.10.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e7ebd44d0047fd48206c83c5cd3214acc7b9d87f001da170145c47ef7d12" -dependencies = [ - "askama_derive", - "askama_escape", - "askama_shared", -] - -[[package]] -name = "askama_derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d7169690c4f56343dcd821ab834972a22570a2662a19a84fd7775d5e1c3881" -dependencies = [ - "askama_shared", - "proc-macro2", - "quote", - "syn", -] +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] -name = "askama_escape" -version = "0.10.1" +name = "arrayvec" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] -name = "askama_shared" -version = "0.10.4" +name = "async-trait" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fc272363345c8cdc030e4c259d9d028237f8b057dc9bb327772a257bde6bb5" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ - "askama_escape", - "humansize", - "nom", - "num-traits", - "percent-encoding", "proc-macro2", "quote", - "serde", "syn", - "toml", -] - -[[package]] -name = "async-compression" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9021768bcce77296b64648cc7a7460e3df99979b97ed5c925c38d1cc83778d98" -dependencies = [ - "bytes", - "flate2", - "futures-core", - "memchr", - "pin-project-lite", ] [[package]] @@ -180,84 +196,88 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "autocfg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" - -[[package]] -name = "backtrace" -version = "0.3.50" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" -dependencies = [ - "addr2line", - "cfg-if 0.1.10", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "blake2b_simd" -version = "0.5.10" +name = "blake3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", - "arrayvec 0.5.1", + "arrayvec 0.7.2", + "cc", + "cfg-if", "constant_time_eq", + "digest", ] [[package]] -name = "blake3" -version = "0.3.6" +name = "block-buffer" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4f9586c9a3151c4b49b19e82ba163dd073614dd057e53c969e1a4db5b52720" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "arrayref", - "arrayvec 0.5.1", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac", - "digest", + "generic-array", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", ] +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "0.5.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.58" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -265,12 +285,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -279,30 +293,34 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", - "time", + "time 0.1.44", + "winapi", ] [[package]] name = "coarsetime" -version = "0.1.14" -source = "git+https://github.com/ankitects/rust-coarsetime.git?branch=old-mac-compat#f9e2c86216f0f4803bc75404828318fc206dab29" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" dependencies = [ - "lazy_static", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "once_cell", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "combine" -version = "4.2.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e5ef862b2df927249f4e2bdc29c1bd13a33105f900884b0c32acdf32aff584" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ "bytes", "memchr", @@ -315,172 +333,144 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] -name = "crc32fast" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -dependencies = [ - "cfg-if 0.1.10", -] - -[[package]] -name = "crossbeam" -version = "0.7.3" +name = "core-foundation" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "cfg-if 0.1.10", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", + "core-foundation-sys", + "libc", ] [[package]] -name = "crossbeam-channel" -version = "0.4.4" +name = "core-foundation-sys" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" -dependencies = [ - "crossbeam-utils", - "maybe-uninit", -] +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] -name = "crossbeam-deque" -version = "0.7.4" +name = "crc32fast" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "maybe-uninit", + "cfg-if", ] [[package]] -name = "crossbeam-epoch" -version = "0.8.2" +name = "crossbeam-channel" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" dependencies = [ - "autocfg", - "cfg-if 0.1.10", + "cfg-if", "crossbeam-utils", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", ] [[package]] -name = "crossbeam-queue" -version = "0.2.3" +name = "crossbeam-utils" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" dependencies = [ - "cfg-if 0.1.10", - "crossbeam-utils", - "maybe-uninit", + "cfg-if", + "once_cell", ] [[package]] -name = "crossbeam-utils" -version = "0.7.2" +name = "crypto-common" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "generic-array", + "typenum", ] [[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +name = "csv" +version = "1.1.6" +source = "git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90#1c9d3aab6f79a7d815c69f925a46a4590c115f90" dependencies = [ - "generic-array", - "subtle", + "bstr", + "csv-core", + "itoa 1.0.2", + "ryu", + "serde", ] [[package]] -name = "derivative" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" +name = "csv-core" +version = "0.1.10" +source = "git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90#1c9d3aab6f79a7d815c69f925a46a4590c115f90" dependencies = [ - "proc-macro2", - "quote", - "syn", + "memchr", ] [[package]] name = "digest" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "generic-array", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "dirs" -version = "2.0.2" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.5" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] name = "either" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] -name = "error-chain" -version = "0.12.4" +name = "encoding_rs" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ - "version_check", + "cfg-if", ] [[package]] -name = "failure" -version = "0.1.8" +name = "env_logger" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ - "backtrace", - "failure_derive", + "log", + "regex", ] [[package]] -name = "failure_derive" -version = "0.1.8" +name = "error-chain" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "version_check", ] [[package]] @@ -495,28 +485,47 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" [[package]] name = "flate2" -version = "1.0.16" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if 0.1.10", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "fluent" -version = "0.10.2" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" dependencies = [ "fluent-bundle", "unic-langid", @@ -524,32 +533,37 @@ dependencies = [ [[package]] name = "fluent-bundle" -version = "0.10.2" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" dependencies = [ "fluent-langneg", "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rental", + "rustc-hash", + "self_cell", "smallvec", "unic-langid", ] [[package]] name = "fluent-langneg" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" dependencies = [ "unic-langid", ] [[package]] name = "fluent-syntax" -version = "0.9.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba" +checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" +dependencies = [ + "thiserror", +] [[package]] name = "fnv" @@ -558,26 +572,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "bitflags", - "fuchsia-zircon-sys", + "matches", + "percent-encoding", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "futf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] [[package]] name = "futures" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -590,9 +608,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -600,15 +618,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -617,17 +635,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -635,9 +652,9 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" @@ -647,9 +664,9 @@ checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -658,54 +675,56 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project", + "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[package]] -name = "fxhash" -version = "0.2.1" +name = "gag" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "a713bee13966e9fbffdf7193af71d54a6b35a0bb34997cd6c9519ebeb5005972" dependencies = [ - "byteorder", + "filedescriptor", + "tempfile", ] [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" -version = "0.1.14" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" - [[package]] name = "h2" -version = "0.2.6" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ "bytes", "fnv", @@ -716,42 +735,71 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.3", "tracing", ] [[package]] name = "hashbrown" -version = "0.8.2" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "autocfg", + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", ] [[package]] name = "heck" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "htmlescape" @@ -761,42 +809,43 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.1" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.2", ] [[package]] name = "http-body" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", + "pin-project-lite", ] [[package]] name = "httparse" -version = "1.3.4" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] -name = "humansize" -version = "1.1.0" +name = "httpdate" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.13.7" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -806,10 +855,10 @@ dependencies = [ "http", "http-body", "httparse", - "itoa", - "pin-project", + "httpdate", + "itoa 1.0.2", + "pin-project-lite", "socket2", - "time", "tokio", "tower-service", "tracing", @@ -817,75 +866,122 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.5.1" +name = "hyper-rustls" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" +checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ - "autocfg", - "hashbrown", + "futures-util", + "hyper", + "log", + "rustls", + "tokio", + "tokio-rustls", + "webpki", ] [[package]] -name = "intl-memoizer" -version = "0.3.0" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +name = "hyper-timeout" +version = "0.4.1" +source = "git+https://github.com/ankitects/hyper-timeout.git?rev=0cb6f7d14c62819e37cd221736f8b0555e823712#0cb6f7d14c62819e37cd221736f8b0555e823712" dependencies = [ - "type-map", - "unic-langid", + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", ] [[package]] -name = "intl_pluralrules" -version = "6.0.0" +name = "id_tree" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002" +checksum = "bcd9db8dd5be8bde5a2624ed4b2dfb74368fe7999eb9c4940fd3ca344b61071a" dependencies = [ - "tinystr", - "unic-langid", + "snowflake", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "idna" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ - "libc", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "itertools" -version = "0.8.2" +name = "indexmap" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3" dependencies = [ - "either", + "autocfg", + "hashbrown 0.12.1", ] [[package]] -name = "itertools" -version = "0.9.0" +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "instant" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "either", + "cfg-if", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18f988384267d7066cc2be425e6faf352900652c046b6971d2e228d3b1c5ecf" +dependencies = [ + "tinystr", + "unic-langid", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "itertools" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.6" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jni" @@ -908,13 +1004,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "jobserver" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", ] [[package]] @@ -929,24 +1033,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec 0.5.1", + "arrayvec 0.5.2", "bitflags", - "cfg-if 1.0.0", + "cfg-if", "ryu", "static_assertions", ] [[package]] name = "libc" -version = "0.2.76" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libsqlite3-sys" -version = "0.18.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e704a02bcaecd4a08b93a23f6be59d0bd79cd161e0963e9499165a0a35df7bd" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" dependencies = [ "cc", "pkg-config", @@ -954,106 +1058,116 @@ dependencies = [ ] [[package]] -name = "linked-hash-map" -version = "0.5.3" +name = "lock_api" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" -version = "0.4.11" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] -name = "lru-cache" -version = "0.1.2" +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ - "linked-hash-map", + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", ] [[package]] -name = "maybe-uninit" -version = "2.0.0" +name = "matches" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] -name = "memoffset" -version = "0.5.5" +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" dependencies = [ - "autocfg", + "mime", + "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.6.22" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", "libc", "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] name = "multimap" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] -name = "net2" -version = "0.2.37" +name = "new_debug_unreachable" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] name = "nodrop" @@ -1063,13 +1177,12 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" -version = "5.1.2" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "lexical-core", "memchr", - "version_check", + "minimal-lexical", ] [[package]] @@ -1079,14 +1192,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ "arrayvec 0.4.12", - "itoa", + "itoa 0.4.8", ] [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -1094,18 +1207,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1113,19 +1226,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ - "derivative", "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1134,16 +1246,56 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.20.0" +name = "num_threads" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] [[package]] name = "once_cell" -version = "1.5.2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pct-str" +version = "1.1.0" +source = "git+https://github.com/timothee-haudebourg/pct-str.git?rev=4adccd8d4a222ab2672350a102f06ae832a0572d#4adccd8d4a222ab2672350a102f06ae832a0572d" +dependencies = [ + "utf8-decode", +] [[package]] name = "percent-encoding" @@ -1153,28 +1305,82 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", "indexmap", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" -version = "0.4.23" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.23" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -1183,9 +1389,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1195,57 +1401,58 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.18" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] -name = "podio" -version = "0.1.7" +name = "ppv-lite86" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] -name = "ppv-lite86" -version = "0.2.8" +name = "precomputed-hash" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ + "thiserror", "toml", ] [[package]] name = "proc-macro-hack" -version = "0.5.18" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro-nested" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.19" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "prost" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce49aefe0a6144a45de32927c77bd2859a5f7677b55f220ae5b744e87389c212" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes", "prost-derive", @@ -1253,30 +1460,32 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b10678c913ecbd69350e8535c3aef91a8676c0773fc1d7b95cdd196d7f2f26" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", "heck", - "itertools 0.8.2", + "itertools", + "lazy_static", "log", "multimap", "petgraph", "prost", "prost-types", + "regex", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools 0.8.2", + "itertools", "proc-macro2", "quote", "syn", @@ -1284,41 +1493,51 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ "bytes", "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + [[package]] name = "quote" -version = "1.0.7" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.7.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1326,56 +1545,55 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "redox_syscall" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ - "rand_core", + "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_users" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", "redox_syscall", - "rust-argon2", + "thiserror", ] [[package]] name = "regex" -version = "1.3.9" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" -version = "0.6.18" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -1383,39 +1601,75 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] -name = "rental" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f" +name = "reqwest" +version = "0.11.3" +source = "git+https://github.com/ankitects/reqwest.git?rev=7591444614de02b658ddab125efba7b2bb4e2335#7591444614de02b658ddab125efba7b2bb4e2335" dependencies = [ - "rental-impl", - "stable_deref_trait", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-timeout", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-socks", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", ] [[package]] -name = "rental-impl" -version = "0.5.5" +name = "ring" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] name = "rsdroid" version = "0.1.0" dependencies = [ + "android_logger", "anki", - "itertools 0.10.0", + "gag", + "itertools", "jni", "lazy_static", "lexical-core", + "log", "num_enum", "prost", "prost-build", @@ -1423,47 +1677,67 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "slog", + "slog-envlogger", ] [[package]] name = "rusqlite" -version = "0.23.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d0fd62e1df63d254714e6cb40d0a0e82e7a1623e7a27f679d851af092ae58b" +checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", + "hashlink", "libsqlite3-sys", - "lru-cache", "memchr", "smallvec", - "time", ] [[package]] -name = "rust-argon2" -version = "0.7.0" +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustls" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework", ] [[package]] -name = "rustc-demangle" -version = "0.1.16" +name = "rustversion" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "same-file" @@ -1474,38 +1748,86 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" + [[package]] name = "serde" -version = "1.0.115" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde-aux" -version = "0.6.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50f53d4b01e854319c1f5b854cd59471f054ea7e554988850d3f36ca1dc852" +checksum = "93abf9799c576f004252b2a05168d58527fb7c54de12e94b4d12fe3475ffad24" dependencies = [ "chrono", "serde", - "serde_derive", "serde_json", ] [[package]] name = "serde_derive" -version = "1.0.115" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -1514,20 +1836,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ - "itoa", + "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" +checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" dependencies = [ "proc-macro2", "quote", @@ -1555,29 +1877,56 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.2", + "ryu", + "serde", +] + [[package]] name = "sha1" -version = "0.6.0" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "siphasher" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "slog" -version = "2.5.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b3336ce47ce2f96673499fc07eb85e3472727b9a7a2959964b002c2ce8fbbb" +checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" dependencies = [ "crossbeam-channel", "slog", @@ -1602,9 +1951,9 @@ dependencies = [ [[package]] name = "slog-scope" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c44c89dd8b0ae4537d1ae318353eaf7840b4869c536e31c41e963d1ea523ee6" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" dependencies = [ "arc-swap", "lazy_static", @@ -1613,11 +1962,10 @@ dependencies = [ [[package]] name = "slog-stdlog" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d87903baf655da2d82bc3ac3f7ef43868c58bf712b3a661fda72009304c23" +checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" dependencies = [ - "crossbeam", "log", "slog", "slog-scope", @@ -1625,15 +1973,15 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab1d807cf71129b05ce36914e1dbb6fbfbdecaf686301cb457f4fa967f9f5b6" +checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" dependencies = [ "atty", - "chrono", "slog", "term", "thread_local", + "time 0.3.9", ] [[package]] @@ -1642,22 +1990,27 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "snowflake" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1" + [[package]] name = "socket2" -version = "0.3.19" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ - "cfg-if 1.0.0", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "static_assertions" @@ -1666,32 +2019,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "subtle" -version = "2.2.3" +name = "string_cache" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] [[package]] -name = "syn" -version = "1.0.38" +name = "string_cache_codegen" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "unicode-xid", ] [[package]] -name = "synstructure" -version = "0.12.4" +name = "strum" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck", "proc-macro2", "quote", + "rustversion", "syn", - "unicode-xid", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1702,26 +2091,58 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", ] [[package]] name = "term" -version = "0.6.1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ - "dirs", - "winapi 0.3.9", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1735,49 +2156,108 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "winapi 0.3.9", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", ] +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa 1.0.2", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinystr" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707151f004e8db265b83b1c7509d6c3b4c2c2bc8696113cbe0a8e595c2fdbd3b" +checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1" [[package]] name = "tinyvec" -version = "0.3.3" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.22" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ "bytes", - "fnv", - "futures-core", - "iovec", - "lazy_static", + "libc", "memchr", "mio", "num_cpus", + "once_cell", "pin-project-lite", - "slab", + "socket2", + "winapi", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.1.1" +source = "git+https://github.com/ankitects/tokio-io-timeout.git?rev=1ee0892217e9a76bba4bb369ec5fab8854935a3c#1ee0892217e9a76bba4bb369ec5fab8854935a3c" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", ] [[package]] name = "tokio-util" -version = "0.3.1" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -1787,39 +2267,53 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" -version = "0.5.6" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] [[package]] name = "tower-service" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ - "cfg-if 0.1.10", - "log", + "cfg-if", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.14" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db63662723c316b43ca36d833707cc93dff82a02ba3d7e354f342682cc8b3545" +checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -1830,24 +2324,45 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "type-map" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717" +checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" dependencies = [ - "fxhash", + "rustc-hash", ] [[package]] name = "typenum" -version = "1.12.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" [[package]] name = "unic-langid" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d81136159f779c35b10655f45210c71cd5ca5a45aadfe9840a61c7071735ed" +checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5" dependencies = [ "unic-langid-impl", "unic-langid-macros", @@ -1855,18 +2370,18 @@ dependencies = [ [[package]] name = "unic-langid-impl" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43c61e94492eb67f20facc7b025778a904de83d953d8fcb60dd9adfd6e2d0ea" +checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d" dependencies = [ "tinystr", ] [[package]] name = "unic-langid-macros" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49bd90791278634d57e3ed4a4073108e3f79bfb87ab6a7b8664ba097425703df" +checksum = "18f980d6d87e8805f2836d64b4138cc95aa7986fa63b1f51f67d5fbff64dd6e5" dependencies = [ "proc-macro-hack", "tinystr", @@ -1876,9 +2391,9 @@ dependencies = [ [[package]] name = "unic-langid-macros-impl" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0098f77bd754f8fb7850cdf4ab143aa821898c4ac6dc16bcb2aa3e62ce858d1" +checksum = "29396ffd97e27574c3e01368b1a64267d3064969e4848e2e130ff668be9daa9f" dependencies = [ "proc-macro-hack", "quote", @@ -1886,6 +2401,27 @@ dependencies = [ "unic-langid-impl", ] +[[package]] +name = "unic-ucd-category" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +dependencies = [ + "matches", + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1895,26 +2431,68 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] -name = "unicode-xid" -version = "0.2.1" +name = "unicode-width" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" [[package]] name = "utime" @@ -1923,29 +2501,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91baa0c65eabd12fcbdac8cc35ff16159cab95cae96d0222d6d0271db6193cef" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "vcpkg" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] @@ -1961,30 +2539,123 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "which" -version = "3.1.1" +name = "wasm-bindgen" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ - "libc", + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + +[[package]] +name = "which" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", +] [[package]] name = "winapi" @@ -1996,12 +2667,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2014,7 +2679,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2024,23 +2689,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "ws2_32-sys" -version = "0.2.1" +name = "windows-sys" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", ] [[package]] name = "zip" -version = "0.5.6" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58287c28d78507f5f91f2a4cf1e8310e2c76fd4c6932f93ac60fd1ceb402db7d" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ + "byteorder", "crc32fast", "flate2", - "podio", - "time", + "thiserror", + "time 0.1.44", +] + +[[package]] +name = "zstd" +version = "0.10.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.6+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", ] diff --git a/rslib-bridge/Cargo.toml b/rslib-bridge/Cargo.toml index adaadbf41..eb3eb42cc 100644 --- a/rslib-bridge/Cargo.toml +++ b/rslib-bridge/Cargo.toml @@ -12,7 +12,7 @@ crate_type = ["dylib"] [dependencies] jni = { version = "0.17.0", default-features = false } anki = { path = "anki/rslib" } -prost = "0.6.1" +prost = "0.9" serde = "1.0.114" serde_json = "1.0.56" serde_derive = "1.0.114" @@ -22,10 +22,15 @@ itertools = "0.10.0" lexical-core = "0.7.5" # picked bundled - TODO: Is this correct? -rusqlite = { version = "0.23.1", features = ["trace", "functions", "collation", "bundled"] } +rusqlite = { version = "0.26.0", features = ["trace", "functions", "collation", "bundled"] } +android_logger = "0.11.0" +log = "0.4.17" +slog = "2.7.0" +gag = "1.0.0" +slog-envlogger = "2.2.0" [features] no-android = [] [build-dependencies] -prost-build = "0.6.1" \ No newline at end of file +prost-build = "0.9" diff --git a/rslib-bridge/anki b/rslib-bridge/anki index cae833d61..2f7a02029 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit cae833d61c4555bc60db345ad6da72f5a5864256 +Subproject commit 2f7a0202976b3a400542b0a9bf5312c0e633472a diff --git a/rslib-bridge/build.rs b/rslib-bridge/build.rs deleted file mode 100644 index ae46e0682..000000000 --- a/rslib-bridge/build.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::fmt::Write; -use std::path::Path; -use std::process::Command; - -// TODO: See if we can reference anki/build.rs so there's no code duplication - -struct CustomGenerator {} - -fn write_method_enum(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use num_enum::TryFromPrimitive; -#[derive(PartialEq,TryFromPrimitive)] -#[repr(u32)] -pub enum BackendMethod { -"#, - ); - for (idx, method) in service.methods.iter().enumerate() { - writeln!(buf, " {} = {},", method.proto_name, idx + 1).unwrap(); - } - buf.push_str("}\n\n"); -} - -fn write_method_trait(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use prost::Message; -pub type BackendResult = std::result::Result; -pub trait DroidBackendService { - fn run_command_bytes2_inner_ad(&self, method: u32, input: &[u8]) -> std::result::Result, anki::err::AnkiError> { - match method { -"#, - ); - - for (idx, method) in service.methods.iter().enumerate() { - write!( - buf, - concat!(" ", - "{idx} => {{ let input = {input_type}::decode(input)?;\n", - "let output = self.{rust_method}(input)?;\n", - "let mut out_bytes = Vec::new(); output.encode(&mut out_bytes)?; Ok(out_bytes) }}, "), - idx = idx + 1, - input_type = method.input_type, - rust_method = method.name - ) - .unwrap(); - } - buf.push_str( - r#" - _ => Err(anki::err::AnkiError::invalid_input("invalid command")), - } - } -"#, - ); - - for method in &service.methods { - write!( - buf, - concat!( - " fn {method_name}(&self, input: {input_type}) -> ", - "BackendResult<{output_type}>;\n" - ), - method_name = method.name, - input_type = method.input_type, - output_type = method.output_type - ) - .unwrap(); - } - buf.push_str("}\n"); -} - -impl prost_build::ServiceGenerator for CustomGenerator { - fn generate(&mut self, service: prost_build::Service, buf: &mut String) { - write_method_enum(buf, &service); - write_method_trait(buf, &service); - } -} - -fn service_generator() -> Box { - Box::new(CustomGenerator {}) -} - - -fn main() -> std::io::Result<()> { - // output protobuf generated code - println!("cargo:rerun-if-changed=proto/AdBackend.proto"); - - let mut config = prost_build::Config::new(); - config - // we avoid default OUT_DIR for now, as it breaks code completion - .out_dir("src") - .service_generator(service_generator()) - .compile_protos(&["proto/AdBackend.proto"], &["proto"]) - .unwrap(); - - if let Err(e) = std::env::var("DONT_RUSTFMT") { - assert_eq!(e, std::env::VarError::NotPresent); - println!("Using rustfmt to format src/backend_proto.rs"); - // rustfmt the protobuf code - let rustfmt = Command::new("rustfmt") - .arg(Path::new("src/backend_proto.rs")) - .status() - .unwrap(); - - assert!(rustfmt.success(), "rustfmt backend_proto.rs failed"); - } - - Ok(()) -} \ No newline at end of file diff --git a/rslib-bridge/buildinfo.txt b/rslib-bridge/buildinfo.txt new file mode 100755 index 000000000..71ecf6174 --- /dev/null +++ b/rslib-bridge/buildinfo.txt @@ -0,0 +1,2 @@ +STABLE_VERSION 2.1.53 +STABLE_BUILDHASH 665d81c4 diff --git a/rslib-bridge/proto/AdBackend.proto b/rslib-bridge/proto/AdBackend.proto deleted file mode 100644 index 86ea1493f..000000000 --- a/rslib-bridge/proto/AdBackend.proto +++ /dev/null @@ -1,70 +0,0 @@ -syntax = "proto3"; - -package BackendProto; - -option java_generic_services = true; - -// Generic containers -/////////////////////////////////////////////////////////// - -// IDs used in RPC calls -/////////////////////////////////////////////////////////// - -// New style RPC definitions -/////////////////////////////////////////////////////////// - -service AnkiDroidBackendService { - rpc SchedTimingTodayLegacy (SchedTimingTodayIn) returns (SchedTimingTodayOut2); - rpc LocalMinutesWestLegacy (LocalMinutesWestIn) returns (LocalMinutesWestOut); - rpc DebugActiveDatabaseSequenceNumbers (DebugActiveDatabaseSequenceNumbersIn) returns (DebugActiveDatabaseSequenceNumbersOut); -} - -// Protobuf stored in .anki2 files -// These should be moved to a separate file in the future -/////////////////////////////////////////////////////////// - -// Containers for passing around database objects -/////////////////////////////////////////////////////////// - -// Backend -/////////////////////////////////////////////////////////// - - -// Errors -/////////////////////////////////////////////////////////// - -// Progress -/////////////////////////////////////////////////////////// - -// Messages -/////////////////////////////////////////////////////////// - -message DebugActiveDatabaseSequenceNumbersIn { - int64 backend_ptr = 1; -} - -message DebugActiveDatabaseSequenceNumbersOut { - repeated int32 sequence_numbers = 1; -} - -message LocalMinutesWestIn { - int64 collection_creation_time = 1; -} - -message LocalMinutesWestOut { - sint32 mins_west = 1; -} - -message SchedTimingTodayIn { - int64 created_secs = 1; - sint32 created_mins_west = 2; - int64 now_secs = 3; - sint32 now_mins_west = 4; - sint32 rollover_hour = 5; -} - -message SchedTimingTodayOut2 { - uint32 days_elapsed = 1; - int64 next_day_at = 2; -} - diff --git a/rslib-bridge/src/ankidroid.rs b/rslib-bridge/src/ankidroid.rs deleted file mode 100644 index 871306926..000000000 --- a/rslib-bridge/src/ankidroid.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anki::backend::Backend as RustBackend; - -pub(crate) struct AnkiDroidBackend { - pub backend: RustBackend, -} - -impl AnkiDroidBackend { - pub fn new(backend: RustBackend) -> AnkiDroidBackend { - AnkiDroidBackend { backend } - } -} \ No newline at end of file diff --git a/rslib-bridge/src/dbcommand.rs b/rslib-bridge/src/dbcommand.rs deleted file mode 100644 index 943da044c..000000000 --- a/rslib-bridge/src/dbcommand.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use anki::backend_proto::{DbResponse, DbResult}; - -// Handles global variables for DbResponse streaming -// COULD_BE_BETTER: Consider converting this into an object returned from the function -// (accessible via JNI) - more idiomatic, but probably less clean than this. - -// COULD_BE_BETTER: make DBResponse.DbResult non-optional - -use i64 as backend_pointer; -use i64 as dbresponse_pointer; - -use anki::backend_proto::{Row, SqlValue}; -use std::mem::size_of; -use anki::backend_proto::sql_value::Data; -use itertools::{Itertools, FoldWhile}; -use itertools::FoldWhile::{Done, Continue}; -use std::ops::Deref; - - -pub trait Sizable { - /** Estimates the heap size of the value, in bytes */ - fn estimate_size(&self) -> usize; -} - -impl Sizable for Data { - fn estimate_size(&self) -> usize { - match self { - Data::StringValue(s) => { s.len() } - Data::LongValue(_) => { size_of::() } - Data::DoubleValue(_) => { size_of::() } - Data::BlobValue(b) => { b.len() } - } - } -} - -impl Sizable for SqlValue { - fn estimate_size(&self) -> usize { - // Add a byte for the optional - self.data.as_ref().map(|f| f.estimate_size() + 1).unwrap_or(1) - } -} - -impl Sizable for Row { - fn estimate_size(&self) -> usize { - self.fields.iter().map(|x| x.estimate_size()).sum() - } -} - -impl Sizable for DbResult { - fn estimate_size(&self) -> usize { - // Performance: It might be best to take the first x rows and determine the data types - // If we have floats or longs, they'll be a fixed size (excluding nulls) and should speed - // up the calculation as we'll only calculate a subset of the columns. - self.rows.iter().map(|x| x.estimate_size()).sum() - } -} - -pub(crate) fn select_next_slice<'a>(mut rows : impl Iterator) -> Vec { - select_slice_of_size(rows, get_max_page_size()).into_inner().1 -} - -fn select_slice_of_size<'a>(mut rows : impl Iterator, max_size: usize) -> FoldWhile<(usize, Vec)> { - let init: Vec = Vec::new(); - let folded = rows.fold_while((0, init), |mut acc, x| { - let new_size = acc.0 + x.estimate_size(); - // If the accumulator is 0, but we're over the size: return a single result so we don't loop forever. - // Theoretically, this shouldn't happen as data should be reasonably sized - if new_size > max_size && acc.0 > 0 { - Done(acc) - } else { - // PERF: should be faster to return (size, numElements) then bulk copy/slice - acc.1.push(x.to_owned()); - Continue((new_size, acc.1)) - } - }); - folded -} - -lazy_static! { - // backend_pointer => Map - static ref HASHMAP: Mutex>> = { - Mutex::new(HashMap::new()) - }; -} - -pub(crate) unsafe fn flush_cache(ptr : &backend_pointer, sequence_number : i32) { - let mut map = HASHMAP.lock().unwrap(); - let entries = map.get_mut(ptr); - match entries { - Some(seq_to_ptr) => { - let entry = seq_to_ptr.remove_entry(&sequence_number); - match entry { - Some(ptr) => { - let raw = ptr.1 as *mut DbResponse; - Box::from_raw(raw); - } - None => { } - } - } - None => { } - } -} - - -pub(crate) unsafe fn flush_all(ptr: &backend_pointer) { - let mut map = HASHMAP.lock().unwrap(); - - // clear the map - let entries = map.remove_entry(ptr); - - match entries { - Some(seq_to_ptr_map) => { - // then clear each value - for val in seq_to_ptr_map.1.values() { - let raw = (*val) as *mut DbResponse; - Box::from_raw(raw); - } - } - None => { } - } -} - -pub(crate) fn active_sequences(ptr : backend_pointer) -> Vec { - let mut map = HASHMAP.lock().unwrap(); - - match map.get_mut(&ptr) { - Some(x) => { - let keys = x.keys(); - keys.into_iter().map(|i| *i).collect_vec() - }, - None => { - Vec::new() - } - } -} - -/** -Store the data in the cache if larger than than the page size.
-Returns: The data capped to the page size -*/ -pub(crate) fn trim_and_cache_remaining(backend_ptr: i64, values: DbResult, sequence_number: i32) -> DbResponse { - let start_index = 0; - - // PERF: Could speed this up by not creating the vector and just calculating the count - let first_result = select_next_slice(values.rows.iter()); - - let row_count = values.rows.len() as i32; - if first_result.len() < values.rows.len() { - let to_store = DbResponse { result: Some(values), sequence_number, row_count, start_index }; - insert_cache(backend_ptr, to_store); - - DbResponse { result: Some(DbResult { rows: first_result }), sequence_number, row_count, start_index } - } else { - DbResponse { result: Some(values), sequence_number, row_count, start_index } - } -} - -fn insert_cache(ptr : backend_pointer, result : DbResponse) { - let mut map = HASHMAP.lock().unwrap(); - - match map.get_mut(&ptr) { - Some(_) => { }, - None => { - let map2 : HashMap = HashMap::new(); - map.insert(ptr, map2); - } - }; - - let out_hash_map = map.get_mut(&ptr).unwrap(); - - out_hash_map.insert(result.sequence_number, Box::into_raw(Box::new(result)) as dbresponse_pointer); -} - -pub(crate) unsafe fn get_next(ptr : backend_pointer, sequence_number : i32, start_index : i64) -> Option { - let result = get_next_result(ptr, &sequence_number, start_index); - - match result.as_ref() { - Some(x) => { - if x.result.is_none() || x.result.as_ref().unwrap().rows.is_empty() { - flush_cache(&ptr, sequence_number) - } - }, - None => {} - } - - result -} - -unsafe fn get_next_result(ptr: backend_pointer, sequence_number: &i32, start_index: i64) -> Option { - let map = HASHMAP.lock().unwrap(); - - let result_map = map.get(&ptr)?; - - let backend_ptr = *result_map.get(&sequence_number)?; - - let current_result = &mut *(backend_ptr as *mut DbResponse); - - // TODO: This shouldn't need to exist - let tmp: Vec = Vec::new(); - let next_rows = current_result.result.as_ref().map(|x| x.rows.iter()).unwrap_or(tmp.iter()); - - let skipped_rows = next_rows.clone().skip(start_index as usize).collect_vec(); - println!("{}", skipped_rows.len()); - - let filtered_rows = select_next_slice(next_rows.skip(start_index as usize)); - - let result = DbResult { rows: filtered_rows }; - - let trimmed_result = DbResponse { result: Some(result), sequence_number: current_result.sequence_number, row_count: current_result.row_count, start_index }; - - Some(trimmed_result) -} - -static mut SEQUENCE_NUMBER: i32 = 0; - -pub(crate) unsafe fn next_sequence_number() -> i32 { - SEQUENCE_NUMBER = SEQUENCE_NUMBER + 1; - SEQUENCE_NUMBER -} - -lazy_static!{ - // same as we get from io.requery.android.database.CursorWindow.sCursorWindowSize - static ref DB_COMMAND_PAGE_SIZE: Mutex = Mutex::new(1024 * 1024 * 2); -} - -pub(crate) fn set_max_page_size(size: usize) { - let mut state = DB_COMMAND_PAGE_SIZE.lock().expect("Could not lock mutex"); - *state = size; -} - -fn get_max_page_size() -> usize { - *DB_COMMAND_PAGE_SIZE.lock().unwrap() -} - - -#[cfg(test)] -mod tests { - use super::*; - - use anki::backend_proto::{sql_value, Row, SqlValue}; - use crate::dbcommand::{Sizable, select_slice_of_size}; - use std::borrow::Borrow; - - fn gen_data() -> Vec { - vec![ - SqlValue{ - data: Some(sql_value::Data::DoubleValue(12.0)) - }, - SqlValue{ - data: Some(sql_value::Data::LongValue(12)) - }, - SqlValue{ - data: Some(sql_value::Data::StringValue("Hellooooooo World".to_string())) - }, - SqlValue{ - data: Some(sql_value::Data::BlobValue(vec![])) - } - ] - } - - #[test] - fn test_size_estimate() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone(), row.clone()] }; - - let actual_size = result.estimate_size(); - - let expected_size = (17 + 8 + 8) * 2; // 1 variable string, 1 long, 1 float - let expected_overhead = (4 * 1) * 2; // 4 optional columns - - assert_eq!(actual_size, expected_overhead + expected_size); - } - - #[test] - fn test_stream_size() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone(), row.clone(), row.clone()] }; - let limit = 74 + 1; // two rows are 74 - - let result = select_slice_of_size(result.rows.iter(), limit).into_inner(); - - assert_eq!(2, result.1.len(), "The final element should not be included"); - assert_eq!(74, result.0, "The size should be the size of the first two objects"); - } - - #[test] - fn test_stream_size_too_small() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone()] }; - let limit = 1; - - let result = select_slice_of_size(result.rows.iter(), limit).into_inner(); - - assert_eq!(1, result.1.len(), "If the limit is too small, a result is still returned"); - assert_eq!(37, result.0, "The size should be the size of the first objects"); - } - - const BACKEND_PTR: i64 = 12; - const SEQUENCE_NUMBER: i32 = 1; - - fn get(index : i64) -> Option { - unsafe { return get_next(BACKEND_PTR, SEQUENCE_NUMBER, index) }; - } - - fn get_first(result : DbResult) -> DbResponse { - trim_and_cache_remaining(BACKEND_PTR, result, SEQUENCE_NUMBER) - } - - fn seq_number_used() -> bool { - HASHMAP.lock().unwrap().get(&BACKEND_PTR).unwrap().contains_key(&SEQUENCE_NUMBER) - } - - #[test] - fn integration_test() { - let row = Row { fields: gen_data() }; - - // return one row at a time - set_max_page_size(row.estimate_size() - 1); - - let db_query_result = DbResult { rows: vec![row.clone(), row.clone()] }; - - let first_jni_response = get_first(db_query_result); - - assert_eq!(row_count(&first_jni_response), 1, "The first call should only return one row"); - - let next_index = first_jni_response.start_index + row_count(&first_jni_response); - - let second_response = get(next_index); - - assert!(second_response.is_some(), "The second response should return a value"); - let valid_second_response = second_response.unwrap(); - assert_eq!(row_count(&valid_second_response), 1); - - let final_index = valid_second_response.start_index + row_count(&valid_second_response); - - assert!(seq_number_used(), "The sequence number is assigned"); - - let final_response = get(final_index); - assert!(final_response.is_some(), "The third call should return something with no rows"); - assert_eq!(row_count(&final_response.unwrap()), 0, "The third call should return something with no rows"); - assert!(!seq_number_used(), "Sequence number data has been cleared"); - } - - fn row_count(resp: &DbResponse) -> i64 { - resp.result.as_ref().map(|x| x.rows.len()).unwrap_or(0) as i64 - } -} \ No newline at end of file diff --git a/rslib-bridge/src/lib.rs b/rslib-bridge/src/lib.rs index 86194b872..2a52ed06b 100644 --- a/rslib-bridge/src/lib.rs +++ b/rslib-bridge/src/lib.rs @@ -1,565 +1,130 @@ -#[macro_use] -extern crate lazy_static; +#![allow(clippy::missing_safety_doc)] +use anki::pb::{backend_error, BackendError, Int64}; +use jni::objects::{JClass, JObject}; +use jni::sys::{jarray, jbyteArray, jint, jlong}; use jni::JNIEnv; -use jni::objects::{JClass, JString, JObject}; -use jni::sys::{jbyteArray, jint, jlong, jobjectArray, jarray, jstring}; -use anki::{backend_proto as pb, log}; -use pb::OpenCollectionIn; -use crate::sqlite::{open_collection_ankidroid, insert_for_id, query_for_affected, get_open_collection_for_downgrade}; - -use anki::backend::{init_backend, anki_error_to_proto_error, Backend}; -use crate::ankidroid::AnkiDroidBackend; - -// allows encode/decode +use anki::backend::{init_backend, Backend}; +use anki::error::{AnkiError, Result}; +use anki::i18n::I18n; use prost::Message; -use std::panic::{catch_unwind, AssertUnwindSafe}; use std::any::Any; -use anki::err::AnkiError; -use anki::i18n::I18n; - -use anki::backend_proto::{DbResult, DbResponse}; -use core::result; -use crate::backend_proto::{DroidBackendService, LocalMinutesWestIn, SchedTimingTodayIn, SchedTimingTodayOut2, LocalMinutesWestOut, DebugActiveDatabaseSequenceNumbersIn, DebugActiveDatabaseSequenceNumbersOut}; -use anki::sched::cutoff; -use anki::timestamp::TimestampSecs; -use anki::sched::cutoff::SchedTimingToday; - -mod dbcommand; -mod sqlite; -mod ankidroid; -mod backend_proto; - -// TODO: Use a macro to handle panics to reduce code duplication - -impl From for SchedTimingTodayOut2 { - fn from(data: SchedTimingToday) -> Self { - SchedTimingTodayOut2 { - days_elapsed: data.days_elapsed, - next_day_at: data.next_day_at - } - } -} - -impl backend_proto::DroidBackendService for Backend { - fn sched_timing_today_legacy(&self, input: SchedTimingTodayIn) -> Result { - let result = cutoff::sched_timing_today( - TimestampSecs::from(input.created_secs), - TimestampSecs::from(input.now_secs), - Some(input.created_mins_west), - Some(input.now_mins_west), - Some(input.rollover_hour as u8) - ); - Ok(SchedTimingTodayOut2::from(result)) - } - - fn local_minutes_west_legacy(&self, input : LocalMinutesWestIn) -> Result { - let out = LocalMinutesWestOut { - mins_west: cutoff::local_minutes_west_for_stamp(input.collection_creation_time) - }; - Ok(out) - } +use std::panic::{catch_unwind, AssertUnwindSafe}; - fn debug_active_database_sequence_numbers(&self, input: DebugActiveDatabaseSequenceNumbersIn) -> Result { - let backend_ptr = input.backend_ptr.clone(); - let result = DebugActiveDatabaseSequenceNumbersOut { - sequence_numbers: dbcommand::active_sequences(backend_ptr) - }; - Ok(result) - } -} +mod logging; #[no_mangle] pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_openBackend( env: JNIEnv, _: JClass, - args: jbyteArray) -> jlong { - // TODO: This does not handle panics - we currently return a pointer - convert to protobuf - - let rust_backend = init_backend(env.convert_byte_array(args).unwrap().as_slice()).unwrap(); - let backend = AnkiDroidBackend::new(rust_backend); - - - Box::into_raw(Box::new(backend)) as jlong -} - -/// Produces an error -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_debugProduceError( - _env: JNIEnv, - _: JClass, - backend_ptr : jlong, - error : JString) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - // We need to go from string -> AnkiError -> BackendError. BackendError is less expressive than - // AnkiError, and we want to test all possibilities. - let error_type_string: String = _env.get_string(error).expect("Couldn't get java string!").into(); - - use anki::err::NetworkErrorKind as Net; - use anki::err::SyncErrorKind as Sync; - use anki::err::DBErrorKind as DB; - - let error_value = "error_value".to_string(); - let err = match error_type_string.as_ref() { - "InvalidInput" => AnkiError::InvalidInput { info: error_value }, - "TemplateError" => AnkiError::TemplateError { info: error_value }, - "TemplateSaveError" => AnkiError::TemplateSaveError { ordinal: 1 }, - "IOError" => AnkiError::IOError { info: error_value }, - "DbErrorFileTooNew" => AnkiError::DBError { info: error_value, kind: DB::FileTooNew }, - "DbErrorFileTooOld" => AnkiError::DBError { info: error_value, kind: DB::FileTooOld }, - "DbErrorMissingEntity" => AnkiError::DBError { info: error_value, kind: DB::MissingEntity }, - "DbErrorCorrupt" => AnkiError::DBError { info: error_value, kind: DB::Corrupt }, - "DbErrorLocked" => AnkiError::DBError { info: error_value, kind: DB::Locked }, - "DbErrorOther" => AnkiError::DBError { info: error_value, kind: DB::Other }, - "NetworkErrorOffline" => AnkiError::NetworkError { info: error_value, kind: Net::Offline}, - "NetworkErrorTimeout" => AnkiError::NetworkError { info: error_value, kind: Net::Timeout}, - "NetworkErrorProxyAuth" => AnkiError::NetworkError { info: error_value, kind: Net::ProxyAuth}, - "NetworkErrorOther" => AnkiError::NetworkError { info: error_value, kind: Net::Other}, - "SyncErrorConflict" => AnkiError::SyncError { info: error_value, kind: Sync::Conflict }, - "SyncErrorServerError" => AnkiError::SyncError { info: error_value, kind: Sync::ServerError }, - "SyncErrorClientTooOld" => AnkiError::SyncError { info: error_value, kind: Sync::ClientTooOld }, - "SyncErrorAuthFailed" => AnkiError::SyncError { info: error_value, kind: Sync::AuthFailed }, - "SyncErrorServerMessage" => AnkiError::SyncError { info: error_value, kind: Sync::ServerMessage }, - "SyncErrorClockIncorrect" => AnkiError::SyncError { info: error_value, kind: Sync::ClockIncorrect}, - "SyncErrorOther" => AnkiError::SyncError { info: error_value, kind: Sync::Other }, - "SyncErrorResyncRequired" => AnkiError::SyncError { info: error_value, kind: Sync::ResyncRequired }, - "SyncErrorDatabaseCheckRequired"=> AnkiError::SyncError { info: error_value, kind: Sync::DatabaseCheckRequired }, - "JSONError" => AnkiError::JSONError { info: error_value}, - "ProtoError" => AnkiError::ProtoError { info: error_value}, - "Interrupted" => AnkiError::Interrupted, - "CollectionNotOpen" => AnkiError::CollectionNotOpen, - "CollectionAlreadyOpen" => AnkiError::CollectionAlreadyOpen, - "NotFound" => AnkiError::NotFound, - "Existing" => AnkiError::Existing, - "DeckIsFiltered" => AnkiError::DeckIsFiltered, - "SearchError" => AnkiError::SearchError(Some(error_value)), - "FatalError" => AnkiError::FatalError { info: error_value}, - unknown => AnkiError::FatalError { info: format!("Unknown Error code: {}", unknown) }, - }; - - let error_as_proto = anki_error_to_proto_error(err, &backend.backend.i18n); - - let mut bytes = Vec::new(); - error_as_proto.encode(&mut bytes).unwrap(); - _env.byte_array_from_slice(bytes.as_slice()).unwrap() + args: jbyteArray, +) -> jarray { + let logger = logging::setup_logging(); + + let input = env.convert_byte_array(args).unwrap(); + let result = init_backend(&input, Some(logger)) + .map(|backend| { + let backend_ptr = Box::into_raw(Box::new(backend)) as i64; + Int64 { val: backend_ptr }.encode_to_vec() + }) + .map_err(|err| { + BackendError { + localized: err, + kind: backend_error::Kind::FatalError as i32, + ..Default::default() + } + .encode_to_vec() + }); + pack_result(result, &env) } - #[no_mangle] pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_closeBackend( _env: JNIEnv, _: JClass, - args: jlong) -> jlong { - // TODO: This does not handle panics - we currently return a pointer - convert to protobuf - - let raw = args as *mut AnkiDroidBackend; + args: jlong, +) { + let raw = args as *mut Backend; Box::from_raw(raw); - - 1 } - #[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_openCollection( +pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_runMethodRaw( env: JNIEnv, _: JClass, backend_ptr: jlong, - args: jbyteArray) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(args).unwrap(); - - let command = OpenCollectionIn::decode(in_bytes.as_slice()).unwrap(); - - let ret = open_collection_ankidroid(&backend.backend,command).and_then(|empty| { - let mut out_bytes = Vec::new(); - empty.encode(&mut out_bytes)?; - Ok(out_bytes) - }).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - }); - - match ret { - Ok(_s) => env.byte_array_from_slice(_s.as_slice()).unwrap(), - Err(_err) => env.byte_array_from_slice(_err.as_slice()).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_command( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - command: jint, + service: jint, + method: jint, args: jbyteArray, ) -> jbyteArray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let command: u32 = command as u32; - let in_bytes = env.convert_byte_array(args).unwrap(); - - // We might want to later change this to append a bit to the head of the stream to specify - // the return type. - - match backend.backend.run_command_bytes(command, &in_bytes) { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -/// Opens a database of V16 or earlier and transforms it into a database of V11 -/// This exists to allow -/// We do not require or keep a backend open for this operation: -/// We wouldn't be able to open a backend - openAnkiDroidCollection fails due to the schema mismatch -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_downgradeDatabase( - env: JNIEnv, - _: JClass, - path: JString) -> jstring { - - let i18n = I18n::new(&[""], "", log::default_logger(None).expect("logger failed")); - let result = catch_unwind(|| { - let collection_path: String = env.get_string(path).expect("Couldn't get java string!").into(); - - // Obtains a collection only if it's version 16. Otherwise throws. - // Cause: The .close() method does not check for schema versions, so can't downgrade - // if tables are missing - let collection = match get_open_collection_for_downgrade(collection_path) { - Ok(r) => r, - Err(err) => panic!(err.localized_description(&i18n)) - }; - - const DOWNGRADE_TO_11: bool = true; - if let Err(msg) = collection.close(DOWNGRADE_TO_11) { - panic!(msg.localized_description(&i18n)) - } - }); - - let result_string = match result { - Ok(_) => "".to_owned(), - Err(s) => panic_to_anki_error(s.as_ref()).localized_description(&i18n), - }; - - env.new_string(result_string) - .expect("Failed to produce string") - .into_inner() -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_executeAnkiDroidCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - command: jint, - args: jbyteArray, -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let command: u32 = command as u32; - let in_bytes = env.convert_byte_array(args).unwrap(); - - match run_ad_command_bytes(backend, command, &in_bytes) { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -pub(crate) fn run_ad_command_bytes(backend: &mut AnkiDroidBackend, method: u32, input: &[u8]) -> result::Result, Vec> { - backend.backend.run_command_bytes2_inner_ad(method, input).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes + let service: u32 = service as u32; + let method: u32 = method as u32; + let input = env.convert_byte_array(args).unwrap(); + with_packed_result(&env, backend.i18n(), || { + backend.run_method(service, method, &input) }) } -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_fullDatabaseCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - // Don't map the error for now - let out_res = backend.backend.run_db_command_bytes(&in_bytes); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_databaseGetNextResultPage( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - sequence_number: jint, - requested_index: jlong -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - - let next_page = dbcommand::get_next( - backend_ptr, - sequence_number, - requested_index - ).unwrap(); - - - let mut out_bytes = Vec::new(); - next_page.encode(&mut out_bytes).unwrap(); - env.byte_array_from_slice(&out_bytes).unwrap() - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_cancelCurrentProtoQuery( - _: JNIEnv, - _: JClass, - backend_ptr : jlong, - sequence_number: jint -) { - dbcommand::flush_cache(&backend_ptr, sequence_number); +unsafe fn to_backend(ptr: jlong) -> &'static mut Backend { + &mut *(ptr as *mut Backend) } -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_cancelAllProtoQueries( - _: JNIEnv, - _: JClass, - backend_ptr : jlong -) { - dbcommand::flush_all(&backend_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_databaseCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - // Normally we'd want this as a Vec, but - let out_res = backend.backend.run_db_command_proto(&in_bytes); - - match out_res { - Ok(db_result) => { - let trimmed = dbcommand::trim_and_cache_remaining(backend_ptr, db_result, dbcommand::next_sequence_number()); - - let mut out_bytes = Vec::new(); - trimmed.encode(&mut out_bytes).unwrap(); - env.byte_array_from_slice(&out_bytes).unwrap() - } - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_sqlInsertForId( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - let out_res = insert_for_id(&in_bytes, backend); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), +macro_rules! null_on_error { + ($arg:expr) => { + match ($arg) { + Ok(ok) => ok, + Err(_) => return JObject::null().into_inner(), } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } - - + }; } -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_sqlQueryForAffected( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - let out_res = query_for_affected(&in_bytes, backend); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); +/// Run provided func and pack result into jarray. Catches panics. +fn with_packed_result(env: &JNIEnv, tr: &I18n, func: F) -> jarray +where + F: FnOnce() -> Result, Vec>, +{ + let result = match catch_unwind(AssertUnwindSafe(func)) { + Ok(result) => result, + Err(panic) => Err(panic_to_anki_error(panic).into_protobuf(tr).encode_to_vec()), + }; + pack_result(result, env) +} + +/// Pack Result into jArray[okBytes, null] | jarray[null, errBytes] | null +/// Null returned in case conversion to a jbyteArray fails (eg low mem), +fn pack_result(result: Result, Vec>, env: &JNIEnv) -> jarray { + // create the outer 2-element array + let byte_array_class = null_on_error!(env.find_class("[B")); + let outer_array = null_on_error!(env.new_object_array(2, byte_array_class, JObject::null())); + // pack return/error into bytearrays + let elems = match result { + Ok(msg) => ( + null_on_error!(env.byte_array_from_slice(&msg)), + JObject::null().into_inner(), + ), + Err(err) => ( + JObject::null().into_inner(), + null_on_error!(env.byte_array_from_slice(&err)), + ), + }; + // pack into outer + null_on_error!(env.set_object_array_element(outer_array, 0, elems.0)); + null_on_error!(env.set_object_array_element(outer_array, 1, elems.1)); - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } + outer_array } - -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_getColumnNames( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : JString -) -> jarray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - - let ret = backend.backend.with_col(|col| { - - let str : String = env.get_string(input).expect("Couldn't get java string!").into(); - let stmt = col.storage.db.prepare(&str)?; - let names = stmt.column_names(); - - let array: jobjectArray = env - .new_object_array( - names.len() as i32, - env.find_class("java/lang/String").unwrap(), - *env.new_string("").unwrap(), - ) - .unwrap(); - - - for (i, name) in names.iter().enumerate() { - env.set_object_array_element( - array, - i as i32, - *env.new_string(&name) - .unwrap() - .to_owned(), - ) - .expect("Could not perform set_object_array_element on array element."); - } - Ok(array) - }); - - match ret { - Ok(_s) => _s, - // This may be incorrect - Err(_) => *JObject::null() +fn panic_to_anki_error(panic: Box) -> AnkiError { + AnkiError::FatalError( + match panic.downcast_ref::<&'static str>() { + Some(msg) => *msg, + None => match panic.downcast_ref::() { + Some(msg) => msg.as_str(), + None => "unknown panic", + }, } - - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } + .to_string(), + ) } - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_setDbPageSize( - _: JNIEnv, - _: JClass, - page_size: jlong) { - dbcommand::set_max_page_size(page_size as usize); -} - -unsafe fn to_backend(ptr: jlong) -> &'static mut AnkiDroidBackend { - // TODO: This is not unwindable, but we can't hard-crash as Android won't send it to ACRA - // As long as the FatalError is sent below, we're OK - &mut *(ptr as *mut AnkiDroidBackend) -} - -fn panic_to_bytes(env: JNIEnv , s: &(dyn Any + Send), i18n: &I18n) -> jbyteArray { - let ret = panic_to_anki_error(s); - let backend_err = anki_error_to_proto_error(ret, i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - env.byte_array_from_slice(bytes.as_slice()).unwrap() -} - -fn panic_to_anki_error(s: &(dyn Any + Send)) -> AnkiError { - if let Some(msg) = s.downcast_ref::(){ - AnkiError::FatalError { - info: msg.to_string() - } - } else { - // The TypeId (the only thing you can reasonably get from Any) doesn't carry the type name - // Confirm an as_ref() rather than a borrow was passed in here. - AnkiError::FatalError { - info: "panic with unknown info".to_string() - } - } -} \ No newline at end of file diff --git a/rslib-bridge/src/logging.rs b/rslib-bridge/src/logging.rs new file mode 100644 index 000000000..8fce2de40 --- /dev/null +++ b/rslib-bridge/src/logging.rs @@ -0,0 +1,99 @@ +//! A simple adaptor that takes log messages from the backend and sends them to +//! the Android logs. +//! It also captures stdout/stderr output, and feeds it to logcat, to make it +//! easier to debug issues with dbg!()/println!() + +use android_logger::{Config, FilterBuilder}; +use log::Level; +use slog::*; +use std::io::{BufRead, BufReader}; +use std::time::Duration; +use std::{fmt, result}; + +use gag::BufferRedirect; + +pub struct AndroidSerializer; + +impl Serializer for AndroidSerializer { + fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments<'_>) -> Result { + log::debug!("{}={}", key, val); + Ok(()) + } +} + +pub struct AndroidDrain; + +impl Drain for AndroidDrain { + type Ok = (); + type Err = (); + + fn log( + &self, + record: &Record<'_>, + values: &OwnedKVList, + ) -> result::Result { + log::debug!("{}", record.msg()); + + record + .kv() + .serialize(record, &mut AndroidSerializer) + .unwrap(); + values.serialize(record, &mut AndroidSerializer).unwrap(); + + Ok(()) + } +} + +fn redirect_io() -> Result<()> { + monitor_io_handle(BufferRedirect::stdout()?); + monitor_io_handle(BufferRedirect::stderr()?); + Ok(()) +} + +fn monitor_io_handle(handle: BufferRedirect) { + let mut handle = BufReader::new(handle); + + std::thread::spawn(move || { + let mut buf = String::new(); + loop { + buf.truncate(0); + match handle.read_line(&mut buf) { + Ok(0) => { + // currently EOF + std::thread::sleep(Duration::from_secs(1)); + } + Ok(_) => { + if !should_ignore_line(&buf) { + log::debug!("{}", buf) + } + } + Err(err) => log::debug!("stdio err: {}", err), + } + } + }); +} + +fn should_ignore_line(buf: &str) -> bool { + // quieten simulator noise + if buf.starts_with("s_glBindAttribLocation") { + true + } else { + false + } +} + +pub(crate) fn setup_logging() -> Logger { + // failure is expected after the first backend invocation + let _ = redirect_io(); + + let filter = format!( + "{},rsdroid::logging=debug", + std::env::var("RUST_LOG").unwrap_or_else(|_| "error".into()) + ); + android_logger::init_once( + Config::default() + .with_min_level(Level::Debug) + .with_filter(FilterBuilder::new().parse(&filter).build()), + ); + Logger::root(slog_envlogger::new(AndroidDrain {}).fuse(), slog_o!()) +} diff --git a/rslib-bridge/src/sqlite.rs b/rslib-bridge/src/sqlite.rs deleted file mode 100644 index 8a1ec23c0..000000000 --- a/rslib-bridge/src/sqlite.rs +++ /dev/null @@ -1,215 +0,0 @@ -/* -While porting: open_collection upgrades the collection in a non-backwards compatible manner. - -We should be able to perform a database migration to V13 as we're past SQLite 3.9 - - - - */ - -use std::path::{PathBuf, Path}; -use anki::i18n::I18n; -use anki::log::Logger; -use anki::collection::{Collection, CollectionState}; -use anki::err::{DBErrorKind, AnkiError}; -use anki::storage::SqliteStorage; -use anki::backend::{Backend, anki_error_to_proto_error}; -use anki::{backend_proto as pb, log}; -use anki::config; - -use rusqlite::{params, NO_PARAMS}; - - -// allows encode/decode -use prost::Message; - -use crate::ankidroid::AnkiDroidBackend; - -#[derive(Deserialize)] -struct DBArgs { - sql: String, - args: Vec -} -use serde_derive::Deserialize; -use anki::sched::cutoff::v1_creation_date; - -pub type AnkiResult = std::result::Result; - -pub fn open_collection_no_update>( - path: P, - media_folder: P, - media_db: P, - server: bool, - i18n: I18n, - log: Logger, - min_schama: u8, - max_schema: u8 -) -> AnkiResult { - let col_path = path.into(); - let storage = open_or_create_no_update(&col_path, &i18n, server, min_schama, max_schema)?; - - let col = Collection { - storage, - col_path, - media_folder: media_folder.into(), - media_db: media_db.into(), - i18n, - log, - server, - state: CollectionState::default(), - }; - - Ok(col) -} - -pub fn open_collection_ankidroid(backend : &Backend, input: pb::OpenCollectionIn) -> AnkiResult { - let mut col = backend.col.lock().unwrap(); - if col.is_some() { - return Err(AnkiError::CollectionAlreadyOpen); - } - - let mut path = input.collection_path.clone(); - path.push_str(".log"); - - let log_path = match input.log_path.as_str() { - "" => None, - path => Some(path), - }; - let logger = log::default_logger(log_path)?; - - const SCHEMA_ANKIDROID_VERSION: u8 = 11; - - let new_col = open_collection_no_update( - input.collection_path, - input.media_folder_path, - input.media_db_path, - false, - backend.i18n.clone(), - logger, - SCHEMA_ANKIDROID_VERSION, - SCHEMA_ANKIDROID_VERSION - )?; - - *col = Some(new_col); - - Ok(().into()) -} - -pub fn get_open_collection_for_downgrade(collection_path: String) -> AnkiResult { - let logger = log::default_logger(None)?; - - const SCHEMA_ANKIDROID_MAX_VERSION: u8 = 16; - - open_collection_no_update( - collection_path, - "".to_owned(), - "".to_owned(), - false, - I18n::new(&[""], "", logger.clone()), - logger, - SCHEMA_ANKIDROID_MAX_VERSION, - SCHEMA_ANKIDROID_MAX_VERSION - ) -} - - -pub(crate) fn open_or_create_no_update(path: &Path, _i18n: &I18n, _server: bool, current_schema: u8, max_schema : u8) -> AnkiResult { - let db = anki::storage::sqlite::open_or_create_collection_db(path)?; - - let (create, ver) = anki::storage::sqlite::schema_version(&db)?; - // Requery uses "TRUNCATE" by default if WAL is not enabled. - // We copy this behaviour here. See https://github.com/ankidroid/Anki-Android/pull/7977 for - // analysis. We may be able to enable WAL at a later time. - db.pragma_update(None, "journal_mode", &"TRUNCATE")?; - - let err = match ver { - v if v < current_schema => Some(DBErrorKind::FileTooOld), - v if v > max_schema => Some(DBErrorKind::FileTooNew), - _ => None, - }; - if let Some(kind) = err { - return Err(AnkiError::DBError { - info: "Got Schema".to_owned() + &*ver.to_string(), - kind, - }); - } - - if !create { - return Ok(SqliteStorage { db }) - } - - - db.execute("begin exclusive", NO_PARAMS)?; - db.execute_batch(include_str!("../anki/rslib/src/storage/schema11.sql"))?; - // start at schema 11, then upgrade below - let crt = v1_creation_date(); - db.execute( - "update col set crt=?, scm=?, ver=?, conf=?", - params![ - crt, - crt * 1000, - current_schema, - &config::schema11_config_as_string() - ], - )?; - - let storage = SqliteStorage { db }; - - // storage.add_default_deck_config(i18n)?; - // storage.add_default_deck(i18n)?; - // storage.add_stock_notetypes(i18n)?; - - storage.commit_trx()?; - - - Ok(storage) -} - - -fn get_args(in_bytes: &Vec) -> AnkiResult { - let ret : DBArgs = serde_json::from_slice(&in_bytes)?; - Ok(ret) -} - - -pub(crate) fn insert_for_id(in_bytes: &Vec, backend: &mut AnkiDroidBackend) -> Result, Vec> { - - let req = get_args(&in_bytes) - .and_then(|req| { - backend.backend.with_col(|col| { - col.storage.db.execute(&req.sql, req.args)?; - Ok(col.storage.db.last_insert_rowid()) - }) - }) - .map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - })?; - - let mut out_bytes : Vec = Vec::new(); - pb::Int64 { val: req }.encode(&mut out_bytes); - Ok(out_bytes) -} - -pub(crate) fn query_for_affected(in_bytes: &Vec, backend: &mut AnkiDroidBackend) -> Result, Vec> { - - let req = get_args(&in_bytes) - .and_then(|req| { - backend.backend.with_col(|col| { - Ok(col.storage.db.execute(&req.sql, req.args)?) - }) - }) - .map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - })?; - - let mut out_bytes : Vec = Vec::new(); - let as_i32 : i32 = req as i32; - pb::Int32 { val: as_i32 }.encode(&mut out_bytes); - Ok(out_bytes) -} diff --git a/test-current.sh b/test-current.sh new file mode 100755 index 000000000..b8a496011 --- /dev/null +++ b/test-current.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +NO_CROSS=true CURRENT_ONLY=true ./gradlew rsdroid:test rsdroid-instrumented:connectedAndroidTest + diff --git a/tools/doctor.sh b/tools/doctor.sh index 3460ef8a6..e7631244f 100755 --- a/tools/doctor.sh +++ b/tools/doctor.sh @@ -62,9 +62,9 @@ else fi -cecho $lgray "Installing rust 1.54.0" # nightly" - temporarily using 1.54.0 - see #168 -rustup install 1.54.0 #nightly -rustup default 1.54.0 +cecho $lgray "Installing rust 1.58.1" +rustup install 1.58.1 +rustup default 1.58.1 cecho $lgray "Adding rust android targets" @@ -90,4 +90,10 @@ if [[ $(pip3 install protobuf) ]]; then else echo -e "Try installing with python 3.7" error_echo "Failed installing Protobuf python libraries" +fi + +if [[ $(pip3 install stringcase) ]]; then + ok_echo "Stringcase is installed" +else + error_echo "Failed installing stringcase python library" fi \ No newline at end of file diff --git a/tools/gen-fluent-proto/gen.py b/tools/gen-fluent-proto/gen.py deleted file mode 100644 index c0e2c0bc3..000000000 --- a/tools/gen-fluent-proto/gen.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -# Currently unused -# Generates fluent.proto from fluent translation (ftl) files. - - -import sys -import os -from fluent.syntax import parse -from fluent.syntax import ast - -# requires: pip install fluent.syntax - -def get_identifiers(fileData): - idents = [] - for message in parse(fileData).body: - if not isinstance(message, ast.Message): - continue - idents.append(message.id.name) - sorted(idents) - return idents - -def proto_enum(idents): - buf = r"""// This file is automatically generated as part of the build process. - -syntax = "proto3"; -package BackendProto; -enum FluentString { -""" - - - for (idx, s) in enumerate(idents): - name = s.replace("-", "_").upper() - buf += " {} = {};\n".format(name, idx) - - buf += "}\n" - - return buf - -if __name__ == "__main__": - path = sys.argv[1] - buffer = "" - - for file in os.listdir(path): - if not file.endswith(".ftl"): - continue - with open(os.path.join(path, file)) as f: - for l in f.readlines(): - buffer += l + "\n" - - idents = get_identifiers(buffer) - buf = proto_enum(idents) - print(buf) - - - \ No newline at end of file diff --git a/tools/gen-fluent-proto/readme.md b/tools/gen-fluent-proto/readme.md deleted file mode 100644 index 63e94c30e..000000000 --- a/tools/gen-fluent-proto/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -This tool generates `fluent.proto` from the fluent translation (ftl) files. - -**Unused. May be useful for future** - -We currently use the rust submodule. `build.rs` performs the same operation as long as it executes first. \ No newline at end of file diff --git a/tools/genfluent/genfluent.bat b/tools/genfluent/genfluent.bat new file mode 100644 index 000000000..f5e1dd0cf --- /dev/null +++ b/tools/genfluent/genfluent.bat @@ -0,0 +1,2 @@ +@ECHO OFF +python "%~dp0\genfluent.py" \ No newline at end of file diff --git a/tools/genfluent/genfluent.py b/tools/genfluent/genfluent.py new file mode 100755 index 000000000..25fc5062c --- /dev/null +++ b/tools/genfluent/genfluent.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +from typing import List, Literal, TypedDict +import stringcase +import re + +def ensure_i18n_module_correct(): + reg = re.compile(r'(\s+)(\S+_(?:commit|zip_csum)) = "(.*)"') + for line in open("rslib-bridge/anki/repos.bzl").readlines(): + m = reg.match(line) + if m: + (indent, key, commit) = m.groups() + if key == "core_i18n_commit": + subprocess.run(["git", "fetch"], cwd="ftl/core", check=True) + subprocess.run(["git", "checkout", commit], cwd="ftl/core", check=True) + break + +def get_strings(): + output_file = Path("output.json").absolute() + subprocess.run(["cargo", "run", output_file], check=True, cwd="rslib-bridge/anki/rslib/i18n") + data = json.load(open(output_file)) + output_file.unlink() + return data + +modules = get_strings() + +def build_source(): + out = """\ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +package anki.i18n; + +import anki.i18n.TranslateArgValue + +fun asTranslateArg(arg: Any): TranslateArgValue { + val builder = TranslateArgValue.newBuilder() + when (arg) { + is String -> builder.setStr(arg) + is Int -> builder.setNumber(arg.toDouble()) + is Double -> builder.setNumber(arg) + else -> throw Exception("invalid arg provided to translation") + } + return builder.build() +} + +// This should be either String, Double or Int +typealias TranslateArg = Any + +typealias TranslateArgMap = Map + +interface GeneratedTranslations { + fun translate(module: Int, translation: Int, args: TranslateArgMap): String; + + +""" + + out += methods() + out += "}" + + return out + +def write_source(): + source = build_source() + path = Path(f"rsdroid/build/generated/source/fluent/anki/GeneratedTranslations.kt") + if not path.parent.exists(): + path.parent.mkdir(parents=True) + open(path, "w").write(source) + + +class Variable(TypedDict): + name: str + kind: Literal["Any", "Int", "String", "Float"] + + +def methods() -> str: + out = [] + for module in modules: + for translation in module["translations"]: + key = stringcase.camelcase(translation["key"].replace("-", "_")) + arg_types = get_arg_types(translation["variables"]) + args = get_args(translation["variables"]) + doc = translation["text"] + out.append( + f""" + /** {doc} */ + fun {key}({arg_types}): String {{ + return translate({module["index"]}, {translation["index"]}, mapOf({args})) + }} +""" + ) + + return "\n".join(out) + "\n" + + +def get_arg_types(args: list[Variable]) -> str: + + return ", ".join( + [f"`{stringcase.camelcase(arg['name'])}`: {arg_kind(arg)}" for arg in args] + ) + + +def arg_kind(arg: Variable) -> str: + if arg["kind"] == "Int": + return "Int" + elif arg["kind"] == "Any": + return "TranslateArg" + elif arg["kind"] == "Float": + return "Double" + else: + return "String" + + +def get_args(args: list[Variable]) -> str: + return ", ".join( + [f'"{arg["name"]}" to asTranslateArg(`{stringcase.camelcase(arg["name"])}`)' for arg in args] + ) + +ensure_i18n_module_correct() +write_source() diff --git a/tools/genfluent/genfluent.sh b/tools/genfluent/genfluent.sh new file mode 100755 index 000000000..16551d4d9 --- /dev/null +++ b/tools/genfluent/genfluent.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +. tools/setup-python +$PYTHON ./tools/genfluent/genfluent.py diff --git a/tools/get-buildinfo.sh b/tools/get-buildinfo.sh new file mode 100755 index 000000000..e542655d8 --- /dev/null +++ b/tools/get-buildinfo.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# The buildinfo.txt file should be generated as part of the build, +# but for now we'll just check it into source control. + +if ! bazel --version > /dev/null 2>&1; then + echo "bazel: command not found. Please install Bazelisk. Your distro may have it," + echo "or you can fetch the binary from https://github.com/bazelbuild/bazelisk/releases" + echo "and rename it to /usr/local/bin/bazel" + exit 1 +fi + +(cd rslib-bridge/anki && bazel build -c opt buildinfo.txt) +cp rslib-bridge/anki/.bazel/bin/buildinfo.txt rslib-bridge \ No newline at end of file diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index dca687527..b5f3e666b 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -4,110 +4,165 @@ # This is the serialization mechanism/calling conventions for protobufs between rslib and rsdroid import sys +import stringcase from google.protobuf.compiler import plugin_pb2 as plugin +TYPE_ENUM = 14 + # Needs map<> and Fluent import rather than Backend ignore_methods_accepting = ["TranslateStringIn"] -backend_name = "Backend" - -class Method: - def __init__(self, method): - self.method = method - self.fields = method.field - - # https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor - def to_java_type(self, type, field): - ### Converts a given protobuf type to the Java Equivalent: (int, or List for example) ### - primitive_list = { - 1: "double", - 2 : "float", - 3 : "long", - # 4 : "uint64", - 5 : "int", - 8 : "boolean", - 9 : "java.lang.String", - 12 : "com.google.protobuf.ByteString", - 13: "int", #"uint32" - 17: "int", - 18: "long" - } - - def fix_namespace(f): - return f.replace(".BackendProto.", "Backend.") - - - if self.is_repeating(field): - primitive_map = {1 : "Double", - 2 : "Float", - 3 : "Long", - 5 : "Integer", - 8 : "Boolean", - 13: "Integer"} - if type != 14 and type != 11: - new_type = primitive_map[type] if type in primitive_map else primitive_list[type] - else: - new_type = fix_namespace(field.type_name) - return "java.util.List<{}>".format(new_type) +def fix_namespace(f): + return f.replace(".anki.", "anki.") + + +def basename(f): + return f.split(".")[-1] + + +def proto_name_to_symbol(f): + base = f.replace(".proto", "") + return base.replace("anki/", "") + +def proto_name_to_package(f): + base = f.replace(".proto", "") + head = base.replace("anki/", "anki.") + return head + +def get_annotation(type, optional=False): + primitive = type not in [9, 12, 11, 14] + if primitive: + return "" + elif optional: + return "@Nullable" + else: + return "@NonNull" + +def is_repeating(field): + return field.label == 3 + +def as_getter(field): + if is_repeating(field): + return f"{field.name}List" + else: + return field.json_name + +# https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor +def to_java_type(type, field, output=False): + ### Converts a given protobuf type to the Java Equivalent: (int, or List for example) ### + primitive_list = { + 1: "Double", + 2: "Float", + 3: "Long", + # 4 : "uint64", + 5: "Int", + 8: "Boolean", + 9: "String", + 12: "com.google.protobuf.ByteString", + 13: "Int", # "uint32" + 17: "Int", + 18: "Long", + } + + if is_repeating(field): + primitive_map = { + 1: "Double", + 2: "Float", + 3: "Long", + 5: "Int", + 8: "Boolean", + 13: "Int", + } + if type != 14 and type != 11: + new_type = ( + primitive_map[type] + if type in primitive_map + else primitive_list[type] + ) + else: + new_type = fix_namespace(field.type_name) + + if output: + return "List<{}>".format(new_type) + else: + return "Iterable<{}>".format(new_type) + + if type == 14 or type == 11: # enum/message + return fix_namespace(field.type_name) + + return primitive_list[type] + +class Message: + def __init__(self, message, proto_file): + self.method = message + self.fields = message.field + self.proto_file = proto_file - if type == 14 or type == 11: # enum/message - return fix_namespace(field.type_name) - return primitive_list[type] - def label_to_annotation(self, label): - return { - 1: "@Nullable ", # LABEL_OPTIONAL - 3: "" # LABEL_REPEATED - }[label] def as_param(self, field): - annotation = self.label_to_annotation(field.label) - # avoid annotations for primitives - if field.type not in [9, 12, 11, 14]: - annotation = "" - return "{}{} {}".format(annotation, self.to_java_type(field.type, field), field.json_name) + optional = getattr(field, "proto3_optional") + annotation = "?" if optional else "" + name = field.json_name + if name == "val": + name = "`val`" - def is_repeating(self, field): - return field.label == 3 + return "{}: {}{}".format( + name, to_java_type(field.type, field), annotation, + ) def as_setter_name(self, field): # We have a method: setXXX, so the first letter is uppercase return field[0].upper() + field[1:] def as_setter(self, field): - prefix = "set" if not self.is_repeating(field) else "addAll" - return ".{}{}({})".format(prefix, self.as_setter_name(field.json_name), field.json_name) + optional = getattr(field, "proto3_optional", False) + prefix = "set" if not is_repeating(field) else "addAll" + name = field.json_name + if name == "val": + name = "`val`" + + return ".{}{}({})".format( + prefix, self.as_setter_name(field.json_name), "it" if optional else name + ) def getFieldSetters(self): return [(self.as_setter(f), f) for f in self.fields] def is_primitive(self, field): - return not self.is_repeating(field) and field.type not in [9, 12, 11, 14] + return not is_repeating(field) and field.type not in [9, 12, 11, 14] def as_builder(self): # we can't set fields to null, so we can't use the builder fluent syntax. - ret = "{namespace}.{methodName}.Builder builder = {namespace}.{methodName}.newBuilder();\n".format(methodName=self.method.name, namespace=backend_name) + ret = "val builder = {namespace}.{methodName}.newBuilder();\n".format( + methodName=self.method.name, namespace=self.proto_file.package + ) for setter, field in self.getFieldSetters(): - if self.is_primitive(field): - ret += " builder{};\n".format(setter) + if not getattr(field, "proto3_optional", False): # self.is_primitive(field): + ret += " builder{};\n".format(setter) else: - ret += " if ({} != null) {{ builder{}; }}\n".format(field.json_name, setter) - - - ret += " {}.{} protobuf = builder.build();\n".format(backend_name, self.method.name) - return ret + ret += " {}?.let {{ builder{}; }}\n".format( + field.json_name, setter + ) + ret += " val input = builder.build();\n".format( + self.proto_file.package, self.method.name + ) + return ret.replace("I18n.", "I18N.") def as_params(self): return ", ".join([self.as_param(f) for f in self.fields]) + class RPC: - def __init__(self, service, command_num, methods): + def __init__(self, service, service_index, command_num, messages, proto_file): self.method = service + self.service_index = service_index self.command_num = command_num - self.method_lookup = methods + self.messages = messages + self.proto_file = proto_file def is_valid(self): return self.get_input() and self.get_output() @@ -122,241 +177,178 @@ def parse(self, str, input): def parse_input(str): if str == ".BackendProto.Empty": return "()" - return "(" + str.replace(".BackendProto.", backend_name + ".") + " args)" + return "(" + fix_namespace(str) + " args)" @staticmethod def parse_output(str): - if str == ".BackendProto.Empty": + str = fix_namespace(str) + if str == "anki.generic.Empty": return "void" - return str.replace(".BackendProto.", backend_name + ".") + return str def get_input(self): return self.parse(self.method.input_type, True) def get_output(self): return self.parse(self.method.output_type, False) - + def as_command_name(self): - return "case {}: return \"{}\";".format(self.command_num, self.method_name()) - - def as_interface(self): - if self.get_output() == "void": - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params())) - else: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv=self.get_input()) - else: - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params())) - else: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv=self.get_input()) + return 'case {}: return "{}";'.format(self.command_num, self.method_name()) def __repr__(self): - args = "args.toByteArray()" if self.get_input() != "()" else "Backend.Empty.getDefaultInstance().toByteArray()" - # These previously were very different - validation changed this. - # Might want to merge these branches now they're so similar. - - if self.get_output() == "void": - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " {deser}\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, protobuf.toByteArray());\n" \ - " Backend.Empty message = Backend.Empty.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params()), - num=self.command_num, - deser=self.method_lookup[k].as_builder()) - else: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, {args});\n" \ - " Backend.Empty message = Backend.Empty.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv=self.get_input(), - num=self.command_num, - args=args) + input_type_name = fix_namespace(self.method.input_type) + output_type_name = fix_namespace(self.method.output_type) + input_msg = self.messages[input_type_name] + out=self.get_output() + name=self.method_name() + inv="({})".format(self.messages[input_type_name].as_params()) + service=self.service_index + method=self.command_num + deser=self.messages[input_type_name].as_builder() + + if name in ("latestProgress", "syncMedia", "translateString", "awaitBackupCompletion"): + raw_method = "runMethodRawNoLock" else: - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " {deser}\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, protobuf.toByteArray());\n" \ - " {out} message = {out}.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " return message;\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params()), - num=self.command_num, - deser=self.method_lookup[k].as_builder()) - else: - # Definitely empty - and TranslateStringIn (manually ignored) - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, {args});\n" \ - " {out} message = {out}.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " return message;\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv=self.get_input(), - num=self.command_num, - args=args) + raw_method = "runMethodRaw" + + buf = f""" +@Throws(BackendException::class) +fun {name}Raw(input: ByteArray): ByteArray {{ + return {raw_method}({service}, {method}, input); +}} +""" + + if input_type_name == "anki.i18n.TranslateStringRequest": + # maps not currently supported + return buf + + if ((input_type_name.endswith("Request") or len(input_msg.fields) < 2) and not contains_oneof(input_msg)): + # unroll input + pass + else: + # skip unroll input + inv=f"(input: {input_type_name})" + deser = "" + + output_msg = self.messages[output_type_name] + if ( + len(output_msg.fields) == 1 + and output_msg.fields[0].type != TYPE_ENUM + ): + # unwrap single return arg + f = output_msg.fields[0] + out = to_java_type(f.type, f, output=True) + single_attribute = f".`{as_getter(f)}`" + else: + single_attribute = "" + + if out == "void": + return_segment = f""" +{name}Raw(input.toByteArray()); + """ + out_with_colon = "" + else: + out_with_colon = f": {out}" + return_segment = f"""\ +try {{ + return {output_type_name}.parseFrom({name}Raw(input.toByteArray())){single_attribute}; +}} catch (exc: com.google.protobuf.InvalidProtocolBufferException) {{ + throw BackendException("protobuf parsing failed"); +}}""" + + buf += f""" +@Throws(BackendException::class) +open fun {name}{inv}{out_with_colon} {{ + {deser} + {return_segment} +}} + """ + + return buf def method_name(self): return self.method.name[0].lower() + self.method.name[1:] -def traverse(proto_file): +def gather_classes(proto_file, all_messages, service_index): classes = [] - allowed_params = {"NoteTypeID", "NoteID", "CardID", "DeckID", "DeckConfigID", "String", "Bool", "Int32", "Int64"} - methods = [Method(m) for m in proto_file.message_type if m.name.endswith("In") or m.name in allowed_params] - - method_lookup = {item.method.name: item for item in set(methods) if item.method.name not in ignore_methods_accepting} for f in proto_file.service: for i, m in enumerate(f.method): - cls = RPC(m, i + 1, method_lookup) + cls = RPC(m, service_index, i, all_messages, proto_file) if not cls.is_valid(): raise ValueError(str(m)) classes.append(cls) return classes + def logRepr(s): sys.stderr.write("\n".join(dir(s))) -def log(s): - sys.stderr.write(str(s) + "\n") - -def gen_backend_methods(file, proto_file, methods, class_name): - contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import BackendProto.Backend;\n\n" - "public class {} {{" - " public static String commandName(int command) {{\n" - " switch (command) {{".format(proto_file.name, __file__, class_name)] - - for method in methods: - contents.append(" " + str(method.as_command_name())) - - - contents.append(" default: return \"unknown: \" + command;\n }\n}") - contents.append("\n}") - file = response.file.add() - file.name = class_name + ".java" - file.content = "\n".join(contents) + +def log(*args): + print(*args, file=open("/tmp/log.txt", "a")) + def generate_code(request, response): - global backend_name + # gather all messages and service indexes + all_messages = {} + service_index = {} for proto_file in request.proto_file: + for message in proto_file.message_type: + all_messages[f"{proto_file.package}.{message.name}"] = Message(message, proto_file) + for enum in proto_file.enum_type: + if enum.name == "ServiceIndex": + for value in enum.value: + pkg = value.name.replace("SERVICE_INDEX_", "").lower() + if pkg == "deck_config": + pkg = "deckconfig" + service_index[f"anki.{pkg}"] = value.number - service_methods = traverse(proto_file) - if not service_methods: + file_contents = [ + """ +/* Auto-generated from the .proto files in AnkiDroidBackend. */ + +@file:Suppress("NAME_SHADOWING") + +package anki.backend; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.GeneratedMessageV3; + +import net.ankiweb.rsdroid.BackendException; + +public abstract class GeneratedBackend { + +@Throws(BackendException::class) +protected abstract fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray; +@Throws(BackendException::class) +protected abstract fun runMethodRawNoLock(service: Int, method: Int, input: ByteArray): ByteArray; + +""" + ] + + for proto_file in request.proto_file: + if not len(proto_file.service): continue - backend_name = proto_file.name.replace(".proto", "") - if backend_name == "backend": - backend_name = "Backend" - - class_name = proto_file.name.capitalize().replace(".proto", "").replace("Backend", "RustBackend") - current_import = "" if backend_name == "Backend" else "import BackendProto.{};".format(backend_name) - file_contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import com.google.protobuf.InvalidProtocolBufferException;\n" - "import com.google.protobuf.GeneratedMessageV3;\n\n" - "import BackendProto.Backend;\n" - "{currentImport}\n\n" - "public abstract class {cls}Impl implements net.ankiweb.rsdroid.{cls} {{\n\n" - " public abstract Pointer ensureBackend();\n\n\n" - " protected void validateMessage(byte[] result, GeneratedMessageV3 message) throws InvalidProtocolBufferException {{\n" - " if (message.getUnknownFields().asMap().isEmpty()) {{\n" - " return;\n" - " }}\n" - " Backend.BackendError ex = Backend.BackendError.parseFrom(result);\n" - " throw BackendException.fromError(ex);\n" - " }}" - "\n" - " protected abstract byte[] executeCommand(long backendPointer, final int command, byte[] args);\n" - "\n" - " protected void validateResult(@Nullable byte[] result) {{\n" - " if (result == null) {{\n" - " return;\n" - " }}\n" - " try {{\n" - " Backend.BackendError ex = Backend.BackendError.parseFrom(result);\n" - " throw BackendException.fromError(ex);\n" - " }} catch (InvalidProtocolBufferException e) {{\n" - " // ignore - throw the original exception\n" - " }}\n" - " }}".format(proto_file.name, __file__, cls=class_name, currentImport = current_import)] + service_methods = gather_classes(proto_file, all_messages, service_index[proto_file.package]) + if not service_methods: + continue for method in service_methods: file_contents.append("\n\n" + str(method)) - - file_contents.append("\n}") - - # Fill response - f = response.file.add() - f.name = class_name + "Impl.java" - f.content = "\n".join(file_contents) - - # generate interface (if methods) - - iface_contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import BackendProto.{backend};\n\n" - "public interface {} {{".format(proto_file.name, __file__, class_name, backend=backend_name)] - for method in service_methods: - iface_contents.append("\n" + str(method.as_interface())) - iface_contents.append("\n}") - iface = response.file.add() - iface.name = class_name + ".java" - iface.content = "\n".join(iface_contents) - # generate BackendMethods - - gen_backend_methods(response.file.add(), proto_file, service_methods, class_name + "Methods") + file_contents.append("\n}") + f = response.file.add() + f.name = "GeneratedBackend.kt" + f.content = "\n".join(file_contents) +def contains_oneof(msg): + return msg.method.oneof_decl -if __name__ == '__main__': +if __name__ == "__main__": # Read request message from stdin data = sys.stdin.buffer.read() @@ -366,6 +358,8 @@ def generate_code(request, response): # Create response response = plugin.CodeGeneratorResponse() + # fixme: check these are actually being handled + response.supported_features |= plugin.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL # Generate code generate_code(request, response) diff --git a/tools/protoc-gen/protoc-gen.sh b/tools/protoc-gen/protoc-gen.sh index d4039ecae..63912015a 100755 --- a/tools/protoc-gen/protoc-gen.sh +++ b/tools/protoc-gen/protoc-gen.sh @@ -1,2 +1,4 @@ #!/bin/bash -./tools/protoc-gen/protoc-gen.py + +. tools/setup-python +$PYTHON ./tools/protoc-gen/protoc-gen.py diff --git a/tools/protoc-gen/test/.gitignore b/tools/protoc-gen/test/.gitignore deleted file mode 100644 index 466e24805..000000000 --- a/tools/protoc-gen/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -out/ \ No newline at end of file diff --git a/tools/protoc-gen/test/backend.proto b/tools/protoc-gen/test/backend.proto deleted file mode 100644 index 6746726ce..000000000 --- a/tools/protoc-gen/test/backend.proto +++ /dev/null @@ -1,1021 +0,0 @@ -syntax = "proto3"; - -import "fluent.proto"; - -package BackendProto; - -option java_generic_services = true; - - -// Generic containers -/////////////////////////////////////////////////////////// - -message Empty {} - -message OptionalInt32 { - sint32 val = 1; -} - -message OptionalUInt32 { - uint32 val = 1; -} - -message Int32 { - sint32 val = 1; -} - -message UInt32 { - uint32 val = 1; -} - -message Int64 { - int64 val = 1; -} - -message String { - string val = 1; -} - -message Json { - bytes json = 1; -} - -message Bool { - bool val = 1; -} - -// IDs used in RPC calls -/////////////////////////////////////////////////////////// - -message NoteTypeID { - int64 ntid = 1; -} - -message NoteID { - int64 nid = 1; -} - -message CardID { - int64 cid = 1; -} - -message DeckID { - int64 did = 1; -} - -message DeckConfigID { - int64 dcid = 1; -} - -// New style RPC definitions -/////////////////////////////////////////////////////////// - -service BackendService { - rpc LatestProgress (Empty) returns (Progress); - rpc SetWantsAbort (Empty) returns (Empty); - - // card rendering - - rpc ExtractAVTags (ExtractAVTagsIn) returns (ExtractAVTagsOut); - rpc ExtractLatex (ExtractLatexIn) returns (ExtractLatexOut); - rpc GetEmptyCards (Empty) returns (EmptyCardsReport); - rpc RenderExistingCard (RenderExistingCardIn) returns (RenderCardOut); - rpc RenderUncommittedCard (RenderUncommittedCardIn) returns (RenderCardOut); - rpc StripAVTags (String) returns (String); - - // searching - - rpc SearchCards (SearchCardsIn) returns (SearchCardsOut); - rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); - rpc FindAndReplace (FindAndReplaceIn) returns (UInt32); - - // scheduling - - rpc LocalMinutesWest (Int64) returns (Int32); - rpc SetLocalMinutesWest (Int32) returns (Empty); - rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut); - rpc StudiedToday (StudiedTodayIn) returns (String); - rpc CongratsLearnMessage (CongratsLearnMessageIn) returns (String); - rpc UpdateStats (UpdateStatsIn) returns (Empty); - rpc ExtendLimits (ExtendLimitsIn) returns (Empty); - rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut); - - // stats - - rpc CardStats (CardID) returns (String); - rpc Graphs(GraphsIn) returns (GraphsOut); - - // media - - rpc CheckMedia (Empty) returns (CheckMediaOut); - rpc TrashMediaFiles (TrashMediaFilesIn) returns (Empty); - rpc AddMediaFile (AddMediaFileIn) returns (String); - rpc EmptyTrash (Empty) returns (Empty); - rpc RestoreTrash (Empty) returns (Empty); - - // decks - - rpc AddOrUpdateDeckLegacy (AddOrUpdateDeckLegacyIn) returns (DeckID); - rpc DeckTree (DeckTreeIn) returns (DeckTreeNode); - rpc DeckTreeLegacy (Empty) returns (Json); - rpc GetAllDecksLegacy (Empty) returns (Json); - rpc GetDeckIDByName (String) returns (DeckID); - rpc GetDeckLegacy (DeckID) returns (Json); - rpc GetDeckNames (GetDeckNamesIn) returns (DeckNames); - rpc NewDeckLegacy (Bool) returns (Json); - rpc RemoveDeck (DeckID) returns (Empty); - - // deck config - - rpc AddOrUpdateDeckConfigLegacy (AddOrUpdateDeckConfigLegacyIn) returns (DeckConfigID); - rpc AllDeckConfigLegacy (Empty) returns (Json); - rpc GetDeckConfigLegacy (DeckConfigID) returns (Json); - rpc NewDeckConfigLegacy (Empty) returns (Json); - rpc RemoveDeckConfig (DeckConfigID) returns (Empty); - - // cards - - rpc GetCard (CardID) returns (Card); - rpc UpdateCard (Card) returns (Empty); - rpc AddCard (Card) returns (CardID); - rpc RemoveCards (RemoveCardsIn) returns (Empty); - - // notes - - rpc NewNote (NoteTypeID) returns (Note); - rpc AddNote (AddNoteIn) returns (NoteID); - rpc UpdateNote (Note) returns (Empty); - rpc GetNote (NoteID) returns (Note); - rpc RemoveNotes (RemoveNotesIn) returns (Empty); - rpc AddNoteTags (AddNoteTagsIn) returns (UInt32); - rpc UpdateNoteTags (UpdateNoteTagsIn) returns (UInt32); - rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut); - rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty); - rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut); - rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut); - - // note types - - rpc AddOrUpdateNotetype (AddOrUpdateNotetypeIn) returns (NoteTypeID); - rpc GetStockNotetypeLegacy (GetStockNotetypeIn) returns (Json); - rpc GetNotetypeLegacy (NoteTypeID) returns (Json); - rpc GetNotetypeNames (Empty) returns (NoteTypeNames); - rpc GetNotetypeNamesAndCounts (Empty) returns (NoteTypeUseCounts); - rpc GetNotetypeIDByName (String) returns (NoteTypeID); - rpc RemoveNotetype (NoteTypeID) returns (Empty); - - // collection - - rpc OpenCollection (OpenCollectionIn) returns (Empty); - rpc CloseCollection (CloseCollectionIn) returns (Empty); - rpc CheckDatabase (Empty) returns (CheckDatabaseOut); - - // sync - - // rpc SyncMedia (SyncAuth) returns (Empty); - // rpc AbortSync (Empty) returns (Empty); - // rpc AbortMediaSync (Empty) returns (Empty); - rpc BeforeUpload (Empty) returns (Empty); - // rpc SyncLogin (SyncLoginIn) returns (SyncAuth); - // rpc SyncStatus (SyncAuth) returns (SyncStatusOut); - // rpc SyncCollection (SyncAuth) returns (SyncCollectionOut); - // rpc FullUpload (SyncAuth) returns (Empty); - // rpc FullDownload (SyncAuth) returns (Empty); - - // translation/messages - - rpc TranslateString (TranslateStringIn) returns (String); - rpc FormatTimespan (FormatTimespanIn) returns (String); - rpc I18nResources (Empty) returns (Json); - - // tags - - rpc RegisterTags (RegisterTagsIn) returns (Bool); - rpc AllTags (Empty) returns (AllTagsOut); - - // config/preferences - - rpc GetConfigJson (String) returns (Json); - rpc SetConfigJson (SetConfigJsonIn) returns (Empty); - rpc RemoveConfig (String) returns (Empty); - rpc SetAllConfig (Json) returns (Empty); - rpc GetAllConfig (Empty) returns (Json); - rpc GetPreferences (Empty) returns (Preferences); - rpc SetPreferences (Preferences) returns (Empty); -} - -// Protobuf stored in .anki2 files -// These should be moved to a separate file in the future -/////////////////////////////////////////////////////////// - -message DeckConfigInner { - enum NewCardOrder { - NEW_CARD_ORDER_DUE = 0; - NEW_CARD_ORDER_RANDOM = 1; - } - - enum LeechAction { - LEECH_ACTION_SUSPEND = 0; - LEECH_ACTION_TAG_ONLY = 1; - } - - repeated float learn_steps = 1; - repeated float relearn_steps = 2; - - reserved 3 to 8; - - uint32 new_per_day = 9; - uint32 reviews_per_day = 10; - - float initial_ease = 11; - float easy_multiplier = 12; - float hard_multiplier = 13; - float lapse_multiplier = 14; - float interval_multiplier = 15; - - uint32 maximum_review_interval = 16; - uint32 minimum_review_interval = 17; - - uint32 graduating_interval_good = 18; - uint32 graduating_interval_easy = 19; - - NewCardOrder new_card_order = 20; - - LeechAction leech_action = 21; - uint32 leech_threshold = 22; - - bool disable_autoplay = 23; - uint32 cap_answer_time_to_secs = 24; - uint32 visible_timer_secs = 25; - bool skip_question_when_replaying_answer = 26; - - bool bury_new = 27; - bool bury_reviews = 28; - - bytes other = 255; -} - -message DeckCommon { - bool study_collapsed = 1; - bool browser_collapsed = 2; - - uint32 last_day_studied = 3; - int32 new_studied = 4; - int32 review_studied = 5; - int32 milliseconds_studied = 7; - - // previously set in the v1 scheduler, - // but not currently used for anything - int32 learning_studied = 6; - - bytes other = 255; -} - -message DeckKind { - oneof kind { - NormalDeck normal = 1; - FilteredDeck filtered = 2; - } -} - -message NormalDeck { - int64 config_id = 1; - uint32 extend_new = 2; - uint32 extend_review = 3; - string description = 4; -} - -message FilteredDeck { - bool reschedule = 1; - repeated FilteredSearchTerm search_terms = 2; - // v1 scheduler only - repeated float delays = 3; - // v2 scheduler only - uint32 preview_delay = 4; -} - -message FilteredSearchTerm { - enum FilteredSearchOrder { - FILTERED_SEARCH_ORDER_OLDEST_FIRST = 0; - FILTERED_SEARCH_ORDER_RANDOM = 1; - FILTERED_SEARCH_ORDER_INTERVALS_ASCENDING = 2; - FILTERED_SEARCH_ORDER_INTERVALS_DESCENDING = 3; - FILTERED_SEARCH_ORDER_LAPSES = 4; - FILTERED_SEARCH_ORDER_ADDED = 5; - FILTERED_SEARCH_ORDER_DUE = 6; - FILTERED_SEARCH_ORDER_REVERSE_ADDED = 7; - FILTERED_SEARCH_ORDER_DUE_PRIORITY = 8; - } - - string search = 1; - uint32 limit = 2; - FilteredSearchOrder order = 3; -} - -message NoteFieldConfig { - bool sticky = 1; - bool rtl = 2; - string font_name = 3; - uint32 font_size = 4; - - bytes other = 255; -} - -message CardTemplateConfig { - string q_format = 1; - string a_format = 2; - string q_format_browser = 3; - string a_format_browser= 4; - int64 target_deck_id = 5; - string browser_font_name = 6; - uint32 browser_font_size = 7; - - bytes other = 255; -} - -message NoteTypeConfig { - enum Kind { - KIND_NORMAL = 0; - KIND_CLOZE = 1; - } - Kind kind = 1; - uint32 sort_field_idx = 2; - string css = 3; - int64 target_deck_id = 4; - string latex_pre = 5; - string latex_post = 6; - bool latex_svg = 7; - repeated CardRequirement reqs = 8; - - bytes other = 255; -} - -message CardRequirement { - enum Kind { - KIND_NONE = 0; - KIND_ANY = 1; - KIND_ALL = 2; - } - uint32 card_ord = 1; - Kind kind = 2; - repeated uint32 field_ords = 3; -} - -// Containers for passing around database objects -/////////////////////////////////////////////////////////// - -message Deck { - int64 id = 1; - string name = 2; - uint32 mtime_secs = 3; - int32 usn = 4; - DeckCommon common = 5; - oneof kind { - NormalDeck normal = 6; - FilteredDeck filtered = 7; - } -} - -message NoteType { - int64 id = 1; - string name = 2; - uint32 mtime_secs = 3; - sint32 usn = 4; - NoteTypeConfig config = 7; - repeated NoteField fields = 8; - repeated CardTemplate templates = 9; -} - -message NoteField { - OptionalUInt32 ord = 1; - string name = 2; - NoteFieldConfig config = 5; -} - -message CardTemplate { - OptionalUInt32 ord = 1; - string name = 2; - uint32 mtime_secs = 3; - sint32 usn = 4; - CardTemplateConfig config = 5; -} - -message Note { - int64 id = 1; - string guid = 2; - int64 ntid = 3; - uint32 mtime_secs = 4; - int32 usn = 5; - repeated string tags = 6; - repeated string fields = 7; -} - -message Card { - int64 id = 1; - int64 nid = 2; - int64 did = 3; - uint32 ord = 4; - int64 mtime = 5; - sint32 usn = 6; - uint32 ctype = 7; - sint32 queue = 8; - sint32 due = 9; - uint32 ivl = 10; - uint32 factor = 11; - uint32 reps = 12; - uint32 lapses = 13; - uint32 left = 14; - sint32 odue = 15; - int64 odid = 16; - uint32 flags = 17; - string data = 18; -} - -// Backend -/////////////////////////////////////////////////////////// - -message BackendInit { - repeated string preferred_langs = 1; - string locale_folder_path = 2; - bool server = 3; -} - -message I18nBackendInit { - repeated string preferred_langs = 4; - string locale_folder_path = 5; -} - -// Errors -/////////////////////////////////////////////////////////// - -message BackendError { - // localized error description suitable for displaying to the user - string localized = 1; - // error specifics - oneof value { - Empty invalid_input = 2; - Empty template_parse = 3; - Empty io_error = 4; - Empty db_error = 5; - NetworkError network_error = 6; - SyncError sync_error = 7; - // user interrupted operation - Empty interrupted = 8; - string json_error = 9; - string proto_error = 10; - Empty not_found_error = 11; - Empty exists = 12; - Empty deck_is_filtered = 13; - } -} - -message NetworkError { - enum NetworkErrorKind { - OTHER = 0; - OFFLINE = 1; - TIMEOUT = 2; - PROXY_AUTH = 3; - } - NetworkErrorKind kind = 1; -} - -message SyncError { - enum SyncErrorKind { - OTHER = 0; - CONFLICT = 1; - SERVER_ERROR = 2; - CLIENT_TOO_OLD = 3; - AUTH_FAILED = 4; - SERVER_MESSAGE = 5; - MEDIA_CHECK_REQUIRED = 6; - RESYNC_REQUIRED = 7; - CLOCK_INCORRECT = 8; - DATABASE_CHECK_REQUIRED = 9; - } - SyncErrorKind kind = 1; -} - -// Progress -/////////////////////////////////////////////////////////// - -message Progress { - oneof value { - Empty none = 1; - MediaSyncProgress media_sync = 2; - string media_check = 3; - FullSyncProgress full_sync = 4; - NormalSyncProgress normal_sync = 5; - DatabaseCheckProgress database_check = 6; - } -} - -message MediaSyncProgress { - string checked = 1; - string added = 2; - string removed = 3; -} - -message FullSyncProgress { - uint32 transferred = 1; - uint32 total = 2; -} - -message MediaSyncUploadProgress { - uint32 files = 1; - uint32 deletions = 2; -} - -message NormalSyncProgress { - string stage = 1; - string added = 2; - string removed = 3; -} - -message DatabaseCheckProgress { - string stage = 1; - uint32 stage_total = 2; - uint32 stage_current = 3; -} - - -// Messages -/////////////////////////////////////////////////////////// - - -message SchedTimingTodayOut { - uint32 days_elapsed = 1; - int64 next_day_at = 2; -} - -message DeckTreeIn { - // if non-zero, counts for the provided timestamp will be included - int64 now = 1; - int64 top_deck_id = 2; -} - -message DeckTreeNode { - int64 deck_id = 1; - string name = 2; - repeated DeckTreeNode children = 3; - uint32 level = 4; - bool collapsed = 5; - - uint32 review_count = 6; - uint32 learn_count = 7; - uint32 new_count = 8; - - bool filtered = 16; -} - -message RenderExistingCardIn { - int64 card_id = 1; - bool browser = 2; -} - -message RenderUncommittedCardIn { - Note note = 1; - uint32 card_ord = 2; - bytes template = 3; - bool fill_empty = 4; -} - -message RenderCardOut { - repeated RenderedTemplateNode question_nodes = 1; - repeated RenderedTemplateNode answer_nodes = 2; -} - -message RenderedTemplateNode { - oneof value { - string text = 1; - RenderedTemplateReplacement replacement = 2; - } -} - -message RenderedTemplateReplacement { - string field_name = 1; - string current_text = 2; - repeated string filters = 3; -} - -message ExtractAVTagsIn { - string text = 1; - bool question_side = 2; -} - -message ExtractAVTagsOut { - string text = 1; - repeated AVTag av_tags = 2; -} - -message AVTag { - oneof value { - string sound_or_video = 1; - TTSTag tts = 2; - } -} - -message TTSTag { - string field_text = 1; - string lang = 2; - repeated string voices = 3; - float speed = 4; - repeated string other_args = 5; -} - -message ExtractLatexIn { - string text = 1; - bool svg = 2; - bool expand_clozes = 3; -} - -message ExtractLatexOut { - string text = 1; - repeated ExtractedLatex latex = 2; -} - -message ExtractedLatex { - string filename = 1; - string latex_body = 2; -} - -message AddMediaFileIn { - string desired_name = 1; - bytes data = 2; -} - -message CheckMediaOut { - repeated string unused = 1; - repeated string missing = 2; - string report = 3; - bool have_trash = 4; -} - -message TrashMediaFilesIn { - repeated string fnames = 1; -} - -message TranslateStringIn { - FluentString key = 2; - map args = 3; -} - -message TranslateArgValue { - oneof value { - string str = 1; - double number = 2; - } -} - -message FormatTimespanIn { - enum Context { - PRECISE = 0; - ANSWER_BUTTONS = 1; - INTERVALS = 2; - } - - float seconds = 1; - Context context = 2; -} - -message StudiedTodayIn { - uint32 cards = 1; - double seconds = 2; -} - -message CongratsLearnMessageIn { - float next_due = 1; - uint32 remaining = 2; -} - -message OpenCollectionIn { - string collection_path = 1; - string media_folder_path = 2; - string media_db_path = 3; - string log_path = 4; -} - -message SearchCardsIn { - string search = 1; - SortOrder order = 2; -} - -message SearchCardsOut { - repeated int64 card_ids = 1; - -} - -message SortOrder { - oneof value { - Empty from_config = 1; - Empty none = 2; - string custom = 3; - BuiltinSearchOrder builtin = 4; - } -} - -message SearchNotesIn { - string search = 1; -} - -message SearchNotesOut { - repeated int64 note_ids = 2; -} - -message BuiltinSearchOrder { - enum BuiltinSortKind { - NOTE_CREATION = 0; - NOTE_MOD = 1; - NOTE_FIELD = 2; - NOTE_TAGS = 3; - NOTE_TYPE = 4; - CARD_MOD = 5; - CARD_REPS = 6; - CARD_DUE = 7; - CARD_EASE = 8; - CARD_LAPSES = 9; - CARD_INTERVAL = 10; - CARD_DECK = 11; - CARD_TEMPLATE = 12; - } - BuiltinSortKind kind = 1; - bool reverse = 2; -} - -message CloseCollectionIn { - bool downgrade_to_schema11 = 1; -} - -message AddOrUpdateDeckConfigLegacyIn { - bytes config = 1; - bool preserve_usn_and_mtime = 2; -} - -message RegisterTagsIn { - string tags = 1; - bool preserve_usn = 2; - int32 usn = 3; - bool clear_first = 4; -} - -message AllTagsOut { - repeated TagUsnTuple tags = 1; -} - -message TagUsnTuple { - string tag = 1; - sint32 usn = 2; -} - -message GetChangedTagsOut { - repeated string tags = 1; -} - -message SetConfigJsonIn { - string key = 1; - bytes value_json = 2; -} - -enum StockNoteType { - STOCK_NOTE_TYPE_BASIC = 0; - STOCK_NOTE_TYPE_BASIC_AND_REVERSED = 1; - STOCK_NOTE_TYPE_BASIC_OPTIONAL_REVERSED = 2; - STOCK_NOTE_TYPE_BASIC_TYPING = 3; - STOCK_NOTE_TYPE_CLOZE = 4; -} - -message GetStockNotetypeIn { - StockNoteType kind = 1; -} - -message NoteTypeNames { - repeated NoteTypeNameID entries = 1; -} - -message NoteTypeUseCounts { - repeated NoteTypeNameIDUseCount entries = 1; -} - -message NoteTypeNameID { - int64 id = 1; - string name = 2; - -} - -message NoteTypeNameIDUseCount { - int64 id = 1; - string name = 2; - uint32 use_count = 3; -} - -message AddOrUpdateNotetypeIn { - bytes json = 1; - bool preserve_usn_and_mtime = 2; -} - -message AddNoteIn { - Note note = 1; - int64 deck_id = 2; -} - -message EmptyCardsReport { - string report = 1; - repeated NoteWithEmptyCards notes = 2; -} - -message NoteWithEmptyCards { - int64 note_id = 1; - repeated int64 card_ids = 2; - bool will_delete_note = 3; -} - -message DeckNames { - repeated DeckNameID entries = 1; -} - -message DeckNameID { - int64 id = 1; - string name = 2; -} - -message AddOrUpdateDeckLegacyIn { - bytes deck = 1; - bool preserve_usn_and_mtime = 2; -} - -message FieldNamesForNotesIn { - repeated int64 nids = 1; -} - -message FieldNamesForNotesOut { - repeated string fields = 1; -} - -message FindAndReplaceIn { - repeated int64 nids = 1; - string search = 2; - string replacement = 3; - bool regex = 4; - bool match_case = 5; - string field_name = 6; -} - -message AfterNoteUpdatesIn { - repeated int64 nids = 1; - bool mark_notes_modified = 2; - bool generate_cards = 3; -} - -message AddNoteTagsIn { - repeated int64 nids = 1; - string tags = 2; -} - -message UpdateNoteTagsIn { - repeated int64 nids = 1; - string tags = 2; - string replacement = 3; - bool regex = 4; -} - -message CheckDatabaseOut { - repeated string problems = 1; -} - -message CollectionSchedulingSettings { - enum NewReviewMix { - DISTRIBUTE = 0; - REVIEWS_FIRST = 1; - NEW_FIRST = 2; - } - - uint32 scheduler_version = 1; - uint32 rollover = 2; - uint32 learn_ahead_secs = 3; - NewReviewMix new_review_mix = 4; - bool show_remaining_due_counts = 5; - bool show_intervals_on_buttons = 6; - uint32 time_limit_secs = 7; - - // v2 only - bool new_timezone = 8; - bool day_learn_first = 9; -} - -message Preferences { - CollectionSchedulingSettings sched = 1; -} - -message ClozeNumbersInNoteOut { - repeated uint32 numbers = 1; -} - -message GetDeckNamesIn { - bool skip_empty_default = 1; - // if unset, implies skip_empty_default - bool include_filtered = 2; -} - -message NoteIsDuplicateOrEmptyOut { - enum State { - NORMAL = 0; - EMPTY = 1; - DUPLICATE = 2; - } - State state = 1; -} - -message SyncLoginIn { - string username = 1; - string password = 2; -} - -message SyncStatusOut { - enum Required { - NO_CHANGES = 0; - NORMAL_SYNC = 1; - FULL_SYNC = 2; - } - Required required = 1; -} - -message SyncCollectionOut { - enum ChangesRequired { - NO_CHANGES = 0; - NORMAL_SYNC = 1; - FULL_SYNC = 2; - // local collection has no cards; upload not an option - FULL_DOWNLOAD = 3; - // remote collection has no cards; download not an option - FULL_UPLOAD = 4; - } - - uint32 host_number = 1; - string server_message = 2; - ChangesRequired required = 3; -} - -message SyncAuth { - string hkey = 1; - uint32 host_number = 2; -} - -message RemoveNotesIn { - repeated int64 note_ids = 1; - repeated int64 card_ids = 2; -} - -message RemoveCardsIn { - repeated int64 card_ids = 1; -} - -message UpdateStatsIn { - int64 deck_id = 1; - int32 new_delta = 2; - int32 review_delta = 4; - int32 millisecond_delta = 5; -} - -message ExtendLimitsIn { - int64 deck_id = 1; - int32 new_delta = 2; - int32 review_delta = 3; -} - -message CountsForDeckTodayOut { - int32 new = 1; - int32 review = 2; -} - -message GraphsIn { - string search = 1; - uint32 days = 2; -} - -message GraphsOut { - repeated Card cards = 1; - repeated RevlogEntry revlog = 2; - uint32 days_elapsed = 3; - // Based on rollover hour - uint32 next_day_at_secs = 4; - uint32 scheduler_version = 5; - /// Seconds to add to UTC timestamps to get local time. - int32 local_offset_secs = 7; -} - -message RevlogEntry { - enum ReviewKind { - LEARNING = 0; - REVIEW = 1; - RELEARNING = 2; - EARLY_REVIEW = 3; - } - int64 id = 1; - int64 cid = 2; - int32 usn = 3; - uint32 button_chosen = 4; - int32 interval = 5; - int32 last_interval = 6; - uint32 ease_factor = 7; - uint32 taken_millis = 8; - ReviewKind review_kind = 9; -} diff --git a/tools/protoc-gen/test/fluent.proto b/tools/protoc-gen/test/fluent.proto deleted file mode 100644 index ab4eaace6..000000000 --- a/tools/protoc-gen/test/fluent.proto +++ /dev/null @@ -1,277 +0,0 @@ -// This file is automatically generated as part of the build process. - -syntax = "proto3"; -package BackendProto; -enum FluentString { - CARD_STATS_ADDED = 0; - CARD_STATS_AVERAGE_TIME = 1; - CARD_STATS_CARD_ID = 2; - CARD_STATS_CARD_TEMPLATE = 3; - CARD_STATS_DECK_NAME = 4; - CARD_STATS_EASE = 5; - CARD_STATS_FIRST_REVIEW = 6; - CARD_STATS_INTERVAL = 7; - CARD_STATS_LAPSE_COUNT = 8; - CARD_STATS_LATEST_REVIEW = 9; - CARD_STATS_NEW_CARD_POSITION = 10; - CARD_STATS_NOTE_ID = 11; - CARD_STATS_NOTE_TYPE = 12; - CARD_STATS_REVIEW_COUNT = 13; - CARD_STATS_REVIEW_LOG_DATE = 14; - CARD_STATS_REVIEW_LOG_RATING = 15; - CARD_STATS_REVIEW_LOG_TIME_TAKEN = 16; - CARD_STATS_REVIEW_LOG_TYPE = 17; - CARD_STATS_REVIEW_LOG_TYPE_FILTERED = 18; - CARD_STATS_REVIEW_LOG_TYPE_LEARN = 19; - CARD_STATS_REVIEW_LOG_TYPE_RELEARN = 20; - CARD_STATS_REVIEW_LOG_TYPE_REVIEW = 21; - CARD_STATS_TOTAL_TIME = 22; - CARD_TEMPLATE_RENDERING_BACK_SIDE_PROBLEM = 23; - CARD_TEMPLATE_RENDERING_CONDITIONAL_NOT_CLOSED = 24; - CARD_TEMPLATE_RENDERING_CONDITIONAL_NOT_OPEN = 25; - CARD_TEMPLATE_RENDERING_EMPTY_FRONT = 26; - CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM = 27; - CARD_TEMPLATE_RENDERING_MISSING_CLOZE = 28; - CARD_TEMPLATE_RENDERING_MORE_INFO = 29; - CARD_TEMPLATE_RENDERING_NO_CLOSING_BRACKETS = 30; - CARD_TEMPLATE_RENDERING_NO_SUCH_FIELD = 31; - CARD_TEMPLATE_RENDERING_WRONG_CONDITIONAL_CLOSED = 32; - CARD_TEMPLATES_ADD_MOBILE_CLASS = 33; - CARD_TEMPLATES_BACK_PREVIEW = 34; - CARD_TEMPLATES_BACK_TEMPLATE = 35; - CARD_TEMPLATES_CARD_TYPE = 36; - CARD_TEMPLATES_CHANGES_SAVED = 37; - CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES = 38; - CARD_TEMPLATES_DISCARD_CHANGES = 39; - CARD_TEMPLATES_FILL_EMPTY = 40; - CARD_TEMPLATES_FRONT_PREVIEW = 41; - CARD_TEMPLATES_FRONT_TEMPLATE = 42; - CARD_TEMPLATES_INVALID_TEMPLATE_NUMBER = 43; - CARD_TEMPLATES_NIGHT_MODE = 44; - CARD_TEMPLATES_PREVIEW_BOX = 45; - CARD_TEMPLATES_PREVIEW_SETTINGS = 46; - CARD_TEMPLATES_SAMPLE_CLOZE = 47; - CARD_TEMPLATES_TEMPLATE_BOX = 48; - CARD_TEMPLATES_TEMPLATE_STYLING = 49; - DATABASE_CHECK_CARD_MISSING_NOTE = 50; - DATABASE_CHECK_CARD_PROPERTIES = 51; - DATABASE_CHECK_CHECKING_CARDS = 52; - DATABASE_CHECK_CHECKING_HISTORY = 53; - DATABASE_CHECK_CHECKING_INTEGRITY = 54; - DATABASE_CHECK_CHECKING_NOTES = 55; - DATABASE_CHECK_CORRUPT = 56; - DATABASE_CHECK_DUPLICATE_CARD_ORDS = 57; - DATABASE_CHECK_FIELD_COUNT = 58; - DATABASE_CHECK_MISSING_DECKS = 59; - DATABASE_CHECK_MISSING_TEMPLATES = 60; - DATABASE_CHECK_NEW_CARD_HIGH_DUE = 61; - DATABASE_CHECK_REBUILDING = 62; - DATABASE_CHECK_REBUILT = 63; - DATABASE_CHECK_REVLOG_PROPERTIES = 64; - DATABASE_CHECK_TITLE = 65; - DECK_CONFIG_DEFAULT_NAME = 66; - DECK_CONFIG_USED_BY_DECKS = 67; - EMPTY_CARDS_COUNT_LINE = 68; - EMPTY_CARDS_DELETE_BUTTON = 69; - EMPTY_CARDS_DELETE_EMPTY_CARDS = 70; - EMPTY_CARDS_DELETE_EMPTY_NOTES = 71; - EMPTY_CARDS_DELETED_COUNT = 72; - EMPTY_CARDS_DELETING = 73; - EMPTY_CARDS_FOR_NOTE_TYPE = 74; - EMPTY_CARDS_NOT_FOUND = 75; - EMPTY_CARDS_PRESERVE_NOTES_CHECKBOX = 76; - EMPTY_CARDS_WINDOW_TITLE = 77; - FILTERING_IS_DUE = 78; - FINDREPLACE_NOTES_UPDATED = 79; - IMPORTING_FAILED_DEBUG_INFO = 80; - MEDIA_CHECK_ALL_LATEX_RENDERED = 81; - MEDIA_CHECK_CHECK_MEDIA_ACTION = 82; - MEDIA_CHECK_CHECKED = 83; - MEDIA_CHECK_DELETE_UNUSED = 84; - MEDIA_CHECK_DELETE_UNUSED_COMPLETE = 85; - MEDIA_CHECK_DELETE_UNUSED_CONFIRM = 86; - MEDIA_CHECK_EMPTY_TRASH = 87; - MEDIA_CHECK_FILES_REMAINING = 88; - MEDIA_CHECK_MISSING_COUNT = 89; - MEDIA_CHECK_MISSING_FILE = 90; - MEDIA_CHECK_MISSING_HEADER = 91; - MEDIA_CHECK_OVERSIZE_COUNT = 92; - MEDIA_CHECK_OVERSIZE_FILE = 93; - MEDIA_CHECK_OVERSIZE_HEADER = 94; - MEDIA_CHECK_RENAMED_COUNT = 95; - MEDIA_CHECK_RENAMED_FILE = 96; - MEDIA_CHECK_RENAMED_HEADER = 97; - MEDIA_CHECK_RENDER_LATEX = 98; - MEDIA_CHECK_RESTORE_TRASH = 99; - MEDIA_CHECK_SUBFOLDER_COUNT = 100; - MEDIA_CHECK_SUBFOLDER_FILE = 101; - MEDIA_CHECK_SUBFOLDER_HEADER = 102; - MEDIA_CHECK_TRASH_COUNT = 103; - MEDIA_CHECK_TRASH_EMPTIED = 104; - MEDIA_CHECK_TRASH_RESTORED = 105; - MEDIA_CHECK_UNUSED_COUNT = 106; - MEDIA_CHECK_UNUSED_FILE = 107; - MEDIA_CHECK_UNUSED_HEADER = 108; - MEDIA_CHECK_WINDOW_TITLE = 109; - NETWORK_DETAILS = 110; - NETWORK_OFFLINE = 111; - NETWORK_OTHER = 112; - NETWORK_PROXY_AUTH = 113; - NETWORK_TIMEOUT = 114; - NOTETYPES_ADD_REVERSE_FIELD = 115; - NOTETYPES_BACK_EXTRA_FIELD = 116; - NOTETYPES_BACK_FIELD = 117; - NOTETYPES_BASIC_NAME = 118; - NOTETYPES_BASIC_OPTIONAL_REVERSED_NAME = 119; - NOTETYPES_BASIC_REVERSED_NAME = 120; - NOTETYPES_BASIC_TYPE_ANSWER_NAME = 121; - NOTETYPES_CARD_1_NAME = 122; - NOTETYPES_CARD_2_NAME = 123; - NOTETYPES_CLOZE_NAME = 124; - NOTETYPES_FRONT_FIELD = 125; - NOTETYPES_TEXT_FIELD = 126; - SCHEDULING_ANSWER_BUTTON_TIME_DAYS = 127; - SCHEDULING_ANSWER_BUTTON_TIME_HOURS = 128; - SCHEDULING_ANSWER_BUTTON_TIME_MINUTES = 129; - SCHEDULING_ANSWER_BUTTON_TIME_MONTHS = 130; - SCHEDULING_ANSWER_BUTTON_TIME_SECONDS = 131; - SCHEDULING_ANSWER_BUTTON_TIME_YEARS = 132; - SCHEDULING_BURIED_CARDS_WERE_DELAYED = 133; - SCHEDULING_CONGRATULATIONS_FINISHED = 134; - SCHEDULING_LEARN_REMAINING = 135; - SCHEDULING_NEXT_LEARN_DUE = 136; - SCHEDULING_TIME_SPAN_DAYS = 137; - SCHEDULING_TIME_SPAN_HOURS = 138; - SCHEDULING_TIME_SPAN_MINUTES = 139; - SCHEDULING_TIME_SPAN_MONTHS = 140; - SCHEDULING_TIME_SPAN_SECONDS = 141; - SCHEDULING_TIME_SPAN_YEARS = 142; - SCHEDULING_TODAY_NEW_LIMIT_REACHED = 143; - SCHEDULING_TODAY_REVIEW_LIMIT_REACHED = 144; - SEARCH_CARD_MODIFIED = 145; - SEARCH_INVALID = 146; - SEARCH_NOTE_MODIFIED = 147; - STATISTICS_ADDED_SUBTITLE = 148; - STATISTICS_ADDED_TITLE = 149; - STATISTICS_AMOUNT_OF_TOTAL_WITH_PERCENTAGE = 150; - STATISTICS_ANSWER_BUTTONS_BUTTON_NUMBER = 151; - STATISTICS_ANSWER_BUTTONS_BUTTON_PRESSED = 152; - STATISTICS_ANSWER_BUTTONS_SUBTITLE = 153; - STATISTICS_ANSWER_BUTTONS_TITLE = 154; - STATISTICS_AVERAGE = 155; - STATISTICS_AVERAGE_ANSWER_TIME = 156; - STATISTICS_AVERAGE_ANSWER_TIME_LABEL = 157; - STATISTICS_AVERAGE_FOR_DAYS_STUDIED = 158; - STATISTICS_AVERAGE_INTERVAL = 159; - STATISTICS_AVERAGE_OVER_PERIOD = 160; - STATISTICS_BACKLOG_CHECKBOX = 161; - STATISTICS_CALENDAR_TITLE = 162; - STATISTICS_CARD_EASE_SUBTITLE = 163; - STATISTICS_CARD_EASE_TITLE = 164; - STATISTICS_CARD_EASE_TOOLTIP = 165; - STATISTICS_CARDS = 166; - STATISTICS_CARDS_DUE = 167; - STATISTICS_CARDS_PER_DAY = 168; - STATISTICS_CARDS_PER_MIN = 169; - STATISTICS_COUNTS_BURIED_CARDS = 170; - STATISTICS_COUNTS_EARLY_CARDS = 171; - STATISTICS_COUNTS_LEARNING_CARDS = 172; - STATISTICS_COUNTS_MATURE_CARDS = 173; - STATISTICS_COUNTS_NEW_CARDS = 174; - STATISTICS_COUNTS_RELEARNING_CARDS = 175; - STATISTICS_COUNTS_SUSPENDED_CARDS = 176; - STATISTICS_COUNTS_TITLE = 177; - STATISTICS_COUNTS_TOTAL_CARDS = 178; - STATISTICS_COUNTS_YOUNG_CARDS = 179; - STATISTICS_DAYS_AGO_RANGE = 180; - STATISTICS_DAYS_AGO_SINGLE = 181; - STATISTICS_DAYS_STUDIED = 182; - STATISTICS_DUE_COUNT = 183; - STATISTICS_DUE_DATE = 184; - STATISTICS_DUE_FOR_NEW_CARD = 185; - STATISTICS_DUE_TOMORROW = 186; - STATISTICS_ELAPSED_TIME_DAYS = 187; - STATISTICS_ELAPSED_TIME_HOURS = 188; - STATISTICS_ELAPSED_TIME_MINUTES = 189; - STATISTICS_ELAPSED_TIME_MONTHS = 190; - STATISTICS_ELAPSED_TIME_SECONDS = 191; - STATISTICS_ELAPSED_TIME_YEARS = 192; - STATISTICS_ERROR_FETCHING = 193; - STATISTICS_FUTURE_DUE_SUBTITLE = 194; - STATISTICS_FUTURE_DUE_TITLE = 195; - STATISTICS_HOURS_CORRECT = 196; - STATISTICS_HOURS_RANGE = 197; - STATISTICS_HOURS_SUBTITLE = 198; - STATISTICS_HOURS_TITLE = 199; - STATISTICS_IN_DAYS_RANGE = 200; - STATISTICS_IN_DAYS_SINGLE = 201; - STATISTICS_IN_TIME_SPAN_DAYS = 202; - STATISTICS_IN_TIME_SPAN_HOURS = 203; - STATISTICS_IN_TIME_SPAN_MINUTES = 204; - STATISTICS_IN_TIME_SPAN_MONTHS = 205; - STATISTICS_IN_TIME_SPAN_SECONDS = 206; - STATISTICS_IN_TIME_SPAN_YEARS = 207; - STATISTICS_INTERVALS_DAY_RANGE = 208; - STATISTICS_INTERVALS_DAY_SINGLE = 209; - STATISTICS_INTERVALS_SUBTITLE = 210; - STATISTICS_INTERVALS_TITLE = 211; - STATISTICS_LONGEST_INTERVAL = 212; - STATISTICS_MINUTES_PER_DAY = 213; - STATISTICS_NO_DATA = 214; - STATISTICS_RANGE_1_YEAR_HISTORY = 215; - STATISTICS_RANGE_ALL_HISTORY = 216; - STATISTICS_RANGE_ALL_TIME = 217; - STATISTICS_RANGE_COLLECTION = 218; - STATISTICS_RANGE_DECK = 219; - STATISTICS_RANGE_SEARCH = 220; - STATISTICS_REVIEWS = 221; - STATISTICS_REVIEWS_COUNT_SUBTITLE = 222; - STATISTICS_REVIEWS_PER_DAY = 223; - STATISTICS_REVIEWS_TIME_CHECKBOX = 224; - STATISTICS_REVIEWS_TIME_SUBTITLE = 225; - STATISTICS_REVIEWS_TITLE = 226; - STATISTICS_RUNNING_TOTAL = 227; - STATISTICS_SECONDS_TAKEN = 228; - STATISTICS_STUDIED_TODAY = 229; - STATISTICS_TODAY_AGAIN_COUNT = 230; - STATISTICS_TODAY_CORRECT_MATURE = 231; - STATISTICS_TODAY_NO_CARDS = 232; - STATISTICS_TODAY_NO_MATURE_CARDS = 233; - STATISTICS_TODAY_TITLE = 234; - STATISTICS_TODAY_TYPE_COUNTS = 235; - STATISTICS_TOTAL = 236; - SYNC_ABORT_BUTTON = 237; - SYNC_ACCOUNT_REQUIRED = 238; - SYNC_ADDED_UPDATED_COUNT = 239; - SYNC_ANKIWEB_ID_LABEL = 240; - SYNC_CANCEL_BUTTON = 241; - SYNC_CHECKING = 242; - SYNC_CLIENT_TOO_OLD = 243; - SYNC_CLOCK_OFF = 244; - SYNC_CONFIRM_EMPTY_DOWNLOAD = 245; - SYNC_CONFLICT = 246; - SYNC_CONFLICT_EXPLANATION = 247; - SYNC_CONNECTING = 248; - SYNC_DOWNLOAD_FROM_ANKIWEB = 249; - SYNC_DOWNLOADING_FROM_ANKIWEB = 250; - SYNC_MEDIA_ABORTED = 251; - SYNC_MEDIA_ABORTING = 252; - SYNC_MEDIA_ADDED_COUNT = 253; - SYNC_MEDIA_CHECKED_COUNT = 254; - SYNC_MEDIA_COMPLETE = 255; - SYNC_MEDIA_DISABLED = 256; - SYNC_MEDIA_FAILED = 257; - SYNC_MEDIA_LOG_BUTTON = 258; - SYNC_MEDIA_LOG_TITLE = 259; - SYNC_MEDIA_REMOVED_COUNT = 260; - SYNC_MEDIA_STARTING = 261; - SYNC_MUST_WAIT_FOR_END = 262; - SYNC_PASSWORD_LABEL = 263; - SYNC_RESYNC_REQUIRED = 264; - SYNC_SANITY_CHECK_FAILED = 265; - SYNC_SERVER_ERROR = 266; - SYNC_SYNCING = 267; - SYNC_UPLOAD_TO_ANKIWEB = 268; - SYNC_UPLOADING_TO_ANKIWEB = 269; - SYNC_WRONG_PASS = 270; -} diff --git a/tools/protoc-gen/test/test_anki.bat b/tools/protoc-gen/test/test_anki.bat deleted file mode 100644 index bcc70c355..000000000 --- a/tools/protoc-gen/test/test_anki.bat +++ /dev/null @@ -1,2 +0,0 @@ -if not exist "%~dp0\out" mkdir "%~dp0\out" -protoc --include_source_info --plugin=protoc-gen-anki="..\protoc-gen.bat" --anki_out="out" backend.proto \ No newline at end of file diff --git a/tools/setup-python b/tools/setup-python new file mode 100644 index 000000000..f4d651cf2 --- /dev/null +++ b/tools/setup-python @@ -0,0 +1,11 @@ +#!/bin/bash +# +# If `python` in the project folder exists, use it instead of the global python. +# + +if [ -e python ]; then + PYTHON=$(readlink $(pwd)/python) +else + PYTHON=$(which python3) +fi +