diff --git a/Cargo.lock b/Cargo.lock index da19d3704..0b7724d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -67,6 +76,7 @@ dependencies = [ "bytes", "chrono", "coarsetime", + "convert_case 0.6.0", "criterion", "csv", "dissimilar", @@ -109,6 +119,7 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "snafu", "strum", "tempfile", "tokio", @@ -215,6 +226,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -409,6 +435,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -578,7 +613,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -623,6 +658,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5" +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dtoa" version = "0.4.8" @@ -932,6 +973,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + [[package]] name = "h2" version = "0.3.14" @@ -1578,6 +1625,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.15.0" @@ -2314,6 +2370,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2665,6 +2727,29 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "snafu" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd726aec4ebad65756394ff89a9b9598793d4e30121cd71690244c1e497b3aee" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712529e9b0b014eabaa345b38e06032767e3dc393e8b017e853b1d7247094e74" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "snowflake" version = "1.3.0" @@ -3175,6 +3260,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + [[package]] name = "unicode-width" version = "0.1.10" diff --git a/Cargo.toml b/Cargo.toml index 803d3e4b0..a62f7b293 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,9 @@ compile_data_attr = "glob([\"**/*.rsv\"])" [package.metadata.raze.crates.bstr.'*'] compile_data_attr = "glob([\"**/*.dfa\"])" +[package.metadata.raze.crates.snafu.'*'] +compile_data_attr = "glob([\"**/*.md\"])" + [package.metadata.raze.crates.pyo3-build-config.'*'] buildrs_additional_environment_variables = { "PYO3_NO_PYTHON" = "1" } diff --git a/cargo/BUILD.bazel b/cargo/BUILD.bazel index 42a561ba5..6d7c0ff13 100644 --- a/cargo/BUILD.bazel +++ b/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", @@ -608,19 +626,9 @@ alias( # Export file for Stardoc support exports_files( - glob([ - "**/*.bazel", - "**/*.bzl", - ]), - visibility = ["//visibility:public"], -) - -filegroup( - name = "srcs", - srcs = glob([ - "**/*.bazel", - "**/*.bzl", - ]), + [ + "crates.bzl", + ], visibility = ["//visibility:public"], ) diff --git a/cargo/crates.bzl b/cargo/crates.bzl index 330ee8dbe..0a49c728f 100644 --- a/cargo/crates.bzl +++ b/cargo/crates.bzl @@ -11,6 +11,16 @@ load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") # buildifier: di def raze_fetch_remote_crates(): """This function defines a collection of repos and should be called in a WORKSPACE file""" + maybe( + http_archive, + name = "raze__addr2line__0_17_0", + url = "https://crates.io/api/v1/crates/addr2line/0.17.0/download", + type = "tar.gz", + sha256 = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b", + strip_prefix = "addr2line-0.17.0", + build_file = Label("//cargo/remote:BUILD.addr2line-0.17.0.bazel"), + ) + maybe( http_archive, name = "raze__adler__1_0_2", @@ -141,6 +151,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.autocfg-1.1.0.bazel"), ) + maybe( + http_archive, + name = "raze__backtrace__0_3_66", + url = "https://crates.io/api/v1/crates/backtrace/0.3.66/download", + type = "tar.gz", + sha256 = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7", + strip_prefix = "backtrace-0.3.66", + build_file = Label("//cargo/remote:BUILD.backtrace-0.3.66.bazel"), + ) + maybe( http_archive, name = "raze__base64__0_13_0", @@ -301,6 +321,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.convert_case-0.4.0.bazel"), ) + maybe( + http_archive, + name = "raze__convert_case__0_6_0", + url = "https://crates.io/api/v1/crates/convert_case/0.6.0/download", + type = "tar.gz", + sha256 = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca", + strip_prefix = "convert_case-0.6.0", + build_file = Label("//cargo/remote:BUILD.convert_case-0.6.0.bazel"), + ) + maybe( http_archive, name = "raze__core_foundation__0_9_3", @@ -451,6 +481,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.dissimilar-1.0.4.bazel"), ) + maybe( + http_archive, + name = "raze__doc_comment__0_3_3", + url = "https://crates.io/api/v1/crates/doc-comment/0.3.3/download", + type = "tar.gz", + sha256 = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10", + strip_prefix = "doc-comment-0.3.3", + build_file = Label("//cargo/remote:BUILD.doc-comment-0.3.3.bazel"), + ) + maybe( http_archive, name = "raze__dtoa__0_4_8", @@ -791,6 +831,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.getrandom-0.2.7.bazel"), ) + maybe( + http_archive, + name = "raze__gimli__0_26_2", + url = "https://crates.io/api/v1/crates/gimli/0.26.2/download", + type = "tar.gz", + sha256 = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d", + strip_prefix = "gimli-0.26.2", + build_file = Label("//cargo/remote:BUILD.gimli-0.26.2.bazel"), + ) + maybe( http_archive, name = "raze__h2__0_3_14", @@ -1441,6 +1491,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.num_threads-0.1.6.bazel"), ) + maybe( + http_archive, + name = "raze__object__0_29_0", + url = "https://crates.io/api/v1/crates/object/0.29.0/download", + type = "tar.gz", + sha256 = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53", + strip_prefix = "object-0.29.0", + build_file = Label("//cargo/remote:BUILD.object-0.29.0.bazel"), + ) + maybe( http_archive, name = "raze__once_cell__1_15_0", @@ -2071,6 +2131,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.rusqlite-0.28.0.bazel"), ) + maybe( + http_archive, + name = "raze__rustc_demangle__0_1_21", + url = "https://crates.io/api/v1/crates/rustc-demangle/0.1.21/download", + type = "tar.gz", + sha256 = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342", + strip_prefix = "rustc-demangle-0.1.21", + build_file = Label("//cargo/remote:BUILD.rustc-demangle-0.1.21.bazel"), + ) + maybe( http_archive, name = "raze__rustc_hash__1_1_0", @@ -2431,6 +2501,26 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.smallvec-1.9.0.bazel"), ) + maybe( + http_archive, + name = "raze__snafu__0_7_2", + url = "https://crates.io/api/v1/crates/snafu/0.7.2/download", + type = "tar.gz", + sha256 = "dd726aec4ebad65756394ff89a9b9598793d4e30121cd71690244c1e497b3aee", + strip_prefix = "snafu-0.7.2", + build_file = Label("//cargo/remote:BUILD.snafu-0.7.2.bazel"), + ) + + maybe( + http_archive, + name = "raze__snafu_derive__0_7_2", + url = "https://crates.io/api/v1/crates/snafu-derive/0.7.2/download", + type = "tar.gz", + sha256 = "712529e9b0b014eabaa345b38e06032767e3dc393e8b017e853b1d7247094e74", + strip_prefix = "snafu-derive-0.7.2", + build_file = Label("//cargo/remote:BUILD.snafu-derive-0.7.2.bazel"), + ) + maybe( http_archive, name = "raze__snowflake__1_3_0", @@ -2961,6 +3051,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.unicode-normalization-0.1.22.bazel"), ) + maybe( + http_archive, + name = "raze__unicode_segmentation__1_10_0", + url = "https://crates.io/api/v1/crates/unicode-segmentation/1.10.0/download", + type = "tar.gz", + sha256 = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a", + strip_prefix = "unicode-segmentation-1.10.0", + build_file = Label("//cargo/remote:BUILD.unicode-segmentation-1.10.0.bazel"), + ) + maybe( http_archive, name = "raze__unicode_width__0_1_10", diff --git a/cargo/licenses.json b/cargo/licenses.json index 18a4c6462..8914d244b 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -1,4 +1,13 @@ [ + { + "name": "addr2line", + "version": "0.17.0", + "authors": null, + "repository": "https://github.com/gimli-rs/addr2line", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A cross-platform symbolication library written in Rust, using `gimli`" + }, { "name": "adler", "version": "1.0.2", @@ -134,6 +143,15 @@ "license_file": null, "description": "Automatic cfg for Rust compiler features" }, + { + "name": "backtrace", + "version": "0.3.66", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang/backtrace-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A library to acquire a stack trace (backtrace) at runtime in a Rust program." + }, { "name": "base64", "version": "0.13.0", @@ -251,6 +269,15 @@ "license_file": null, "description": "Compares two equal-sized byte strings in constant time." }, + { + "name": "convert_case", + "version": "0.6.0", + "authors": "Rutrum ", + "repository": "https://github.com/rutrum/convert-case", + "license": "MIT", + "license_file": null, + "description": "Convert strings into any case" + }, { "name": "core-foundation", "version": "0.9.3", @@ -359,6 +386,15 @@ "license_file": null, "description": "Diff library with semantic cleanup, based on Google's diff-match-patch" }, + { + "name": "doc-comment", + "version": "0.3.3", + "authors": "Guillaume Gomez ", + "repository": "https://github.com/GuillaumeGomez/doc-comment", + "license": "MIT", + "license_file": null, + "description": "Macro to generate doc comments" + }, { "name": "either", "version": "1.8.0", @@ -620,6 +656,15 @@ "license_file": null, "description": "A small cross-platform library for retrieving random data from system source" }, + { + "name": "gimli", + "version": "0.26.2", + "authors": null, + "repository": "https://github.com/gimli-rs/gimli", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A library for reading and writing the DWARF debugging format." + }, { "name": "h2", "version": "0.3.14", @@ -1142,6 +1187,15 @@ "license_file": null, "description": "A minimal library that determines the number of running threads for the current process." }, + { + "name": "object", + "version": "0.29.0", + "authors": null, + "repository": "https://github.com/gimli-rs/object", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A unified interface for reading and writing object file formats." + }, { "name": "once_cell", "version": "1.15.0", @@ -1565,6 +1619,15 @@ "license_file": null, "description": "Ergonomic wrapper for SQLite" }, + { + "name": "rustc-demangle", + "version": "0.1.21", + "authors": "Alex Crichton ", + "repository": "https://github.com/alexcrichton/rustc-demangle", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Rust compiler symbol demangling." + }, { "name": "rustc-hash", "version": "1.1.0", @@ -1844,6 +1907,24 @@ "license_file": null, "description": "'Small vector' optimization: store up to a small number of items on the stack" }, + { + "name": "snafu", + "version": "0.7.2", + "authors": "Jake Goulding ", + "repository": "https://github.com/shepmaster/snafu", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "An ergonomic error handling library" + }, + { + "name": "snafu-derive", + "version": "0.7.2", + "authors": "Jake Goulding ", + "repository": "https://github.com/shepmaster/snafu", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "An ergonomic error handling library" + }, { "name": "snowflake", "version": "1.3.0", @@ -2294,6 +2375,15 @@ "license_file": null, "description": "This crate provides functions for normalization of Unicode strings, including Canonical and Compatible Decomposition and Recomposition, as described in Unicode Standard Annex #15." }, + { + "name": "unicode-segmentation", + "version": "1.10.0", + "authors": "kwantam |Manish Goregaokar ", + "repository": "https://github.com/unicode-rs/unicode-segmentation", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "This crate provides Grapheme Cluster, Word and Sentence boundaries according to Unicode Standard Annex #29 rules." + }, { "name": "unicode-width", "version": "0.1.10", diff --git a/cargo/remote/BUILD.addr2line-0.17.0.bazel b/cargo/remote/BUILD.addr2line-0.17.0.bazel new file mode 100644 index 000000000..1e4fbe0ff --- /dev/null +++ b/cargo/remote/BUILD.addr2line-0.17.0.bazel @@ -0,0 +1,63 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" +]) + +# Generated Targets + +# Unsupported target "addr2line" with type "example" omitted + +rust_library( + name = "addr2line", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + data = [], + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=addr2line", + "manual", + ], + version = "0.17.0", + # buildifier: leave-alone + deps = [ + "@raze__gimli__0_26_2//:gimli", + ], +) + +# Unsupported target "correctness" with type "test" omitted + +# Unsupported target "output_equivalence" with type "test" omitted + +# Unsupported target "parse" with type "test" omitted diff --git a/cargo/remote/BUILD.ahash-0.7.6.bazel b/cargo/remote/BUILD.ahash-0.7.6.bazel index eb25f39f4..37e057173 100644 --- a/cargo/remote/BUILD.ahash-0.7.6.bazel +++ b/cargo/remote/BUILD.ahash-0.7.6.bazel @@ -66,7 +66,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -80,7 +79,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -123,7 +121,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -138,7 +135,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.atty-0.2.14.bazel b/cargo/remote/BUILD.atty-0.2.14.bazel index 997bcd03a..cf17b5107 100644 --- a/cargo/remote/BUILD.atty-0.2.14.bazel +++ b/cargo/remote/BUILD.atty-0.2.14.bazel @@ -61,7 +61,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.backtrace-0.3.66.bazel b/cargo/remote/BUILD.backtrace-0.3.66.bazel new file mode 100644 index 000000000..f789a196f --- /dev/null +++ b/cargo/remote/BUILD.backtrace-0.3.66.bazel @@ -0,0 +1,111 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets +# buildifier: disable=out-of-order-load +# buildifier: disable=load-on-top +load( + "@rules_rust//cargo:cargo_build_script.bzl", + "cargo_build_script", +) + +cargo_build_script( + name = "backtrace_build_script", + srcs = glob(["**/*.rs"]), + build_script_env = { + }, + crate_features = [ + "default", + "std", + ], + crate_root = "build.rs", + data = glob(["**"]), + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.3.66", + visibility = ["//visibility:private"], + deps = [ + "@raze__cc__1_0_73//:cc", + ], +) + +# Unsupported target "benchmarks" with type "bench" omitted + +# Unsupported target "backtrace" with type "example" omitted + +# Unsupported target "raw" with type "example" omitted + +rust_library( + name = "backtrace", + srcs = glob(["**/*.rs"]), + crate_features = [ + "default", + "std", + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=backtrace", + "manual", + ], + version = "0.3.66", + # buildifier: leave-alone + deps = [ + ":backtrace_build_script", + "@raze__addr2line__0_17_0//:addr2line", + "@raze__cfg_if__1_0_0//:cfg_if", + "@raze__libc__0_2_133//:libc", + "@raze__miniz_oxide__0_5_4//:miniz_oxide", + "@raze__object__0_29_0//:object", + "@raze__rustc_demangle__0_1_21//:rustc_demangle", + ], +) + +# Unsupported target "accuracy" with type "test" omitted + +# Unsupported target "concurrent-panics" with type "test" omitted + +# Unsupported target "long_fn_name" with type "test" omitted + +# Unsupported target "skip_inner_frames" with type "test" omitted + +# Unsupported target "smoke" with type "test" omitted diff --git a/cargo/remote/BUILD.bazel b/cargo/remote/BUILD.bazel index b49fb6866..e69de29bb 100644 --- a/cargo/remote/BUILD.bazel +++ b/cargo/remote/BUILD.bazel @@ -1,17 +0,0 @@ -# Export file for Stardoc support -exports_files( - glob([ - "**/*.bazel", - "**/*.bzl", - ]), - visibility = ["//visibility:public"], -) - -filegroup( - name = "srcs", - srcs = glob([ - "**/*.bazel", - "**/*.bzl", - ]), - visibility = ["//visibility:public"], -) diff --git a/cargo/remote/BUILD.coarsetime-0.1.22.bazel b/cargo/remote/BUILD.coarsetime-0.1.22.bazel index 14cf6d12a..3e1568d8d 100644 --- a/cargo/remote/BUILD.coarsetime-0.1.22.bazel +++ b/cargo/remote/BUILD.coarsetime-0.1.22.bazel @@ -61,7 +61,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.convert_case-0.6.0.bazel b/cargo/remote/BUILD.convert_case-0.6.0.bazel new file mode 100644 index 000000000..1d60eff2f --- /dev/null +++ b/cargo/remote/BUILD.convert_case-0.6.0.bazel @@ -0,0 +1,57 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT" +]) + +# Generated Targets + +rust_library( + name = "convert_case", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=convert_case", + "manual", + ], + version = "0.6.0", + # buildifier: leave-alone + deps = [ + "@raze__unicode_segmentation__1_10_0//:unicode_segmentation", + ], +) + +# Unsupported target "string_types" with type "test" omitted diff --git a/cargo/remote/BUILD.dirs-sys-next-0.1.2.bazel b/cargo/remote/BUILD.dirs-sys-next-0.1.2.bazel index 359b296e8..426662649 100644 --- a/cargo/remote/BUILD.dirs-sys-next-0.1.2.bazel +++ b/cargo/remote/BUILD.dirs-sys-next-0.1.2.bazel @@ -59,7 +59,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.doc-comment-0.3.3.bazel b/cargo/remote/BUILD.doc-comment-0.3.3.bazel new file mode 100644 index 000000000..bfaa3a649 --- /dev/null +++ b/cargo/remote/BUILD.doc-comment-0.3.3.bazel @@ -0,0 +1,84 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT" +]) + +# Generated Targets +# buildifier: disable=out-of-order-load +# buildifier: disable=load-on-top +load( + "@rules_rust//cargo:cargo_build_script.bzl", + "cargo_build_script", +) + +cargo_build_script( + name = "doc_comment_build_script", + srcs = glob(["**/*.rs"]), + build_script_env = { + }, + crate_features = [ + ], + crate_root = "build.rs", + data = glob(["**"]), + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.3.3", + visibility = ["//visibility:private"], + deps = [ + ], +) + +rust_library( + name = "doc_comment", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + data = [], + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=doc_comment", + "manual", + ], + version = "0.3.3", + # buildifier: leave-alone + deps = [ + ":doc_comment_build_script", + ], +) diff --git a/cargo/remote/BUILD.getrandom-0.1.16.bazel b/cargo/remote/BUILD.getrandom-0.1.16.bazel index f09656a07..a5b028b63 100644 --- a/cargo/remote/BUILD.getrandom-0.1.16.bazel +++ b/cargo/remote/BUILD.getrandom-0.1.16.bazel @@ -65,7 +65,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -107,7 +106,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.getrandom-0.2.7.bazel b/cargo/remote/BUILD.getrandom-0.2.7.bazel index c6840008d..3dfabb194 100644 --- a/cargo/remote/BUILD.getrandom-0.2.7.bazel +++ b/cargo/remote/BUILD.getrandom-0.2.7.bazel @@ -63,7 +63,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.gimli-0.26.2.bazel b/cargo/remote/BUILD.gimli-0.26.2.bazel new file mode 100644 index 000000000..05f4bcd54 --- /dev/null +++ b/cargo/remote/BUILD.gimli-0.26.2.bazel @@ -0,0 +1,70 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +# Unsupported target "bench" with type "bench" omitted + +# Unsupported target "dwarf-validate" with type "example" omitted + +# Unsupported target "dwarfdump" with type "example" omitted + +# Unsupported target "simple" with type "example" omitted + +# Unsupported target "simple_line" with type "example" omitted + +rust_library( + name = "gimli", + srcs = glob(["**/*.rs"]), + crate_features = [ + "read", + "read-core", + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=gimli", + "manual", + ], + version = "0.26.2", + # buildifier: leave-alone + deps = [ + ], +) + +# Unsupported target "convert_self" with type "test" omitted + +# Unsupported target "parse_self" with type "test" omitted diff --git a/cargo/remote/BUILD.iana-time-zone-0.1.50.bazel b/cargo/remote/BUILD.iana-time-zone-0.1.50.bazel index d8f9ff753..2169adefb 100644 --- a/cargo/remote/BUILD.iana-time-zone-0.1.50.bazel +++ b/cargo/remote/BUILD.iana-time-zone-0.1.50.bazel @@ -63,7 +63,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ "@raze__core_foundation_sys__0_8_3//:core_foundation_sys", diff --git a/cargo/remote/BUILD.jobserver-0.1.25.bazel b/cargo/remote/BUILD.jobserver-0.1.25.bazel index dbc795154..620b1c76f 100644 --- a/cargo/remote/BUILD.jobserver-0.1.25.bazel +++ b/cargo/remote/BUILD.jobserver-0.1.25.bazel @@ -59,7 +59,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.mio-0.8.4.bazel b/cargo/remote/BUILD.mio-0.8.4.bazel index 3e60da1a0..e4ff26a63 100644 --- a/cargo/remote/BUILD.mio-0.8.4.bazel +++ b/cargo/remote/BUILD.mio-0.8.4.bazel @@ -70,7 +70,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.native-tls-0.2.10.bazel b/cargo/remote/BUILD.native-tls-0.2.10.bazel index 6f4828c20..dfd46a448 100644 --- a/cargo/remote/BUILD.native-tls-0.2.10.bazel +++ b/cargo/remote/BUILD.native-tls-0.2.10.bazel @@ -63,7 +63,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ "@raze__security_framework_sys__2_6_1//:security_framework_sys", @@ -122,7 +121,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ "@raze__lazy_static__1_4_0//:lazy_static", diff --git a/cargo/remote/BUILD.num_cpus-1.13.1.bazel b/cargo/remote/BUILD.num_cpus-1.13.1.bazel index f5a470d5a..3c78844bc 100644 --- a/cargo/remote/BUILD.num_cpus-1.13.1.bazel +++ b/cargo/remote/BUILD.num_cpus-1.13.1.bazel @@ -61,7 +61,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.num_threads-0.1.6.bazel b/cargo/remote/BUILD.num_threads-0.1.6.bazel index 1da23ad24..5d1451ffd 100644 --- a/cargo/remote/BUILD.num_threads-0.1.6.bazel +++ b/cargo/remote/BUILD.num_threads-0.1.6.bazel @@ -58,7 +58,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ "@raze__libc__0_2_133//:libc", diff --git a/cargo/remote/BUILD.object-0.29.0.bazel b/cargo/remote/BUILD.object-0.29.0.bazel new file mode 100644 index 000000000..12147579f --- /dev/null +++ b/cargo/remote/BUILD.object-0.29.0.bazel @@ -0,0 +1,66 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" +]) + +# Generated Targets + +rust_library( + name = "object", + srcs = glob(["**/*.rs"]), + crate_features = [ + "archive", + "coff", + "elf", + "macho", + "pe", + "read_core", + "unaligned", + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=object", + "manual", + ], + version = "0.29.0", + # buildifier: leave-alone + deps = [ + "@raze__memchr__2_5_0//:memchr", + ], +) + +# Unsupported target "integration" with type "test" omitted + +# Unsupported target "parse_self" with type "test" omitted diff --git a/cargo/remote/BUILD.parking_lot_core-0.9.3.bazel b/cargo/remote/BUILD.parking_lot_core-0.9.3.bazel index 4d1d6eaf5..9cd02fa89 100644 --- a/cargo/remote/BUILD.parking_lot_core-0.9.3.bazel +++ b/cargo/remote/BUILD.parking_lot_core-0.9.3.bazel @@ -64,7 +64,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -112,7 +111,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.rand-0.7.3.bazel b/cargo/remote/BUILD.rand-0.7.3.bazel index 50c4b9b5b..3b55a73be 100644 --- a/cargo/remote/BUILD.rand-0.7.3.bazel +++ b/cargo/remote/BUILD.rand-0.7.3.bazel @@ -84,7 +84,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -98,7 +97,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.rand-0.8.5.bazel b/cargo/remote/BUILD.rand-0.8.5.bazel index c189ff18d..d0dbf909e 100644 --- a/cargo/remote/BUILD.rand-0.8.5.bazel +++ b/cargo/remote/BUILD.rand-0.8.5.bazel @@ -69,7 +69,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.reqwest-0.11.3.bazel b/cargo/remote/BUILD.reqwest-0.11.3.bazel index 41cc4b706..9715c45d7 100644 --- a/cargo/remote/BUILD.reqwest-0.11.3.bazel +++ b/cargo/remote/BUILD.reqwest-0.11.3.bazel @@ -104,7 +104,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.rustc-demangle-0.1.21.bazel b/cargo/remote/BUILD.rustc-demangle-0.1.21.bazel new file mode 100644 index 000000000..c9e262fd0 --- /dev/null +++ b/cargo/remote/BUILD.rustc-demangle-0.1.21.bazel @@ -0,0 +1,54 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "rustc_demangle", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + data = [], + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=rustc-demangle", + "manual", + ], + version = "0.1.21", + # buildifier: leave-alone + deps = [ + ], +) diff --git a/cargo/remote/BUILD.rustls-native-certs-0.5.0.bazel b/cargo/remote/BUILD.rustls-native-certs-0.5.0.bazel index fd4a7dd5c..d11a3a8f2 100644 --- a/cargo/remote/BUILD.rustls-native-certs-0.5.0.bazel +++ b/cargo/remote/BUILD.rustls-native-certs-0.5.0.bazel @@ -62,7 +62,6 @@ rust_library( ( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.snafu-0.7.2.bazel b/cargo/remote/BUILD.snafu-0.7.2.bazel new file mode 100644 index 000000000..bdecb4d2e --- /dev/null +++ b/cargo/remote/BUILD.snafu-0.7.2.bazel @@ -0,0 +1,134 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "snafu", + srcs = glob(["**/*.rs"]), + crate_features = [ + "backtrace", + "backtraces", + "default", + "rust_1_39", + "rust_1_46", + "std", + ], + crate_root = "src/lib.rs", + data = [], + compile_data = glob(["**/*.md"]), + edition = "2018", + proc_macro_deps = [ + "@raze__snafu_derive__0_7_2//:snafu_derive", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=snafu", + "manual", + ], + version = "0.7.2", + # buildifier: leave-alone + deps = [ + "@raze__backtrace__0_3_66//:backtrace", + "@raze__doc_comment__0_3_3//:doc_comment", + ], +) + +# Unsupported target "backtrace" with type "test" omitted + +# Unsupported target "backtrace-optional" with type "test" omitted + +# Unsupported target "backtrace-optional-enabled" with type "test" omitted + +# Unsupported target "backtrace_attributes" with type "test" omitted + +# Unsupported target "basic" with type "test" omitted + +# Unsupported target "boxed_error_trait_object" with type "test" omitted + +# Unsupported target "build-leaf-error" with type "test" omitted + +# Unsupported target "context_selector_name" with type "test" omitted + +# Unsupported target "default_error_display" with type "test" omitted + +# Unsupported target "display-shorthand" with type "test" omitted + +# Unsupported target "doc_comment" with type "test" omitted + +# Unsupported target "ensure" with type "test" omitted + +# Unsupported target "error_chain" with type "test" omitted + +# Unsupported target "generics" with type "test" omitted + +# Unsupported target "generics_with_default" with type "test" omitted + +# Unsupported target "implicit" with type "test" omitted + +# Unsupported target "location" with type "test" omitted + +# Unsupported target "mapping_result_without_try_operator" with type "test" omitted + +# Unsupported target "module" with type "test" omitted + +# Unsupported target "multiple_attributes" with type "test" omitted + +# Unsupported target "name-conflicts" with type "test" omitted + +# Unsupported target "no_context" with type "test" omitted + +# Unsupported target "opaque" with type "test" omitted + +# Unsupported target "options" with type "test" omitted + +# Unsupported target "premade_error" with type "test" omitted + +# Unsupported target "raw_idents" with type "test" omitted + +# Unsupported target "recursive_error" with type "test" omitted + +# Unsupported target "report" with type "test" omitted + +# Unsupported target "send_between_threads" with type "test" omitted + +# Unsupported target "single_use_lifetimes_lint" with type "test" omitted + +# Unsupported target "source_attributes" with type "test" omitted + +# Unsupported target "stringly_typed" with type "test" omitted + +# Unsupported target "structs" with type "test" omitted + +# Unsupported target "visibility" with type "test" omitted diff --git a/cargo/remote/BUILD.snafu-derive-0.7.2.bazel b/cargo/remote/BUILD.snafu-derive-0.7.2.bazel new file mode 100644 index 000000000..b4e32faad --- /dev/null +++ b/cargo/remote/BUILD.snafu-derive-0.7.2.bazel @@ -0,0 +1,60 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_proc_macro( + name = "snafu_derive", + srcs = glob(["**/*.rs"]), + crate_features = [ + "rust_1_39", + "rust_1_46", + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=snafu-derive", + "manual", + ], + version = "0.7.2", + # buildifier: leave-alone + deps = [ + "@raze__heck__0_4_0//:heck", + "@raze__proc_macro2__1_0_43//:proc_macro2", + "@raze__quote__1_0_21//:quote", + "@raze__syn__1_0_100//:syn", + ], +) diff --git a/cargo/remote/BUILD.socket2-0.4.7.bazel b/cargo/remote/BUILD.socket2-0.4.7.bazel index 701ef8c38..a7879ac47 100644 --- a/cargo/remote/BUILD.socket2-0.4.7.bazel +++ b/cargo/remote/BUILD.socket2-0.4.7.bazel @@ -60,7 +60,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.tempfile-3.3.0.bazel b/cargo/remote/BUILD.tempfile-3.3.0.bazel index db901ba29..d9e9c1806 100644 --- a/cargo/remote/BUILD.tempfile-3.3.0.bazel +++ b/cargo/remote/BUILD.tempfile-3.3.0.bazel @@ -62,7 +62,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.time-0.3.14.bazel b/cargo/remote/BUILD.time-0.3.14.bazel index 0fb422e0f..38d33dcb9 100644 --- a/cargo/remote/BUILD.time-0.3.14.bazel +++ b/cargo/remote/BUILD.time-0.3.14.bazel @@ -73,7 +73,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.tokio-1.21.1.bazel b/cargo/remote/BUILD.tokio-1.21.1.bazel index 585db515f..9db4a89b4 100644 --- a/cargo/remote/BUILD.tokio-1.21.1.bazel +++ b/cargo/remote/BUILD.tokio-1.21.1.bazel @@ -90,7 +90,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -103,7 +102,6 @@ cargo_build_script( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -183,7 +181,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ @@ -197,7 +194,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/cargo/remote/BUILD.unicode-segmentation-1.10.0.bazel b/cargo/remote/BUILD.unicode-segmentation-1.10.0.bazel new file mode 100644 index 000000000..ba8b5c456 --- /dev/null +++ b/cargo/remote/BUILD.unicode-segmentation-1.10.0.bazel @@ -0,0 +1,60 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +# buildifier: disable=load +load( + "@rules_rust//rust:defs.bzl", + "rust_binary", + "rust_library", + "rust_proc_macro", + "rust_test", +) + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +# Unsupported target "graphemes" with type "bench" omitted + +# Unsupported target "unicode_words" with type "bench" omitted + +# Unsupported target "word_bounds" with type "bench" omitted + +rust_library( + name = "unicode_segmentation", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "crate-name=unicode-segmentation", + "manual", + ], + version = "1.10.0", + # buildifier: leave-alone + deps = [ + ], +) diff --git a/cargo/remote/BUILD.utime-0.3.1.bazel b/cargo/remote/BUILD.utime-0.3.1.bazel index a8250cb6e..41ebbe194 100644 --- a/cargo/remote/BUILD.utime-0.3.1.bazel +++ b/cargo/remote/BUILD.utime-0.3.1.bazel @@ -59,7 +59,6 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu", "@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-apple-ios", - "@rules_rust//rust/platform:aarch64-apple-ios-sim", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-ios", ): [ diff --git a/ftl/core/errors.ftl b/ftl/core/errors.ftl index 5b74899e4..4f53f8579 100644 --- a/ftl/core/errors.ftl +++ b/ftl/core/errors.ftl @@ -12,6 +12,7 @@ errors-please-check-database = Please use the Check Database action, then try ag errors-please-check-media = Please use the Check Media action, then try again. errors-collection-too-new = This collection requires a newer version of Anki to open. errors-invalid-ids = This deck contains timestamps in the future. Please contact the deck author and ask them to fix the issue. +errors-inconsistent-db-state = Your database appears to be in an inconsistent state. Please use the Check Database action. ## Card Rendering diff --git a/proto/anki/backend.proto b/proto/anki/backend.proto index a1c19e61b..6cb7959f9 100644 --- a/proto/anki/backend.proto +++ b/proto/anki/backend.proto @@ -66,10 +66,14 @@ message BackendError { CARD_TYPE_ERROR = 18; } - // localized error description suitable for displaying to the user - string localized = 1; + // error description, usually localized, suitable for displaying to the user + string message = 1; // the error subtype Kind kind = 2; // optional page in the manual optional links.HelpPageLinkRequest.HelpPage help_page = 3; + // additional information about the context in which the error occured + string context = 4; + // a backtrace of the underlying error; requires RUST_BACKTRACE to be set + string backtrace = 5; } diff --git a/proto/protobuf.bzl b/proto/protobuf.bzl index 2874b23e6..8267ffdae 100644 --- a/proto/protobuf.bzl +++ b/proto/protobuf.bzl @@ -33,9 +33,9 @@ def setup_protobuf_binary(name): http_archive, name = "protoc_bin_macos", urls = [ - "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-osx-x86_64.zip", + "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-osx-universal_binary.zip", ], - sha256 = "d8b55cf1e887917dd43c447d77bd5bd213faff1e18ac3a176b35558d86f7ffff", + sha256 = "e3324d3bc2e9bc967a0bec2472e0ec73b26f952c7c87f2403197414f780c3c6c", build_file_content = """exports_files(["bin/protoc"])""", ) @@ -43,9 +43,9 @@ def setup_protobuf_binary(name): http_archive, name = "protoc_bin_linux_x86_64", urls = [ - "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip", + "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-x86_64.zip", ], - sha256 = "058d29255a08f8661c8096c92961f3676218704cbd516d3916ec468e139cbd87", + sha256 = "f90d0dd59065fef94374745627336d622702b67f0319f96cee894d41a974d47a", build_file_content = """exports_files(["bin/protoc"])""", ) @@ -53,9 +53,9 @@ def setup_protobuf_binary(name): http_archive, name = "protoc_bin_linux_arm64", urls = [ - "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-aarch_64.zip", + "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-aarch_64.zip", ], - sha256 = "95584939e733bdd6ffb8245616b2071f565cd4c28163b6c21c8f936a9ee20861", + sha256 = "f3d8eb5839d6186392d8c7b54fbeabbb6fcdd90618a500b77cb2e24faa245cad", build_file_content = """exports_files(["bin/protoc"])""", ) @@ -63,9 +63,9 @@ def setup_protobuf_binary(name): http_archive, name = "protoc_bin_windows", urls = [ - "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-win64.zip", + "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-win64.zip", ], - sha256 = "828d2bdfe410e988cfc46462bcabd34ffdda8cc172867989ec647eadc55b03b5", + sha256 = "3657053024faa439ff5f8c1dd2ee06bac0f9b9a3d660e99944f015a7451e87ec", build_file_content = """exports_files(["bin/protoc.exe"])""", ) diff --git a/pylib/.pylintrc b/pylib/.pylintrc index 2d01c1e80..cee6511ed 100644 --- a/pylib/.pylintrc +++ b/pylib/.pylintrc @@ -3,30 +3,6 @@ ignore-patterns=.*_pb2.* persistent = no extension-pkg-whitelist=orjson -[TYPECHECK] -ignored-classes= - BrowserColumns, - BrowserRow, - HelpPage, - FormatTimespanRequest, - CardAnswer, - QueuedCards, - UnburyDeckRequest, - BuryOrSuspendCardsRequest, - NoteFieldsCheckResponse, - BackendError, - SetDeckCollapsedRequest, - ConfigKey, - HelpPageLinkRequest, - StripHtmlRequest, - CustomStudyRequest, - Cram, - ScheduleCardsAsNewRequest, - ExportLimit, - CsvColumn, - CsvMetadata, - ImportCsvRequest, - [REPORTS] output-format=colorized @@ -64,3 +40,6 @@ good-names = db, ok, ip, + +[IMPORTS] +ignored-modules = anki.*_pb2, anki.sync_pb2 diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index 48eeac94f..54577dfa3 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -19,6 +19,7 @@ from anki.dbproxy import ValueForDB from anki.utils import from_json_bytes, to_json_bytes from ..errors import ( + BackendError, BackendIOError, CardTypeError, CustomStudyError, @@ -27,7 +28,6 @@ from ..errors import ( FilteredDeckError, Interrupted, InvalidInput, - LocalizedError, NetworkError, NotFoundError, SearchError, @@ -172,58 +172,64 @@ class Translations(GeneratedTranslations): def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception: kind = backend_pb2.BackendError val = err.kind + help_page = err.help_page if err.HasField("help_page") else None + context = err.context if err.context else None + backtrace = err.backtrace if err.backtrace else None + if val == kind.INTERRUPTED: - return Interrupted() + return Interrupted(err.message, help_page, context, backtrace) elif val == kind.NETWORK_ERROR: - return NetworkError(err.localized) + return NetworkError(err.message, help_page, context, backtrace) elif val == kind.SYNC_AUTH_ERROR: - return SyncError(err.localized, SyncErrorKind.AUTH) + return SyncError(err.message, help_page, context, backtrace, SyncErrorKind.AUTH) elif val == kind.SYNC_OTHER_ERROR: - return SyncError(err.localized, SyncErrorKind.OTHER) + return SyncError( + err.message, help_page, context, backtrace, SyncErrorKind.OTHER + ) elif val == kind.IO_ERROR: - return BackendIOError(err.localized) + return BackendIOError(err.message, help_page, context, backtrace) elif val == kind.DB_ERROR: - return DBError(err.localized) + return DBError(err.message, help_page, context, backtrace) elif val == kind.CARD_TYPE_ERROR: - return CardTypeError(err.localized, err.help_page) + return CardTypeError(err.message, help_page, context, backtrace) elif val == kind.TEMPLATE_PARSE: - return TemplateError(err.localized) + return TemplateError(err.message, help_page, context, backtrace) elif val == kind.INVALID_INPUT: - return InvalidInput(err.localized) + return InvalidInput(err.message, help_page, context, backtrace) elif val == kind.JSON_ERROR: - return LocalizedError(err.localized) + return BackendError(err.message, help_page, context, backtrace) elif val == kind.NOT_FOUND_ERROR: - return NotFoundError() + return NotFoundError(err.message, help_page, context, backtrace) elif val == kind.EXISTS: - return ExistsError() + return ExistsError(err.message, help_page, context, backtrace) elif val == kind.FILTERED_DECK_ERROR: - return FilteredDeckError(err.localized) + return FilteredDeckError(err.message, help_page, context, backtrace) elif val == kind.PROTO_ERROR: - return LocalizedError(err.localized) + return BackendError(err.message, help_page, context, backtrace) elif val == kind.SEARCH_ERROR: - return SearchError(markdown(err.localized)) + return SearchError(markdown(err.message), help_page, context, backtrace) elif val == kind.UNDO_EMPTY: - return UndoEmpty() + return UndoEmpty(err.message, help_page, context, backtrace) elif val == kind.CUSTOM_STUDY_ERROR: - return CustomStudyError(err.localized) + return CustomStudyError(err.message, help_page, context, backtrace) else: # sadly we can't do exhaustiveness checking on protobuf enums # assert_exhaustive(val) - return LocalizedError(err.localized) + return BackendError(err.message, help_page, context, backtrace) diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index b1ca0c7c6..a3778a65d 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -25,30 +25,31 @@ class AnkiException(Exception): """ -class LocalizedError(AnkiException): - "An error with a localized description." +class BackendError(AnkiException): + "An error originating from Anki's backend." - def __init__(self, localized: str) -> None: - self._localized = localized + def __init__( + self, + message: str, + help_page: anki.collection.HelpPage.V | None, + context: str | None, + backtrace: str | None, + ) -> None: super().__init__() + self._message = message + self.help_page = help_page + self.context = context + self.backtrace = backtrace def __str__(self) -> str: - return self._localized + return self._message -class DocumentedError(LocalizedError): - """A localized error described in the manual.""" - - def __init__(self, localized: str, help_page: anki.collection.HelpPage.V) -> None: - self.help_page = help_page - super().__init__(localized) - - -class Interrupted(AnkiException): +class Interrupted(BackendError): pass -class NetworkError(LocalizedError): +class NetworkError(BackendError): pass @@ -57,57 +58,64 @@ class SyncErrorKind(Enum): OTHER = 2 -class SyncError(LocalizedError): - def __init__(self, localized: str, kind: SyncErrorKind): +class SyncError(BackendError): + def __init__( + self, + message: str, + help_page: anki.collection.HelpPage.V | None, + context: str | None, + backtrace: str | None, + kind: SyncErrorKind, + ): self.kind = kind - super().__init__(localized) + super().__init__(message, help_page, context, backtrace) -class BackendIOError(LocalizedError): +class BackendIOError(BackendError): pass -class CustomStudyError(LocalizedError): +class CustomStudyError(BackendError): pass -class DBError(LocalizedError): +class DBError(BackendError): pass -class CardTypeError(DocumentedError): +class CardTypeError(BackendError): pass -class TemplateError(LocalizedError): +class TemplateError(BackendError): pass -class NotFoundError(AnkiException): +class NotFoundError(BackendError): pass -class DeletedError(LocalizedError): +class DeletedError(BackendError): pass -class ExistsError(AnkiException): +class ExistsError(BackendError): pass -class UndoEmpty(AnkiException): +class UndoEmpty(BackendError): pass -class FilteredDeckError(LocalizedError): +class FilteredDeckError(BackendError): pass -class InvalidInput(LocalizedError): +class InvalidInput(BackendError): pass -class SearchError(LocalizedError): +class SearchError(BackendError): pass diff --git a/pylib/rsbridge/cargo/BUILD.bazel b/pylib/rsbridge/cargo/BUILD.bazel index cf09ad565..8e90077cc 100644 --- a/pylib/rsbridge/cargo/BUILD.bazel +++ b/pylib/rsbridge/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", diff --git a/qt/.pylintrc b/qt/.pylintrc index ba32bc5a6..0aaa1c769 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -4,22 +4,7 @@ extension-pkg-whitelist=PyQt6 ignore = forms,hooks_gen.py [TYPECHECK] -ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio -ignored-classes= - BrowserColumns, - BrowserRow, - SearchNode, - ConfigKey, - OpChanges, - UnburyDeckRequest, - CardAnswer, - QueuedCards, - ChangeNotetypeRequest, - CustomStudyRequest, - Cram, - ScheduleCardsAsNewRequest, - CsvColumn, - CsvMetadata, +ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio,anki.scheduler_pb2 [REPORTS] output-format=colorized diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index e3621e1b3..3e50e30b8 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -11,7 +11,7 @@ from anki.cards import Card, CardId from anki.collection import BrowserColumns as Columns from anki.collection import Collection from anki.consts import * -from anki.errors import LocalizedError, NotFoundError +from anki.errors import BackendError, NotFoundError from anki.notes import Note, NoteId from aqt import gui_hooks from aqt.browser.table import Cell, CellRow, Column, ItemId, SearchContext @@ -101,7 +101,7 @@ class DataModel(QAbstractTableModel): def _fetch_row_from_backend(self, item: ItemId) -> CellRow: try: row = CellRow(*self.col.browser_row_for_id(item)) - except LocalizedError as e: + except BackendError as e: return CellRow.disabled(self.len_columns(), str(e)) except Exception as e: return CellRow.disabled( diff --git a/qt/aqt/browser/table/state.py b/qt/aqt/browser/table/state.py index 579b9f2ab..91f368844 100644 --- a/qt/aqt/browser/table/state.py +++ b/qt/aqt/browser/table/state.py @@ -194,7 +194,7 @@ class NoteState(ItemState): def get_card(self, item: ItemId) -> Card: if cards := self.get_note(item).cards(): return cards[0] - raise NotFoundError + raise NotFoundError("card not found", None, None, None) def get_note(self, item: ItemId) -> Note: return self.col.get_note(NoteId(item)) diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index 6fb5469b6..7993693e0 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Optional, TextIO, cast from markdown import markdown import aqt -from anki.errors import DocumentedError, Interrupted, LocalizedError +from anki.errors import BackendError, Interrupted from aqt.qt import * from aqt.utils import showText, showWarning, supportText, tr @@ -25,15 +25,19 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None: if isinstance(exception, Interrupted): # nothing to do return - help_page = exception.help_page if isinstance(exception, DocumentedError) else None - if not isinstance(exception, LocalizedError): + if isinstance(exception, BackendError): + if exception.context: + print(exception.context) + if exception.backtrace: + print(exception.backtrace) + showWarning(str(exception), parent=parent, help=exception.help_page) + else: # if the error is not originating from the backend, dump # a traceback to the console to aid in debugging traceback.print_exception( None, exception, exception.__traceback__, file=sys.stdout ) - - showWarning(str(exception), parent=parent, help=help_page) + showWarning(str(exception), parent=parent) if not os.environ.get("DEBUG"): diff --git a/qt/bundle/build.py b/qt/bundle/build.py index 72627ef97..686ce6671 100644 --- a/qt/bundle/build.py +++ b/qt/bundle/build.py @@ -35,7 +35,6 @@ cargo_target = output_root / f"target-{platform.machine()}" artifacts = output_root / "artifacts" pyo3_config = output_root / "pyo3-build-config-file.txt" pyoxidizer_folder = bazel_external / "pyoxidizer" -arm64_protobuf_wheel = bazel_external / "protobuf_wheel_mac_arm64" pyoxidizer_binary = cargo_target / "release" / with_exe_extension("pyoxidizer") for path in dist_folder.glob("*.zst"): @@ -64,7 +63,7 @@ elif sys.platform.startswith("darwin"): else: pyqt5_folder_name = "pyqt514" os.environ["TARGET"] = "x86_64-apple-darwin" - os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.13" + os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.14" else: is_lin = True if platform.machine() == "x86_64": @@ -133,17 +132,8 @@ def install_wheels_into_venv(): buf = f.read() with open(constraints, "w") as f: extracted = re.findall("^(\S+==\S+) ", buf, flags=re.M) - extracted = [ - line for line in extracted if not arm64_mac or "protobuf" not in line - ] + extracted = [line for line in extracted if "protobuf" not in line] f.write("\n".join(extracted)) - # pypi protobuf lacks C extension on darwin-arm64, so we have to use a version - # we built ourselves - if arm64_mac: - wheels = glob.glob(str(arm64_protobuf_wheel / "*.whl")) - subprocess.run( - [pip, "install", "--upgrade", "-c", constraints, *wheels], check=True - ) # install wheels and upgrade any deps wheels = glob.glob(str(workspace / ".bazel" / "out" / "dist" / "*.whl")) subprocess.run( diff --git a/repos.bzl b/repos.bzl index 3a51186c8..0b45a64bc 100644 --- a/repos.bzl +++ b/repos.bzl @@ -115,12 +115,12 @@ def register_repos(): ################ core_i18n_repo = "anki-core-i18n" - core_i18n_commit = "b9d5c896f22fe6e79810194f41222d8638c13e16" - core_i18n_zip_csum = "8ef7888373cacf682c17f41056dc1f5348f60a15e1809c8db0f66f4072e7d5fb" + core_i18n_commit = "3817899a01a67c8d3a9426268af466b935d762b8" + core_i18n_zip_csum = "7146dac6b6f6b2dafe6b6a9e03a9126e2a7772abb9a16fe66ecb359ef5010675" qtftl_i18n_repo = "anki-desktop-ftl" - qtftl_i18n_commit = "a8bd0e284e2785421180af2ce10dd1d534b0033d" - qtftl_i18n_zip_csum = "f88398324a64be99521bd5cd7e79e7dda64c31a2cd4e568328a211c7765b23ac" + qtftl_i18n_commit = "f1f4859e6bcdd18f0b077ad4f9169a518d9634bf" + qtftl_i18n_zip_csum = "96fafee1fe6416586775fd33ce08d5656c7d41b3e09ef680eb62a1f486b8f35c" i18n_build_content = """ filegroup( @@ -194,16 +194,6 @@ exports_files(["l10n.toml"]) sha256 = "0815a601baba05e03bc36b568cdc2332b1cf4aa17125fc33c69de125f8dd687f", ) - maybe( - http_archive, - name = "protobuf_wheel_mac_arm64", - build_file_content = " ", - urls = [ - "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/protobuf-wheel-mac-arm64.tar", - ], - sha256 = "401d1cd6d949af463b3945f0d5dc887185b27fa5478cb6847bf94f680ea797b4", - ) - maybe( http_archive, name = "audio_mac_amd64", diff --git a/rslib/BUILD.bazel b/rslib/BUILD.bazel index 7dfb7abfb..a29eedb91 100644 --- a/rslib/BUILD.bazel +++ b/rslib/BUILD.bazel @@ -77,6 +77,7 @@ rust_library( "//rslib/cargo:bytes", "//rslib/cargo:chrono", "//rslib/cargo:coarsetime", + "//rslib/cargo:convert_case", "//rslib/cargo:csv", "//rslib/cargo:dissimilar", "//rslib/cargo:flate2", @@ -110,6 +111,7 @@ rust_library( "//rslib/cargo:slog_async", "//rslib/cargo:slog_envlogger", "//rslib/cargo:slog_term", + "//rslib/cargo:snafu", "//rslib/cargo:strum", "//rslib/cargo:tempfile", "//rslib/cargo:tokio", diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index efc87b693..f694e1132 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -103,3 +103,5 @@ zstd = { version="0.11.2", features=["zstdmt"] } num_cpus = "1.13.1" csv = { git="https://github.com/ankitects/rust-csv.git", rev="1c9d3aab6f79a7d815c69f925a46a4590c115f90" } dissimilar = "1.0.4" +snafu = { version = "0.7.2", features = ["backtraces"] } +convert_case = "0.6.0" diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index c8f85b131..9c819f9fe 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -32,7 +32,7 @@ pub trait Service { } buf.push_str( r#" - _ => Err(crate::error::AnkiError::invalid_input("invalid command")), + _ => crate::invalid_input!("invalid command"), } } "#, diff --git a/rslib/cargo/BUILD.bazel b/rslib/cargo/BUILD.bazel index cf09ad565..8e90077cc 100644 --- a/rslib/cargo/BUILD.bazel +++ b/rslib/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", diff --git a/rslib/i18n/cargo/BUILD.bazel b/rslib/i18n/cargo/BUILD.bazel index cf09ad565..8e90077cc 100644 --- a/rslib/i18n/cargo/BUILD.bazel +++ b/rslib/i18n/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", diff --git a/rslib/i18n_helpers/cargo/BUILD.bazel b/rslib/i18n_helpers/cargo/BUILD.bazel index cf09ad565..8e90077cc 100644 --- a/rslib/i18n_helpers/cargo/BUILD.bazel +++ b/rslib/i18n_helpers/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", diff --git a/rslib/linkchecker/cargo/BUILD.bazel b/rslib/linkchecker/cargo/BUILD.bazel index cf09ad565..8e90077cc 100644 --- a/rslib/linkchecker/cargo/BUILD.bazel +++ b/rslib/linkchecker/cargo/BUILD.bazel @@ -66,6 +66,15 @@ alias( ], ) +alias( + name = "convert_case", + actual = "@raze__convert_case__0_6_0//:convert_case", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "csv", actual = "@raze__csv__1_1_6//:csv", @@ -489,6 +498,15 @@ alias( ], ) +alias( + name = "snafu", + actual = "@raze__snafu__0_7_2//:snafu", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "strum", actual = "@raze__strum__0_24_1//:strum", diff --git a/rslib/src/adding.rs b/rslib/src/adding.rs index 5a8051c71..a3994ee92 100644 --- a/rslib/src/adding.rs +++ b/rslib/src/adding.rs @@ -65,7 +65,7 @@ impl Collection { return Ok(home_deck); } // default deck - self.get_deck(DeckId(1))?.ok_or(AnkiError::NotFound) + self.get_deck(DeckId(1))?.or_not_found(DeckId(1)) } fn get_current_notetype_for_adding(&mut self) -> Result> { @@ -79,7 +79,7 @@ impl Collection { if let Some((ntid, _)) = self.storage.get_all_notetype_names()?.first() { Ok(self.get_notetype(*ntid)?.unwrap()) } else { - Err(AnkiError::NotFound) + invalid_input!("collection has no notetypes"); } } diff --git a/rslib/src/backend/card.rs b/rslib/src/backend/card.rs index 0cafc0f8c..7736985f5 100644 --- a/rslib/src/backend/card.rs +++ b/rslib/src/backend/card.rs @@ -11,10 +11,11 @@ use crate::{ impl CardsService for Backend { fn get_card(&self, input: pb::CardId) -> Result { + let cid = input.into(); self.with_col(|col| { col.storage - .get_card(input.into()) - .and_then(|opt| opt.ok_or(AnkiError::NotFound)) + .get_card(cid) + .and_then(|opt| opt.or_not_found(cid)) .map(Into::into) }) } @@ -67,10 +68,8 @@ impl TryFrom for Card { type Error = AnkiError; fn try_from(c: pb::Card) -> Result { - let ctype = CardType::try_from(c.ctype as u8) - .map_err(|_| AnkiError::invalid_input("invalid card type"))?; - let queue = CardQueue::try_from(c.queue as i8) - .map_err(|_| AnkiError::invalid_input("invalid card queue"))?; + let ctype = CardType::try_from(c.ctype as u8).or_invalid("invalid card type")?; + let queue = CardQueue::try_from(c.queue as i8).or_invalid("invalid card queue")?; Ok(Card { id: CardId(c.id), note_id: NoteId(c.note_id), diff --git a/rslib/src/backend/cardrendering.rs b/rslib/src/backend/cardrendering.rs index 6b7177f43..252be16cc 100644 --- a/rslib/src/backend/cardrendering.rs +++ b/rslib/src/backend/cardrendering.rs @@ -86,11 +86,8 @@ impl CardRenderingService for Backend { &self, input: pb::RenderUncommittedCardRequest, ) -> Result { - let template = input.template.ok_or(AnkiError::NotFound)?.into(); - let mut note = input - .note - .ok_or_else(|| AnkiError::invalid_input("missing note"))? - .into(); + let template = input.template.or_invalid("missing template")?.into(); + let mut note = input.note.or_invalid("missing note")?.into(); let ord = input.card_ord as u16; let fill_empty = input.fill_empty; self.with_col(|col| { @@ -105,10 +102,7 @@ impl CardRenderingService for Backend { ) -> Result { let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; let template = schema11.into(); - let mut note = input - .note - .ok_or_else(|| AnkiError::invalid_input("missing note"))? - .into(); + let mut note = input.note.or_invalid("missing note")?.into(); let ord = input.card_ord as u16; let fill_empty = input.fill_empty; self.with_col(|col| { diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 7c89562c5..2b3641768 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -57,7 +57,7 @@ impl ConfigService for Backend { fn get_config_json(&self, input: pb::String) -> Result { self.with_col(|col| { let val: Option = col.get_config_optional(input.val.as_str()); - val.ok_or(AnkiError::NotFound) + val.or_not_found(input.val) .and_then(|v| serde_json::to_vec(&v).map_err(Into::into)) .map(Into::into) }) diff --git a/rslib/src/backend/decks.rs b/rslib/src/backend/decks.rs index be2a5cafb..aba633e7c 100644 --- a/rslib/src/backend/decks.rs +++ b/rslib/src/backend/decks.rs @@ -80,21 +80,14 @@ impl DecksService for Backend { fn get_deck_id_by_name(&self, input: pb::String) -> Result { self.with_col(|col| { - col.get_deck_id(&input.val).and_then(|d| { - d.ok_or(AnkiError::NotFound) - .map(|d| pb::DeckId { did: d.0 }) - }) + col.get_deck_id(&input.val) + .and_then(|d| d.or_not_found(input.val).map(|d| pb::DeckId { did: d.0 })) }) } fn get_deck(&self, input: pb::DeckId) -> Result { - self.with_col(|col| { - Ok(col - .storage - .get_deck(input.into())? - .ok_or(AnkiError::NotFound)? - .into()) - }) + let did = input.into(); + self.with_col(|col| Ok(col.storage.get_deck(did)?.or_not_found(did)?.into())) } fn update_deck(&self, input: pb::Deck) -> Result { @@ -113,12 +106,9 @@ impl DecksService for Backend { } fn get_deck_legacy(&self, input: pb::DeckId) -> Result { + let did = input.into(); self.with_col(|col| { - let deck: DeckSchema11 = col - .storage - .get_deck(input.into())? - .ok_or(AnkiError::NotFound)? - .into(); + let deck: DeckSchema11 = col.storage.get_deck(did)?.or_not_found(did)?.into(); serde_json::to_vec(&deck) .map_err(Into::into) .map(Into::into) @@ -273,10 +263,7 @@ impl TryFrom for Deck { mtime_secs: TimestampSecs(d.mtime_secs), usn: Usn(d.usn), common: d.common.unwrap_or_default(), - kind: d - .kind - .ok_or_else(|| AnkiError::invalid_input("missing kind"))? - .into(), + kind: d.kind.or_invalid("missing kind")?.into(), }) } } diff --git a/rslib/src/backend/error.rs b/rslib/src/backend/error.rs index a385a2ce8..41dc676c1 100644 --- a/rslib/src/backend/error.rs +++ b/rslib/src/backend/error.rs @@ -9,43 +9,46 @@ use crate::{ }; impl AnkiError { - pub(super) fn into_protobuf(self, tr: &I18n) -> pb::BackendError { - let localized = self.localized_description(tr); + pub fn into_protobuf(self, tr: &I18n) -> pb::BackendError { + let message = self.message(tr); let help_page = self.help_page().map(|page| page as i32); + let context = self.context(); + let backtrace = self.backtrace(); let kind = match self { - AnkiError::InvalidInput(_) => Kind::InvalidInput, - AnkiError::TemplateError(_) => Kind::TemplateParse, - AnkiError::IoError(_) => Kind::IoError, - AnkiError::DbError(_) => Kind::DbError, - AnkiError::NetworkError(_) => Kind::NetworkError, - AnkiError::SyncError(err) => err.kind.into(), + AnkiError::InvalidInput { .. } => Kind::InvalidInput, + AnkiError::TemplateError { .. } => Kind::TemplateParse, + AnkiError::DbError { .. } => Kind::DbError, + AnkiError::NetworkError { .. } => Kind::NetworkError, + AnkiError::SyncError { source } => source.kind.into(), AnkiError::Interrupted => Kind::Interrupted, AnkiError::CollectionNotOpen => Kind::InvalidInput, AnkiError::CollectionAlreadyOpen => Kind::InvalidInput, - AnkiError::JsonError(_) => Kind::JsonError, - AnkiError::ProtoError(_) => Kind::ProtoError, - AnkiError::NotFound => Kind::NotFoundError, + AnkiError::JsonError { .. } => Kind::JsonError, + AnkiError::ProtoError { .. } => Kind::ProtoError, + AnkiError::NotFound { .. } => Kind::NotFoundError, AnkiError::Deleted => Kind::Deleted, AnkiError::Existing => Kind::Exists, - AnkiError::FilteredDeckError(_) => Kind::FilteredDeckError, - AnkiError::SearchError(_) => Kind::SearchError, - AnkiError::CardTypeError(_) => Kind::CardTypeError, + AnkiError::FilteredDeckError { .. } => Kind::FilteredDeckError, + AnkiError::SearchError { .. } => Kind::SearchError, + AnkiError::CardTypeError { .. } => Kind::CardTypeError, AnkiError::ParseNumError => Kind::InvalidInput, - AnkiError::InvalidRegex(_) => Kind::InvalidInput, + AnkiError::InvalidRegex { .. } => Kind::InvalidInput, AnkiError::UndoEmpty => Kind::UndoEmpty, AnkiError::MultipleNotetypesSelected => Kind::InvalidInput, AnkiError::DatabaseCheckRequired => Kind::InvalidInput, - AnkiError::CustomStudyError(_) => Kind::CustomStudyError, - AnkiError::ImportError(_) => Kind::ImportError, - AnkiError::FileIoError(_) => Kind::IoError, + AnkiError::CustomStudyError { .. } => Kind::CustomStudyError, + AnkiError::ImportError { .. } => Kind::ImportError, + AnkiError::FileIoError { .. } => Kind::IoError, AnkiError::MediaCheckRequired => Kind::InvalidInput, AnkiError::InvalidId => Kind::InvalidInput, }; pb::BackendError { kind: kind as i32, - localized, + message, help_page, + context, + backtrace, } } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 818f8ffed..a3cffbb05 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -60,13 +60,7 @@ use self::{ sync::{SyncService, SyncState}, tags::TagsService, }; -use crate::{ - backend::dbproxy::db_command_bytes, - collection::Collection, - error::{AnkiError, Result}, - i18n::I18n, - log, pb, -}; +use crate::{backend::dbproxy::db_command_bytes, log, pb, prelude::*}; pub struct Backend { col: Arc>>, @@ -126,7 +120,7 @@ impl Backend { input: &[u8], ) -> result::Result, Vec> { pb::ServiceIndex::from_i32(service as i32) - .ok_or_else(|| AnkiError::invalid_input("invalid service")) + .or_invalid("invalid service") .and_then(|service| match service { pb::ServiceIndex::Scheduler => SchedulerService::run_method(self, method, input), pb::ServiceIndex::Decks => DecksService::run_method(self, method, input), diff --git a/rslib/src/backend/notes.rs b/rslib/src/backend/notes.rs index 2f349f3fd..e8c51c118 100644 --- a/rslib/src/backend/notes.rs +++ b/rslib/src/backend/notes.rs @@ -13,15 +13,16 @@ use crate::{ impl NotesService for Backend { fn new_note(&self, input: pb::NotetypeId) -> Result { + let ntid = input.into(); self.with_col(|col| { - let nt = col.get_notetype(input.into())?.ok_or(AnkiError::NotFound)?; + let nt = col.get_notetype(ntid)?.or_not_found(ntid)?; Ok(nt.new_note().into()) }) } fn add_note(&self, input: pb::AddNoteRequest) -> Result { self.with_col(|col| { - let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); + let mut note: Note = input.note.or_invalid("no note provided")?.into(); let changes = col.add_note(&mut note, DeckId(input.deck_id))?; Ok(pb::AddNoteResponse { note_id: note.id.0, @@ -62,12 +63,8 @@ impl NotesService for Backend { } fn get_note(&self, input: pb::NoteId) -> Result { - self.with_col(|col| { - col.storage - .get_note(input.into())? - .ok_or(AnkiError::NotFound) - .map(Into::into) - }) + let nid = input.into(); + self.with_col(|col| col.storage.get_note(nid)?.or_not_found(nid).map(Into::into)) } fn remove_notes(&self, input: pb::RemoveNotesRequest) -> Result { diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs index dea4fde61..c2d72ba41 100644 --- a/rslib/src/backend/notetypes.rs +++ b/rslib/src/backend/notetypes.rs @@ -80,21 +80,20 @@ impl NotetypesService for Backend { } fn get_notetype(&self, input: pb::NotetypeId) -> Result { + let ntid = input.into(); self.with_col(|col| { col.storage - .get_notetype(input.into())? - .ok_or(AnkiError::NotFound) + .get_notetype(ntid)? + .or_not_found(ntid) .map(Into::into) }) } fn get_notetype_legacy(&self, input: pb::NotetypeId) -> Result { + let ntid = input.into(); self.with_col(|col| { - let schema11: NotetypeSchema11 = col - .storage - .get_notetype(input.into())? - .ok_or(AnkiError::NotFound)? - .into(); + let schema11: NotetypeSchema11 = + col.storage.get_notetype(ntid)?.or_not_found(ntid)?.into(); Ok(serde_json::to_vec(&schema11)?).map(Into::into) }) } @@ -131,7 +130,7 @@ impl NotetypesService for Backend { self.with_col(|col| { col.storage .get_notetype_id(&input.val) - .and_then(|nt| nt.ok_or(AnkiError::NotFound)) + .and_then(|nt| nt.or_not_found(input.val)) .map(|ntid| pb::NotetypeId { ntid: ntid.0 }) }) } diff --git a/rslib/src/backend/search/search_node.rs b/rslib/src/backend/search/search_node.rs index 0354fb17d..0ae6cae11 100644 --- a/rslib/src/backend/search/search_node.rs +++ b/rslib/src/backend/search/search_node.rs @@ -70,7 +70,7 @@ impl TryFrom for Node { Filter::Negated(term) => Node::try_from(*term)?.negated(), Filter::Group(mut group) => { match group.nodes.len() { - 0 => return Err(AnkiError::invalid_input("empty group")), + 0 => invalid_input!("empty group"), // a group of 1 doesn't need to be a group 1 => group.nodes.pop().unwrap().try_into()?, // 2+ nodes diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 5362d6cb8..a1b85e0fd 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -236,7 +236,7 @@ impl Collection { self.state .active_browser_columns .as_ref() - .ok_or_else(|| AnkiError::invalid_input("Active browser columns not set."))?, + .or_invalid("Active browser columns not set.")?, ); RowContext::new(self, id, notes_mode, card_render_required(&columns))?.browser_row(&columns) } @@ -250,7 +250,7 @@ impl Collection { } else { self.storage.get_note_without_fields(id)? } - .ok_or(AnkiError::NotFound) + .or_not_found(id) } } @@ -264,7 +264,7 @@ impl RenderContext { question: rendered_nodes_to_str(&render.qnodes), answer_nodes: render.anodes, }, - Err(err) => RenderContext::Err(err.localized_description(&col.tr)), + Err(err) => RenderContext::Err(err.message(&col.tr)), } } @@ -312,12 +312,9 @@ impl RowContext { if notes_mode { note = col .get_note_maybe_with_fields(NoteId(id), with_card_render) - .map_err(|e| { - if e == AnkiError::NotFound { - AnkiError::Deleted - } else { - e - } + .map_err(|e| match e { + AnkiError::NotFound { .. } => AnkiError::Deleted, + _ => e, })?; cards = col.storage.all_cards_of_note(note.id)?; if cards.is_empty() { @@ -332,12 +329,14 @@ impl RowContext { } let notetype = col .get_notetype(note.notetype_id)? - .ok_or(AnkiError::NotFound)?; - let deck = col.get_deck(cards[0].deck_id)?.ok_or(AnkiError::NotFound)?; + .or_not_found(note.notetype_id)?; + let deck = col + .get_deck(cards[0].deck_id)? + .or_not_found(cards[0].deck_id)?; let original_deck = if cards[0].original_deck_id.0 != 0 { Some( col.get_deck(cards[0].original_deck_id)? - .ok_or(AnkiError::NotFound)?, + .or_not_found(cards[0].original_deck_id)?, ) } else { None diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index dfd5c9fa1..a38e70575 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -190,7 +190,7 @@ impl Collection { if undoable { self.transact(Op::UpdateCard, |col| { for mut card in cards { - let existing = col.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?; + let existing = col.storage.get_card(card.id)?.or_not_found(card.id)?; col.update_card_inner(&mut card, existing, col.usn()?)? } Ok(()) @@ -198,7 +198,7 @@ impl Collection { } else { self.transact_no_undo(|col| { for mut card in cards { - let existing = col.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?; + let existing = col.storage.get_card(card.id)?.or_not_found(card.id)?; col.update_card_inner(&mut card, existing, col.usn()?)?; } Ok(OpOutput { @@ -220,10 +220,7 @@ impl Collection { where F: FnOnce(&mut Card) -> Result, { - let orig = self - .storage - .get_card(cid)? - .ok_or_else(|| AnkiError::invalid_input("no such card"))?; + let orig = self.storage.get_card(cid)?.or_invalid("no such card")?; let mut card = orig.clone(); func(&mut card)?; self.update_card_inner(&mut card, orig, self.usn()?)?; @@ -242,9 +239,7 @@ impl Collection { } pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> { - if card.id.0 != 0 { - return Err(AnkiError::invalid_input("card id already set")); - } + require!(card.id.0 == 0, "card id already set"); card.mtime = TimestampSecs::now(); card.usn = self.usn()?; self.add_card_undoable(card) @@ -271,10 +266,10 @@ impl Collection { } pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result> { - let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; - let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError( - FilteredDeckError::CanNotMoveCardsInto, - ))?; + let deck = self.get_deck(deck_id)?.or_not_found(deck_id)?; + let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError { + source: FilteredDeckError::CanNotMoveCardsInto, + })?; let config = self.get_deck_config(config_id, true)?.unwrap(); let mut steps_adjuster = RemainingStepsAdjuster::new(&config); let sched = self.scheduler_version(); @@ -296,9 +291,7 @@ impl Collection { } pub fn set_card_flag(&mut self, cards: &[CardId], flag: u32) -> Result> { - if flag > 7 { - return Err(AnkiError::invalid_input("invalid flag")); - } + require!(flag < 8, "invalid flag"); let flag = flag as u8; let usn = self.usn()?; diff --git a/rslib/src/card/undo.rs b/rslib/src/card/undo.rs index e83f096f7..e0b80a1fd 100644 --- a/rslib/src/card/undo.rs +++ b/rslib/src/card/undo.rs @@ -20,7 +20,7 @@ impl Collection { let current = self .storage .get_card(card.id)? - .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; + .or_invalid("card disappeared")?; self.update_card_undoable(&mut *card, current) } UndoableCardChange::Removed(card) => self.restore_deleted_card(*card), @@ -44,9 +44,7 @@ impl Collection { } pub(super) fn update_card_undoable(&mut self, card: &mut Card, original: Card) -> Result<()> { - if card.id.0 == 0 { - return Err(AnkiError::invalid_input("card id not set")); - } + require!(card.id.0 != 0, "card id not set"); self.save_undo(UndoableCardChange::Updated(Box::new(original))); self.storage.update_card(card) } diff --git a/rslib/src/collection/backup.rs b/rslib/src/collection/backup.rs index 71379222b..a08abe76e 100644 --- a/rslib/src/collection/backup.rs +++ b/rslib/src/collection/backup.rs @@ -14,7 +14,8 @@ use itertools::Itertools; use log::error; use crate::{ - import_export::package::export_colpkg_from_data, log, pb::preferences::BackupLimits, prelude::*, + import_export::package::export_colpkg_from_data, io::read_file, log, + pb::preferences::BackupLimits, prelude::*, }; const BACKUP_FORMAT_STRING: &str = "backup-%Y-%m-%d-%H.%M.%S.colpkg"; @@ -37,7 +38,7 @@ impl Collection { let log = self.log.clone(); let tr = self.tr.clone(); self.storage.checkpoint()?; - let col_data = std::fs::read(&self.col_path)?; + let col_data = read_file(&self.col_path)?; self.update_last_backup_timestamp()?; Ok(Some(thread::spawn(move || { backup_inner(&col_data, &backup_folder, limits, log, &tr) diff --git a/rslib/src/config/notetype.rs b/rslib/src/config/notetype.rs index ae21a4830..9256db50f 100644 --- a/rslib/src/config/notetype.rs +++ b/rslib/src/config/notetype.rs @@ -23,7 +23,7 @@ impl Collection { card_ordinal: usize, key: &str, ) -> Result { - let nt = self.get_notetype(ntid)?.ok_or(AnkiError::NotFound)?; + let nt = self.get_notetype(ntid)?.or_not_found(ntid)?; let ordinal = if matches!(nt.config.kind(), NotetypeKind::Cloze) { 0 } else { diff --git a/rslib/src/config/undo.rs b/rslib/src/config/undo.rs index 6efc00fc7..25dbb352d 100644 --- a/rslib/src/config/undo.rs +++ b/rslib/src/config/undo.rs @@ -19,7 +19,7 @@ impl Collection { let current = self .storage .get_config_entry(&entry.key)? - .ok_or_else(|| AnkiError::invalid_input("config disappeared"))?; + .or_invalid("config disappeared")?; self.update_config_entry_undoable(entry, current) .map(|_| ()) } diff --git a/rslib/src/dbcheck.rs b/rslib/src/dbcheck.rs index 0c15a1eda..9f79e3527 100644 --- a/rslib/src/dbcheck.rs +++ b/rslib/src/dbcheck.rs @@ -304,10 +304,13 @@ impl Collection { match self.storage.get_note(nid) { Ok(note) => Ok(note.unwrap()), Err(err) => match err { - AnkiError::DbError(DbError { - kind: DbErrorKind::Utf8, - .. - }) => { + AnkiError::DbError { + source: + DbError { + kind: DbErrorKind::Utf8, + .. + }, + } => { // fix note then fetch again self.storage.fix_invalid_utf8_in_note(nid)?; out.invalid_utf8 += 1; diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index fca8a38f9..c724d02de 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -19,14 +19,7 @@ pub use crate::pb::deck_config::{ /// Old deck config and cards table store 250% as 2500. pub(crate) const INITIAL_EASE_FACTOR_THOUSANDS: u16 = (INITIAL_EASE_FACTOR * 1000.0) as u16; -use crate::{ - collection::Collection, - define_newtype, - error::{AnkiError, Result}, - scheduler::states::review::INITIAL_EASE_FACTOR, - timestamp::{TimestampMillis, TimestampSecs}, - types::Usn, -}; +use crate::{define_newtype, prelude::*, scheduler::states::review::INITIAL_EASE_FACTOR}; define_newtype!(DeckConfigId, i64); @@ -121,7 +114,7 @@ impl Collection { let original = self .storage .get_deck_config(config.id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(config.id)?; self.update_deck_config_inner(config, original, usn) } } @@ -176,13 +169,8 @@ impl Collection { /// Remove a deck configuration. This will force a full sync. pub(crate) fn remove_deck_config_inner(&mut self, dcid: DeckConfigId) -> Result<()> { - if dcid.0 == 1 { - return Err(AnkiError::invalid_input("can't delete default conf")); - } - let conf = self - .storage - .get_deck_config(dcid)? - .ok_or(AnkiError::NotFound)?; + require!(dcid.0 != 1, "can't delete default conf"); + let conf = self.storage.get_deck_config(dcid)?.or_not_found(dcid)?; self.set_schema_modified()?; self.remove_deck_config_undoable(conf) } diff --git a/rslib/src/deckconfig/undo.rs b/rslib/src/deckconfig/undo.rs index 0a0ed7a2d..fcffa9ed0 100644 --- a/rslib/src/deckconfig/undo.rs +++ b/rslib/src/deckconfig/undo.rs @@ -22,7 +22,7 @@ impl Collection { let current = self .storage .get_deck_config(config.id)? - .ok_or_else(|| AnkiError::invalid_input("deck config disappeared"))?; + .or_invalid("deck config disappeared")?; self.update_deck_config_undoable(&config, current) } UndoableDeckConfigChange::Removed(config) => self.restore_deleted_deck_config(*config), diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index ed735f8bf..e99064b02 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -88,7 +88,7 @@ impl Collection { } fn get_current_deck_for_update(&mut self, deck: DeckId) -> Result { - let deck = self.get_deck(deck)?.ok_or(AnkiError::NotFound)?; + let deck = self.get_deck(deck)?.or_not_found(deck)?; let normal = deck.normal()?; let today = self.timing_today()?.days_elapsed; @@ -119,9 +119,7 @@ impl Collection { } fn update_deck_configs_inner(&mut self, mut input: UpdateDeckConfigsRequest) -> Result<()> { - if input.configs.is_empty() { - return Err(AnkiError::invalid_input("config not provided")); - } + require!(!input.configs.is_empty(), "config not provided"); let configs_before_update = self.storage.get_deck_config_map()?; let mut configs_after_update = configs_before_update.clone(); @@ -142,7 +140,7 @@ impl Collection { let deck = self .storage .get_deck(input.target_deck_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(input.target_deck_id)?; self.storage .child_decks(&deck)? .iter() diff --git a/rslib/src/decks/addupdate.rs b/rslib/src/decks/addupdate.rs index 159c57abd..180001ba1 100644 --- a/rslib/src/decks/addupdate.rs +++ b/rslib/src/decks/addupdate.rs @@ -14,7 +14,7 @@ impl Collection { pub fn update_deck(&mut self, deck: &mut Deck) -> Result> { self.transact(Op::UpdateDeck, |col| { - let existing_deck = col.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; + let existing_deck = col.storage.get_deck(deck.id)?.or_not_found(deck.id)?; col.update_deck_inner(deck, existing_deck, col.usn()?) }) } @@ -43,9 +43,7 @@ impl Collection { } pub(crate) fn add_deck_inner(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { - if deck.id.0 != 0 { - return Err(AnkiError::invalid_input("deck to add must have id 0")); - } + require!(deck.id.0 == 0, "deck to add must have id 0"); self.prepare_deck_for_update(deck, usn)?; deck.set_modified(usn); self.match_or_create_parents(deck, usn)?; @@ -153,9 +151,7 @@ impl Collection { machine_name: &str, recursion_level: usize, ) -> Result> { - if recursion_level > 10 { - return Err(AnkiError::invalid_input("deck nesting level too deep")); - } + require!(recursion_level < 11, "deck nesting level too deep"); if let Some(parent_name) = immediate_parent_name(machine_name) { if let Some(parent_did) = self.storage.get_deck_id(parent_name)? { self.storage.get_deck(parent_did) diff --git a/rslib/src/decks/counts.rs b/rslib/src/decks/counts.rs index d1c780aa4..f4e0f5453 100644 --- a/rslib/src/decks/counts.rs +++ b/rslib/src/decks/counts.rs @@ -44,7 +44,7 @@ impl Collection { did: DeckId, ) -> Result { let today = self.current_due_day(0)?; - let mut deck = self.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let mut deck = self.storage.get_deck(did)?.or_not_found(did)?; deck.reset_stats_if_day_changed(today); Ok(pb::CountsForDeckTodayResponse { new: deck.common.new_studied, diff --git a/rslib/src/decks/current.rs b/rslib/src/decks/current.rs index 3b466a052..f2162f431 100644 --- a/rslib/src/decks/current.rs +++ b/rslib/src/decks/current.rs @@ -16,7 +16,7 @@ impl Collection { if let Some(deck) = self.get_deck(self.get_current_deck_id())? { return Ok(deck); } - self.get_deck(DeckId(1))?.ok_or(AnkiError::NotFound) + self.get_deck(DeckId(1))?.or_not_found(DeckId(1)) } } diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 64a43a881..6d3e9ba67 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -91,19 +91,17 @@ impl Deck { #[allow(dead_code)] pub(crate) fn normal(&self) -> Result<&NormalDeck> { - if let DeckKind::Normal(normal) = &self.kind { - Ok(normal) - } else { - Err(AnkiError::invalid_input("deck not normal")) + match &self.kind { + DeckKind::Normal(normal) => Ok(normal), + _ => invalid_input!("deck not normal"), } } #[allow(dead_code)] pub(crate) fn normal_mut(&mut self) -> Result<&mut NormalDeck> { - if let DeckKind::Normal(normal) = &mut self.kind { - Ok(normal) - } else { - Err(AnkiError::invalid_input("deck not normal")) + match &mut self.kind { + DeckKind::Normal(normal) => Ok(normal), + _ => invalid_input!("deck not normal"), } } diff --git a/rslib/src/decks/name.rs b/rslib/src/decks/name.rs index d4445b69a..6da4c9230 100644 --- a/rslib/src/decks/name.rs +++ b/rslib/src/decks/name.rs @@ -111,7 +111,7 @@ impl Collection { pub fn rename_deck(&mut self, did: DeckId, new_human_name: &str) -> Result> { self.transact(Op::RenameDeck, |col| { - let existing_deck = col.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let existing_deck = col.storage.get_deck(did)?.or_not_found(did)?; let mut deck = existing_deck.clone(); deck.name = NativeDeckName::from_human_name(new_human_name); col.update_deck_inner(&mut deck, existing_deck, col.usn()?) diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index 5bbdb81f7..6a07c7191 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -21,7 +21,7 @@ impl Collection { let current = self .storage .get_deck(deck.id)? - .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; + .or_invalid("deck disappeared")?; self.update_single_deck_undoable(&mut *deck, current) } UndoableDeckChange::Removed(deck) => self.restore_deleted_deck(*deck), diff --git a/rslib/src/error/db.rs b/rslib/src/error/db.rs index f233c8076..323017830 100644 --- a/rslib/src/error/db.rs +++ b/rslib/src/error/db.rs @@ -5,10 +5,11 @@ use std::str::Utf8Error; use anki_i18n::I18n; use rusqlite::{types::FromSqlError, Error}; +use snafu::Snafu; use super::AnkiError; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub struct DbError { pub info: String, pub kind: DbErrorKind, @@ -27,10 +28,12 @@ pub enum DbErrorKind { impl AnkiError { pub(crate) fn db_error(info: impl Into, kind: DbErrorKind) -> Self { - AnkiError::DbError(DbError { - info: info.into(), - kind, - }) + AnkiError::DbError { + source: DbError { + info: info.into(), + kind, + }, + } } } @@ -38,19 +41,25 @@ impl From for AnkiError { fn from(err: Error) -> Self { if let Error::SqliteFailure(error, Some(reason)) = &err { if error.code == rusqlite::ErrorCode::DatabaseBusy { - return AnkiError::DbError(DbError { - info: "".to_string(), - kind: DbErrorKind::Locked, - }); + return AnkiError::DbError { + source: DbError { + info: "".to_string(), + kind: DbErrorKind::Locked, + }, + }; } if reason.contains("regex parse error") { - return AnkiError::InvalidRegex(reason.to_owned()); + return AnkiError::InvalidRegex { + info: reason.to_owned(), + }; } } - AnkiError::DbError(DbError { - info: format!("{:?}", err), - kind: DbErrorKind::Other, - }) + AnkiError::DbError { + source: DbError { + info: format!("{:?}", err), + kind: DbErrorKind::Other, + }, + } } } @@ -58,21 +67,25 @@ impl From for AnkiError { fn from(err: FromSqlError) -> Self { if let FromSqlError::Other(ref err) = err { if let Some(_err) = err.downcast_ref::() { - return AnkiError::DbError(DbError { - info: "".to_string(), - kind: DbErrorKind::Utf8, - }); + return AnkiError::DbError { + source: DbError { + info: "".to_string(), + kind: DbErrorKind::Utf8, + }, + }; } } - AnkiError::DbError(DbError { - info: format!("{:?}", err), - kind: DbErrorKind::Other, - }) + AnkiError::DbError { + source: DbError { + info: format!("{:?}", err), + kind: DbErrorKind::Other, + }, + } } } impl DbError { - pub fn localized_description(&self, _tr: &I18n) -> String { + pub fn message(&self, _tr: &I18n) -> String { match self.kind { DbErrorKind::Corrupt => self.info.clone(), // fixme: i18n diff --git a/rslib/src/error/file_io.rs b/rslib/src/error/file_io.rs new file mode 100644 index 000000000..c80dbe037 --- /dev/null +++ b/rslib/src/error/file_io.rs @@ -0,0 +1,87 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::path::PathBuf; + +use snafu::Snafu; + +/// Wrapper for [std::io::Error] with additional information on the attempted +/// operation. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub struct FileIoError { + pub path: PathBuf, + pub op: FileOp, + pub source: std::io::Error, +} + +impl PartialEq for FileIoError { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.op == other.op + } +} + +impl Eq for FileIoError {} + +#[derive(Debug, PartialEq, Clone, Eq)] +pub enum FileOp { + Read, + Open, + Create, + Write, + CopyFrom(PathBuf), + Persist, + Sync, + /// For legacy errors without any context. + Unknown, +} + +impl FileOp { + pub fn copy(from: impl Into) -> Self { + Self::CopyFrom(from.into()) + } +} + +impl FileIoError { + pub fn message(&self) -> String { + format!( + "Failed to {} '{}':
{}", + match &self.op { + FileOp::Unknown => return format!("{}", self.source), + FileOp::Open => "open".into(), + FileOp::Read => "read".into(), + FileOp::Create => "create file in".into(), + FileOp::Write => "write".into(), + FileOp::CopyFrom(p) => format!("copy from '{}' to", p.to_string_lossy()), + FileOp::Persist => "persist".into(), + FileOp::Sync => "sync".into(), + }, + self.path.to_string_lossy(), + self.source + ) + } + + pub(crate) fn is_not_found(&self) -> bool { + self.source.kind() == std::io::ErrorKind::NotFound + } +} + +impl From for FileIoError { + fn from(err: tempfile::PathPersistError) -> Self { + FileIoError { + path: err.path.to_path_buf(), + op: FileOp::Persist, + source: err.error, + } + } +} + +impl From for FileIoError { + fn from(err: tempfile::PersistError) -> Self { + FileIoError { + path: err.file.path().into(), + op: FileOp::Persist, + source: err.error, + } + } +} diff --git a/rslib/src/error/filtered.rs b/rslib/src/error/filtered.rs index 39bb86127..c43b2c5d2 100644 --- a/rslib/src/error/filtered.rs +++ b/rslib/src/error/filtered.rs @@ -2,10 +2,9 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use anki_i18n::I18n; +use snafu::Snafu; -use super::AnkiError; - -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub enum FilteredDeckError { MustBeLeafNode, CanNotMoveCardsInto, @@ -14,7 +13,7 @@ pub enum FilteredDeckError { } impl FilteredDeckError { - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { match self { FilteredDeckError::MustBeLeafNode => tr.errors_filtered_parent_deck(), FilteredDeckError::CanNotMoveCardsInto => { @@ -27,20 +26,14 @@ impl FilteredDeckError { } } -impl From for AnkiError { - fn from(e: FilteredDeckError) -> Self { - AnkiError::FilteredDeckError(e) - } -} - -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub enum CustomStudyError { NoMatchingCards, ExistingDeck, } impl CustomStudyError { - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { match self { Self::NoMatchingCards => tr.custom_study_no_cards_matched_the_criteria_you(), Self::ExistingDeck => tr.custom_study_must_rename_deck(), @@ -48,9 +41,3 @@ impl CustomStudyError { .into() } } - -impl From for AnkiError { - fn from(e: CustomStudyError) -> Self { - AnkiError::CustomStudyError(e) - } -} diff --git a/rslib/src/error/invalid_input.rs b/rslib/src/error/invalid_input.rs new file mode 100644 index 000000000..75f473a56 --- /dev/null +++ b/rslib/src/error/invalid_input.rs @@ -0,0 +1,99 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; + +use crate::prelude::*; + +/// General-purpose error for unexpected [Err]s, [None]s, and other +/// violated constraints. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub), display("{message}"), whatever)] +pub struct InvalidInputError { + pub message: String, + #[snafu(source(from(Box, Some)))] + pub source: Option>, + pub backtrace: Option, +} + +impl InvalidInputError { + pub fn message(&self) -> String { + self.message.clone() + } + + pub fn context(&self) -> String { + if let Some(source) = &self.source { + format!("{}", source) + } else { + String::new() + } + } +} + +impl PartialEq for InvalidInputError { + fn eq(&self, other: &Self) -> bool { + self.message == other.message + } +} + +impl Eq for InvalidInputError {} + +/// Allows generating [AnkiError::InvalidInput] from [Option::None] and the +/// typical [core::result::Result::Err]. +pub trait OrInvalid { + type Value; + fn or_invalid(self, message: impl Into) -> Result; +} + +impl OrInvalid for Option { + type Value = T; + + fn or_invalid(self, message: impl Into) -> Result { + self.whatever_context::<_, InvalidInputError>(message) + .map_err(Into::into) + } +} + +impl OrInvalid for Result { + type Value = T; + + fn or_invalid(self, message: impl Into) -> Result { + self.whatever_context::<_, InvalidInputError>(message) + .map_err(Into::into) + } +} + +/// Returns an [AnkiError::InvalidInput] with the provided format string and an +/// optional underlying error. +#[macro_export] +macro_rules! invalid_input { + ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { + return core::result::Result::Err({ $crate::error::AnkiError::InvalidInput { + source: snafu::FromString::without_source( + format!($fmt$(, $($arg),*)*), + ) + }}) + }; + ($source:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { + return core::result::Result::Err({ $crate::error::AnkiError::InvalidInput { + source: snafu::FromString::with_source( + core::convert::Into::into($source), + format!($fmt$(, $($arg),*)*), + ) + }}) + }; +} + +/// Returns an [AnkiError::InvalidInput] unless the condition is true. +#[macro_export] +macro_rules! require { + ($condition:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { + if !$condition { + return core::result::Result::Err({ $crate::error::AnkiError::InvalidInput { + source: snafu::FromString::without_source( + format!($fmt$(, $($arg),*)*), + ) + }}); + } + }; +} diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs index 83a462d4f..a155ef83b 100644 --- a/rslib/src/error/mod.rs +++ b/rslib/src/error/mod.rs @@ -2,133 +2,161 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod db; +mod file_io; mod filtered; +mod invalid_input; mod network; +mod not_found; mod search; -use std::{fmt::Display, io, path::Path}; - pub use db::{DbError, DbErrorKind}; pub use filtered::{CustomStudyError, FilteredDeckError}; pub use network::{NetworkError, NetworkErrorKind, SyncError, SyncErrorKind}; pub use search::{ParseError, SearchErrorKind}; -use tempfile::PathPersistError; +use snafu::Snafu; +pub use self::{ + file_io::{FileIoError, FileIoSnafu, FileOp}, + invalid_input::{InvalidInputError, OrInvalid}, + not_found::{NotFoundError, OrNotFound}, +}; use crate::{i18n::I18n, links::HelpPage}; pub type Result = std::result::Result; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub enum AnkiError { - InvalidInput(String), - TemplateError(String), - CardTypeError(CardTypeError), - IoError(String), - FileIoError(FileIoError), - DbError(DbError), - NetworkError(NetworkError), - SyncError(SyncError), - JsonError(String), - ProtoError(String), + #[snafu(context(false))] + InvalidInput { + source: InvalidInputError, + }, + TemplateError { + info: String, + }, + #[snafu(context(false))] + CardTypeError { + source: CardTypeError, + }, + #[snafu(context(false))] + FileIoError { + source: FileIoError, + }, + #[snafu(context(false))] + DbError { + source: DbError, + }, + #[snafu(context(false))] + NetworkError { + source: NetworkError, + }, + #[snafu(context(false))] + SyncError { + source: SyncError, + }, + JsonError { + info: String, + }, + ProtoError { + info: String, + }, ParseNumError, Interrupted, CollectionNotOpen, CollectionAlreadyOpen, - NotFound, + #[snafu(context(false))] + NotFound { + source: NotFoundError, + }, /// Indicates an absent card or note, but (unlike [AnkiError::NotFound]) in /// a non-critical context like the browser table, where deleted ids are /// deliberately not removed. Deleted, Existing, - FilteredDeckError(FilteredDeckError), - SearchError(SearchErrorKind), - InvalidRegex(String), + #[snafu(context(false))] + FilteredDeckError { + source: FilteredDeckError, + }, + #[snafu(context(false))] + SearchError { + source: SearchErrorKind, + }, + InvalidRegex { + info: String, + }, UndoEmpty, MultipleNotetypesSelected, DatabaseCheckRequired, MediaCheckRequired, - CustomStudyError(CustomStudyError), - ImportError(ImportError), + #[snafu(context(false))] + CustomStudyError { + source: CustomStudyError, + }, + #[snafu(context(false))] + ImportError { + source: ImportError, + }, InvalidId, } -impl std::error::Error for AnkiError {} - -impl Display for AnkiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - // error helpers impl AnkiError { - pub(crate) fn invalid_input>(s: S) -> AnkiError { - AnkiError::InvalidInput(s.into()) - } - - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { match self { - AnkiError::SyncError(err) => err.localized_description(tr), - AnkiError::NetworkError(err) => err.localized_description(tr), - AnkiError::TemplateError(info) => { + AnkiError::SyncError { source } => source.message(tr), + AnkiError::NetworkError { source } => source.message(tr), + AnkiError::TemplateError { info: source } => { // already localized - info.into() + source.into() } - AnkiError::CardTypeError(err) => { + AnkiError::CardTypeError { source } => { let header = - tr.card_templates_invalid_template_number(err.ordinal + 1, &err.notetype); - let details = match err.details { - CardTypeErrorDetails::TemplateError | CardTypeErrorDetails::NoSuchField => { - tr.card_templates_see_preview() - } + tr.card_templates_invalid_template_number(source.ordinal + 1, &source.notetype); + let details = match source.source { + CardTypeErrorDetails::TemplateParseError + | CardTypeErrorDetails::NoSuchField => tr.card_templates_see_preview(), CardTypeErrorDetails::NoFrontField => tr.card_templates_no_front_field(), - CardTypeErrorDetails::Duplicate(i) => tr.card_templates_identical_front(i + 1), + CardTypeErrorDetails::Duplicate { index } => { + tr.card_templates_identical_front(index + 1) + } CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(), CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(), }; format!("{}
{}", header, details) } - AnkiError::DbError(err) => err.localized_description(tr), - AnkiError::SearchError(kind) => kind.localized_description(tr), - AnkiError::InvalidInput(info) => { - if info.is_empty() { - tr.errors_invalid_input_empty().into() - } else { - tr.errors_invalid_input_details(info.as_str()).into() - } - } + AnkiError::DbError { source } => source.message(tr), + AnkiError::SearchError { source } => source.message(tr), AnkiError::ParseNumError => tr.errors_parse_number_fail().into(), - AnkiError::FilteredDeckError(err) => err.localized_description(tr), - AnkiError::InvalidRegex(err) => format!("
{}
", err), + AnkiError::FilteredDeckError { source } => source.message(tr), + AnkiError::InvalidRegex { info: source } => format!("
{}
", source), AnkiError::MultipleNotetypesSelected => tr.errors_multiple_notetypes_selected().into(), AnkiError::DatabaseCheckRequired => tr.errors_please_check_database().into(), AnkiError::MediaCheckRequired => tr.errors_please_check_media().into(), - AnkiError::CustomStudyError(err) => err.localized_description(tr), - AnkiError::ImportError(err) => err.localized_description(tr), + AnkiError::CustomStudyError { source } => source.message(tr), + AnkiError::ImportError { source } => source.message(tr), AnkiError::Deleted => tr.browsing_row_deleted().into(), AnkiError::InvalidId => tr.errors_invalid_ids().into(), - AnkiError::IoError(_) - | AnkiError::JsonError(_) - | AnkiError::ProtoError(_) + AnkiError::JsonError { .. } + | AnkiError::ProtoError { .. } | AnkiError::Interrupted | AnkiError::CollectionNotOpen | AnkiError::CollectionAlreadyOpen - | AnkiError::NotFound | AnkiError::Existing | AnkiError::UndoEmpty => format!("{:?}", self), - AnkiError::FileIoError(err) => { - format!("{}: {}", err.path, err.error) - } + AnkiError::FileIoError { source } => source.message(), + AnkiError::InvalidInput { source } => source.message(), + AnkiError::NotFound { source } => source.message(tr), } } pub fn help_page(&self) -> Option { match self { - Self::CardTypeError(CardTypeError { details, .. }) => Some(match details { - CardTypeErrorDetails::TemplateError | CardTypeErrorDetails::NoSuchField => { + Self::CardTypeError { + source: CardTypeError { source, .. }, + } => Some(match source { + CardTypeErrorDetails::TemplateParseError | CardTypeErrorDetails::NoSuchField => { HelpPage::CardTypeTemplateError } - CardTypeErrorDetails::Duplicate(_) => HelpPage::CardTypeDuplicate, + CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate, CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField, CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze, CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze, @@ -136,6 +164,31 @@ impl AnkiError { _ => None, } } + + pub fn context(&self) -> String { + match self { + Self::InvalidInput { source } => source.context(), + Self::NotFound { source } => source.context(), + _ => String::new(), + } + } + + pub fn backtrace(&self) -> String { + match self { + Self::InvalidInput { source } => { + if let Some(bt) = snafu::ErrorCompat::backtrace(source) { + return format!("{bt}"); + } + } + Self::NotFound { source } => { + if let Some(bt) = snafu::ErrorCompat::backtrace(source) { + return format!("{bt}"); + } + } + _ => (), + } + String::new() + } } #[derive(Debug, PartialEq, Eq)] @@ -153,103 +206,100 @@ pub enum TemplateError { NoSuchConditional(String), } -impl From for AnkiError { - fn from(err: io::Error) -> Self { - AnkiError::IoError(format!("{:?}", err)) - } -} - impl From for AnkiError { fn from(err: serde_json::Error) -> Self { - AnkiError::JsonError(err.to_string()) + AnkiError::JsonError { + info: err.to_string(), + } } } impl From for AnkiError { fn from(err: prost::EncodeError) -> Self { - AnkiError::ProtoError(err.to_string()) + AnkiError::ProtoError { + info: err.to_string(), + } } } impl From for AnkiError { fn from(err: prost::DecodeError) -> Self { - AnkiError::ProtoError(err.to_string()) + AnkiError::ProtoError { + info: err.to_string(), + } } } -impl From for AnkiError { - fn from(e: PathPersistError) -> Self { - AnkiError::IoError(e.to_string()) +impl From for AnkiError { + fn from(e: tempfile::PathPersistError) -> Self { + FileIoError::from(e).into() + } +} + +impl From for AnkiError { + fn from(e: tempfile::PersistError) -> Self { + FileIoError::from(e).into() } } impl From for AnkiError { fn from(err: regex::Error) -> Self { - AnkiError::InvalidRegex(err.to_string()) + AnkiError::InvalidRegex { + info: err.to_string(), + } } } -impl From for AnkiError { - fn from(err: csv::Error) -> Self { - AnkiError::InvalidInput(err.to_string()) +// stopgap; implicit mapping should be phased out in favor of manual +// context attachment +impl From for AnkiError { + fn from(source: std::io::Error) -> Self { + FileIoError { + path: std::path::PathBuf::new(), + op: FileOp::Unknown, + source, + } + .into() } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] +#[snafu(visibility(pub))] pub struct CardTypeError { pub notetype: String, pub ordinal: usize, - pub details: CardTypeErrorDetails, + pub source: CardTypeErrorDetails, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] +#[snafu(visibility(pub))] pub enum CardTypeErrorDetails { - TemplateError, - Duplicate(usize), + TemplateParseError, + Duplicate { index: usize }, NoFrontField, NoSuchField, MissingCloze, ExtraneousCloze, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Snafu)] pub enum ImportError { Corrupt, TooNew, - MediaImportFailed(String), + MediaImportFailed { info: String }, NoFieldColumn, } impl ImportError { - fn localized_description(&self, tr: &I18n) -> String { + fn message(&self, tr: &I18n) -> String { match self { ImportError::Corrupt => tr.importing_the_provided_file_is_not_a(), ImportError::TooNew => tr.errors_collection_too_new(), - ImportError::MediaImportFailed(err) => tr.importing_failed_to_import_media_file(err), + ImportError::MediaImportFailed { info } => { + tr.importing_failed_to_import_media_file(info) + } ImportError::NoFieldColumn => tr.importing_file_must_contain_field_column(), } .into() } } - -#[derive(Debug, PartialEq, Eq, Clone)] - -pub struct FileIoError { - pub path: String, - pub error: String, -} - -impl AnkiError { - pub(crate) fn file_io_error>(err: std::io::Error, path: P) -> Self { - AnkiError::FileIoError(FileIoError::new(err, path.as_ref())) - } -} - -impl FileIoError { - pub fn new(err: std::io::Error, path: &Path) -> FileIoError { - FileIoError { - path: path.to_string_lossy().to_string(), - error: err.to_string(), - } - } -} diff --git a/rslib/src/error/network.rs b/rslib/src/error/network.rs index e856a1003..0b418e1bc 100644 --- a/rslib/src/error/network.rs +++ b/rslib/src/error/network.rs @@ -3,10 +3,11 @@ use anki_i18n::I18n; use reqwest::StatusCode; +use snafu::Snafu; use super::AnkiError; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub struct NetworkError { pub info: String, pub kind: NetworkErrorKind, @@ -20,7 +21,7 @@ pub enum NetworkErrorKind { Other, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub struct SyncError { pub info: String, pub kind: SyncErrorKind, @@ -43,10 +44,12 @@ pub enum SyncErrorKind { impl AnkiError { pub(crate) fn sync_error(info: impl Into, kind: SyncErrorKind) -> Self { - AnkiError::SyncError(SyncError { - info: info.into(), - kind, - }) + AnkiError::SyncError { + source: SyncError { + info: info.into(), + kind, + }, + } } pub(crate) fn server_message>(msg: S) -> AnkiError { @@ -62,10 +65,12 @@ impl From for AnkiError { let info = str_err.replace(url, ""); if err.is_timeout() { - AnkiError::NetworkError(NetworkError { - info, - kind: NetworkErrorKind::Timeout, - }) + AnkiError::NetworkError { + source: NetworkError { + info, + kind: NetworkErrorKind::Timeout, + }, + } } else if err.is_status() { error_for_status_code(info, err.status().unwrap()) } else { @@ -77,36 +82,50 @@ impl From for AnkiError { fn error_for_status_code(info: String, code: StatusCode) -> AnkiError { use reqwest::StatusCode as S; match code { - S::PROXY_AUTHENTICATION_REQUIRED => AnkiError::NetworkError(NetworkError { - info, - kind: NetworkErrorKind::ProxyAuth, - }), - S::CONFLICT => AnkiError::SyncError(SyncError { - info, - kind: SyncErrorKind::Conflict, - }), - S::FORBIDDEN => AnkiError::SyncError(SyncError { - info, - kind: SyncErrorKind::AuthFailed, - }), - S::NOT_IMPLEMENTED => AnkiError::SyncError(SyncError { - info, - kind: SyncErrorKind::ClientTooOld, - }), - S::INTERNAL_SERVER_ERROR | S::BAD_GATEWAY | S::GATEWAY_TIMEOUT | S::SERVICE_UNAVAILABLE => { - AnkiError::SyncError(SyncError { + S::PROXY_AUTHENTICATION_REQUIRED => AnkiError::NetworkError { + source: NetworkError { info, - kind: SyncErrorKind::ServerError, - }) + kind: NetworkErrorKind::ProxyAuth, + }, + }, + S::CONFLICT => AnkiError::SyncError { + source: SyncError { + info, + kind: SyncErrorKind::Conflict, + }, + }, + S::FORBIDDEN => AnkiError::SyncError { + source: SyncError { + info, + kind: SyncErrorKind::AuthFailed, + }, + }, + S::NOT_IMPLEMENTED => AnkiError::SyncError { + source: SyncError { + info, + kind: SyncErrorKind::ClientTooOld, + }, + }, + S::INTERNAL_SERVER_ERROR | S::BAD_GATEWAY | S::GATEWAY_TIMEOUT | S::SERVICE_UNAVAILABLE => { + AnkiError::SyncError { + source: SyncError { + info, + kind: SyncErrorKind::ServerError, + }, + } } - S::BAD_REQUEST => AnkiError::SyncError(SyncError { - info, - kind: SyncErrorKind::DatabaseCheckRequired, - }), - _ => AnkiError::NetworkError(NetworkError { - info, - kind: NetworkErrorKind::Other, - }), + S::BAD_REQUEST => AnkiError::SyncError { + source: SyncError { + info, + kind: SyncErrorKind::DatabaseCheckRequired, + }, + }, + _ => AnkiError::NetworkError { + source: NetworkError { + info, + kind: NetworkErrorKind::Other, + }, + }, } } @@ -131,7 +150,9 @@ fn guess_reqwest_error(mut info: String) -> AnkiError { NetworkErrorKind::Other }; - AnkiError::NetworkError(NetworkError { info, kind }) + AnkiError::NetworkError { + source: NetworkError { info, kind }, + } } impl From for AnkiError { @@ -141,7 +162,7 @@ impl From for AnkiError { } impl SyncError { - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { match self.kind { SyncErrorKind::ServerMessage => self.info.clone().into(), SyncErrorKind::Other => self.info.clone().into(), @@ -160,7 +181,7 @@ impl SyncError { } impl NetworkError { - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { let summary = match self.kind { NetworkErrorKind::Offline => tr.network_offline(), NetworkErrorKind::Timeout => tr.network_timeout(), diff --git a/rslib/src/error/not_found.rs b/rslib/src/error/not_found.rs new file mode 100644 index 000000000..6fd4353ef --- /dev/null +++ b/rslib/src/error/not_found.rs @@ -0,0 +1,75 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::{any, fmt}; + +use convert_case::{Case, Casing}; +use snafu::{Backtrace, OptionExt, Snafu}; + +use crate::prelude::*; + +/// Something was unexpectedly missing from the database. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub struct NotFoundError { + pub type_name: String, + pub identifier: String, + pub backtrace: Option, +} + +impl NotFoundError { + pub fn message(&self, tr: &I18n) -> String { + tr.errors_inconsistent_db_state().into() + } + + pub fn context(&self) -> String { + format!("No such {}: '{}'", self.type_name, self.identifier) + } +} + +impl PartialEq for NotFoundError { + fn eq(&self, other: &Self) -> bool { + self.type_name == other.type_name && self.identifier == other.identifier + } +} + +impl Eq for NotFoundError {} + +/// Allows generating [AnkiError::NotFound] from [Option::None]. +pub trait OrNotFound { + type Value; + fn or_not_found(self, identifier: impl fmt::Display) -> Result; +} + +impl OrNotFound for Option { + type Value = T; + + fn or_not_found(self, identifier: impl fmt::Display) -> Result { + self.with_context(|| NotFoundSnafu { + type_name: unqualified_lowercase_type_name::(), + identifier: format!("{identifier}"), + }) + .map_err(Into::into) + } +} + +fn unqualified_lowercase_type_name() -> String { + any::type_name::() + .split("::") + .last() + .unwrap_or_default() + .to_case(Case::Lower) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_unqualified_lowercase_type_name() { + assert_eq!( + unqualified_lowercase_type_name::(), + "card id" + ); + } +} diff --git a/rslib/src/error/search.rs b/rslib/src/error/search.rs index 4c623cab9..a05a1ce08 100644 --- a/rslib/src/error/search.rs +++ b/rslib/src/error/search.rs @@ -5,6 +5,7 @@ use std::num::ParseIntError; use anki_i18n::I18n; use nom::error::{ErrorKind as NomErrorKind, ParseError as NomParseError}; +use snafu::Snafu; use super::AnkiError; @@ -14,7 +15,7 @@ pub enum ParseError<'a> { Nom(&'a str, NomErrorKind), } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Snafu)] pub enum SearchErrorKind { MisplacedAnd, MisplacedOr, @@ -24,24 +25,26 @@ pub enum SearchErrorKind { EmptyQuote, UnclosedQuote, MissingKey, - UnknownEscape(String), - InvalidState(String), + UnknownEscape { provided: String }, + InvalidState { provided: String }, InvalidFlag, - InvalidPropProperty(String), - InvalidPropOperator(String), + InvalidPropProperty { provided: String }, + InvalidPropOperator { provided: String }, InvalidNumber { provided: String, context: String }, InvalidWholeNumber { provided: String, context: String }, InvalidPositiveWholeNumber { provided: String, context: String }, InvalidNegativeWholeNumber { provided: String, context: String }, InvalidAnswerButton { provided: String, context: String }, - Other(Option), + Other { info: Option }, } impl From> for AnkiError { fn from(err: ParseError) -> Self { match err { - ParseError::Anki(_, kind) => AnkiError::SearchError(kind), - ParseError::Nom(_, _) => AnkiError::SearchError(SearchErrorKind::Other(None)), + ParseError::Anki(_, kind) => AnkiError::SearchError { source: kind }, + ParseError::Nom(_, _) => AnkiError::SearchError { + source: SearchErrorKind::Other { info: None }, + }, } } } @@ -51,7 +54,9 @@ impl From>> for AnkiError { match err { nom::Err::Error(e) => e.into(), nom::Err::Failure(e) => e.into(), - nom::Err::Incomplete(_) => AnkiError::SearchError(SearchErrorKind::Other(None)), + nom::Err::Incomplete(_) => AnkiError::SearchError { + source: SearchErrorKind::Other { info: None }, + }, } } } @@ -73,7 +78,7 @@ impl From for AnkiError { } impl SearchErrorKind { - pub fn localized_description(&self, tr: &I18n) -> String { + pub fn message(&self, tr: &I18n) -> String { let reason = match self { SearchErrorKind::MisplacedAnd => tr.search_misplaced_and(), SearchErrorKind::MisplacedOr => tr.search_misplaced_or(), @@ -83,20 +88,22 @@ impl SearchErrorKind { SearchErrorKind::EmptyQuote => tr.search_empty_quote(), SearchErrorKind::UnclosedQuote => tr.search_unclosed_quote(), SearchErrorKind::MissingKey => tr.search_missing_key(), - SearchErrorKind::UnknownEscape(ctx) => tr.search_unknown_escape(ctx.replace('`', "'")), - SearchErrorKind::InvalidState(state) => { - tr.search_invalid_argument("is:", state.replace('`', "'")) + SearchErrorKind::UnknownEscape { provided } => { + tr.search_unknown_escape(provided.replace('`', "'")) + } + SearchErrorKind::InvalidState { provided } => { + tr.search_invalid_argument("is:", provided.replace('`', "'")) } SearchErrorKind::InvalidFlag => tr.search_invalid_flag_2(), - SearchErrorKind::InvalidPropProperty(prop) => { - tr.search_invalid_argument("prop:", prop.replace('`', "'")) + SearchErrorKind::InvalidPropProperty { provided } => { + tr.search_invalid_argument("prop:", provided.replace('`', "'")) } - SearchErrorKind::InvalidPropOperator(ctx) => { - tr.search_invalid_prop_operator(ctx.as_str()) + SearchErrorKind::InvalidPropOperator { provided } => { + tr.search_invalid_prop_operator(provided.as_str()) } - SearchErrorKind::Other(Some(info)) => info.into(), - SearchErrorKind::Other(None) => tr.search_invalid_other(), + SearchErrorKind::Other { info: Some(info) } => info.into(), + SearchErrorKind::Other { info: None } => tr.search_invalid_other(), SearchErrorKind::InvalidNumber { provided, context } => { tr.search_invalid_number(context.replace('`', "'"), provided.replace('`', "'")) } diff --git a/rslib/src/import_export/gather.rs b/rslib/src/import_export/gather.rs index 1662f3c4a..028a142a7 100644 --- a/rslib/src/import_export/gather.rs +++ b/rslib/src/import_export/gather.rs @@ -258,7 +258,7 @@ impl Collection { .map(|config_id| { self.storage .get_deck_config(config_id)? - .ok_or(AnkiError::NotFound) + .or_not_found(config_id) }) .collect() } diff --git a/rslib/src/import_export/package/apkg/export.rs b/rslib/src/import_export/package/apkg/export.rs index 5cca25a28..aad5fd5fa 100644 --- a/rslib/src/import_export/package/apkg/export.rs +++ b/rslib/src/import_export/package/apkg/export.rs @@ -6,8 +6,6 @@ use std::{ path::{Path, PathBuf}, }; -use tempfile::NamedTempFile; - use crate::{ collection::CollectionBuilder, import_export::{ @@ -18,7 +16,7 @@ use crate::{ }, ExportProgress, IncrementableProgress, }, - io::{atomic_rename, tempfile_in_parent_of}, + io::{atomic_rename, new_tempfile, new_tempfile_in_parent_of}, prelude::*, }; @@ -37,12 +35,12 @@ impl Collection { ) -> Result { let mut progress = IncrementableProgress::new(progress_fn); progress.call(ExportProgress::File)?; - let temp_apkg = tempfile_in_parent_of(out_path.as_ref())?; - let mut temp_col = NamedTempFile::new()?; + let temp_apkg = new_tempfile_in_parent_of(out_path.as_ref())?; + let mut temp_col = new_tempfile()?; let temp_col_path = temp_col .path() .to_str() - .ok_or_else(|| AnkiError::IoError("tempfile with non-unicode name".into()))?; + .or_invalid("non-unicode filename")?; let meta = if legacy { Meta::new_legacy() } else { diff --git a/rslib/src/import_export/package/apkg/import/decks.rs b/rslib/src/import_export/package/apkg/import/decks.rs index 4ec1966ea..154b39d83 100644 --- a/rslib/src/import_export/package/apkg/import/decks.rs +++ b/rslib/src/import_export/package/apkg/import/decks.rs @@ -143,7 +143,7 @@ impl DeckContext<'_> { } else if let (Ok(new), Ok(old)) = (new_deck.filtered_mut(), deck.filtered()) { *new = old.clone(); } else { - return Err(AnkiError::invalid_input("decks have different kinds")); + invalid_input!("decks have different kinds"); } self.imported_decks.insert(deck.id, new_deck.id); self.target_col diff --git a/rslib/src/import_export/package/apkg/import/media.rs b/rslib/src/import_export/package/apkg/import/media.rs index b1350c5f8..06d18bd3f 100644 --- a/rslib/src/import_export/package/apkg/import/media.rs +++ b/rslib/src/import_export/package/apkg/import/media.rs @@ -7,6 +7,7 @@ use zip::ZipArchive; use super::Context; use crate::{ + error::{FileIoSnafu, FileOp}, import_export::{ package::{ colpkg::export::MediaCopier, @@ -122,7 +123,10 @@ impl SafeMediaEntry { fn ensure_sha1_set(&mut self, archive: &mut ZipArchive) -> Result<()> { if self.sha1.is_none() { let mut reader = self.fetch_file(archive)?; - self.sha1 = Some(sha1_of_reader(&mut reader)?); + self.sha1 = Some(sha1_of_reader(&mut reader).context(FileIoSnafu { + path: &self.name, + op: FileOp::Read, + })?); } Ok(()) } diff --git a/rslib/src/import_export/package/apkg/import/mod.rs b/rslib/src/import_export/package/apkg/import/mod.rs index ab87d5a15..192bb5f1d 100644 --- a/rslib/src/import_export/package/apkg/import/mod.rs +++ b/rslib/src/import_export/package/apkg/import/mod.rs @@ -6,19 +6,20 @@ mod decks; mod media; mod notes; -use std::{collections::HashSet, fs::File, io, path::Path}; +use std::{collections::HashSet, fs::File, path::Path}; pub(crate) use notes::NoteMeta; use rusqlite::OptionalExtension; use tempfile::NamedTempFile; use zip::ZipArchive; -use zstd::stream::copy_decode; use crate::{ collection::CollectionBuilder, + error::{FileIoSnafu, FileOp}, import_export::{ gather::ExchangeData, package::Meta, ImportProgress, IncrementableProgress, NoteLog, }, + io::{new_tempfile, open_file}, media::MediaManager, prelude::*, search::SearchNode, @@ -40,7 +41,7 @@ impl Collection { path: impl AsRef, progress_fn: impl 'static + FnMut(ImportProgress, bool) -> bool, ) -> Result> { - let file = File::open(path)?; + let file = open_file(path)?; let archive = ZipArchive::new(file)?; self.transact(Op::Import, |col| { @@ -134,13 +135,12 @@ impl ExchangeData { fn collection_to_tempfile(meta: &Meta, archive: &mut ZipArchive) -> Result { let mut zip_file = archive.by_name(meta.collection_filename())?; - let mut tempfile = NamedTempFile::new()?; - if meta.zstd_compressed() { - copy_decode(zip_file, &mut tempfile) - } else { - io::copy(&mut zip_file, &mut tempfile).map(|_| ()) - } - .map_err(|err| AnkiError::file_io_error(err, tempfile.path()))?; + let mut tempfile = new_tempfile()?; + meta.copy(&mut zip_file, &mut tempfile) + .with_context(|_| FileIoSnafu { + path: tempfile.path(), + op: FileOp::copy(zip_file.name()), + })?; Ok(tempfile) } diff --git a/rslib/src/import_export/package/apkg/import/notes.rs b/rslib/src/import_export/package/apkg/import/notes.rs index eb5410eb7..987ceda39 100644 --- a/rslib/src/import_export/package/apkg/import/notes.rs +++ b/rslib/src/import_export/package/apkg/import/notes.rs @@ -210,16 +210,11 @@ impl<'n> NoteContext<'n> { } fn get_expected_notetype(&mut self, ntid: NotetypeId) -> Result> { - self.target_col - .get_notetype(ntid)? - .ok_or(AnkiError::NotFound) + self.target_col.get_notetype(ntid)?.or_not_found(ntid) } fn get_expected_note(&mut self, nid: NoteId) -> Result { - self.target_col - .storage - .get_note(nid)? - .ok_or(AnkiError::NotFound) + self.target_col.storage.get_note(nid)?.or_not_found(nid) } fn maybe_update_note(&mut self, note: Note, meta: NoteMeta) -> Result<()> { diff --git a/rslib/src/import_export/package/colpkg/export.rs b/rslib/src/import_export/package/colpkg/export.rs index 79f383f5e..eaeef76b9 100644 --- a/rslib/src/import_export/package/colpkg/export.rs +++ b/rslib/src/import_export/package/colpkg/export.rs @@ -23,7 +23,7 @@ use super::super::{MediaEntries, MediaEntry, Meta, Version}; use crate::{ collection::CollectionBuilder, import_export::{ExportProgress, IncrementableProgress}, - io::{atomic_rename, read_dir_files, tempfile_in_parent_of}, + io::{atomic_rename, new_tempfile, new_tempfile_in_parent_of, open_file, read_dir_files}, media::files::filename_if_normalized, prelude::*, storage::SchemaVersion, @@ -45,7 +45,7 @@ impl Collection { let mut progress = IncrementableProgress::new(progress_fn); progress.call(ExportProgress::File)?; let colpkg_name = out_path.as_ref(); - let temp_colpkg = tempfile_in_parent_of(colpkg_name)?; + let temp_colpkg = new_tempfile_in_parent_of(colpkg_name)?; let src_path = self.col_path.clone(); let src_media_folder = if include_media { Some(self.media_folder.clone()) @@ -67,7 +67,9 @@ impl Collection { &tr, &mut progress, )?; - atomic_rename(temp_colpkg, colpkg_name, true) + atomic_rename(temp_colpkg, colpkg_name, true)?; + + Ok(()) } } @@ -113,7 +115,7 @@ fn export_collection_file( } else { Meta::new() }; - let mut col_file = File::open(col_path)?; + let mut col_file = open_file(col_path)?; let col_size = col_file.metadata()?.len() as usize; let media = if let Some(path) = media_dir { MediaIter::from_folder(&path)? @@ -199,7 +201,7 @@ fn write_dummy_collection(zip: &mut ZipWriter, tr: &I18n) -> Result<()> { } fn create_dummy_collection_file(tr: &I18n) -> Result { - let tempfile = NamedTempFile::new()?; + let tempfile = new_tempfile()?; let mut dummy_col = CollectionBuilder::new(tempfile.path()).build()?; dummy_col.add_dummy_note(tr)?; dummy_col @@ -290,10 +292,8 @@ fn write_media_files( zip.start_file(index.to_string(), file_options_stored())?; - let mut file = File::open(&path)?; - let file_name = path - .file_name() - .ok_or_else(|| AnkiError::invalid_input("not a file path"))?; + let mut file = open_file(&path)?; + let file_name = path.file_name().or_invalid("not a file path")?; let name = normalized_unicode_file_name(file_name)?; let (size, sha1) = copier.copy(&mut file, zip)?; @@ -304,12 +304,7 @@ fn write_media_files( } fn normalized_unicode_file_name(filename: &OsStr) -> Result { - let filename = filename.to_str().ok_or_else(|| { - AnkiError::IoError(format!( - "non-unicode file name: {}", - filename.to_string_lossy() - )) - })?; + let filename = filename.to_str().or_invalid("non-unicode filename")?; filename_if_normalized(filename) .map(Cow::into_owned) .ok_or(AnkiError::MediaCheckRequired) diff --git a/rslib/src/import_export/package/colpkg/import.rs b/rslib/src/import_export/package/colpkg/import.rs index 3e3c16439..e907374d3 100644 --- a/rslib/src/import_export/package/colpkg/import.rs +++ b/rslib/src/import_export/package/colpkg/import.rs @@ -12,7 +12,7 @@ use zstd::{self, stream::copy_decode}; use crate::{ collection::CollectionBuilder, - error::ImportError, + error::{FileIoSnafu, FileOp, ImportError}, import_export::{ package::{ media::{extract_media_entries, SafeMediaEntry}, @@ -20,7 +20,7 @@ use crate::{ }, ImportProgress, IncrementableProgress, }, - io::{atomic_rename, tempfile_in_parent_of}, + io::{atomic_rename, create_dir_all, new_tempfile_in_parent_of, open_file}, media::MediaManager, prelude::*, }; @@ -36,9 +36,9 @@ pub fn import_colpkg( let mut progress = IncrementableProgress::new(progress_fn); progress.call(ImportProgress::File)?; let col_path = PathBuf::from(target_col_path); - let mut tempfile = tempfile_in_parent_of(&col_path)?; + let mut tempfile = new_tempfile_in_parent_of(&col_path)?; - let backup_file = File::open(colpkg_path)?; + let backup_file = open_file(colpkg_path)?; let mut archive = ZipArchive::new(backup_file)?; let meta = Meta::from_archive(&mut archive)?; @@ -56,7 +56,9 @@ pub fn import_colpkg( log, )?; - atomic_rename(tempfile, &col_path, true) + atomic_rename(tempfile, &col_path, true)?; + + Ok(()) } fn check_collection_and_mod_schema(col_path: &Path) -> Result<()> { @@ -72,7 +74,9 @@ fn check_collection_and_mod_schema(col_path: &Path) -> Result<()> { .ok() }) .and_then(|s| (s == "ok").then_some(())) - .ok_or(AnkiError::ImportError(ImportError::Corrupt)) + .ok_or(AnkiError::ImportError { + source: ImportError::Corrupt, + }) } fn restore_media( @@ -88,7 +92,7 @@ fn restore_media( return Ok(()); } - std::fs::create_dir_all(media_folder)?; + create_dir_all(media_folder)?; let media_manager = MediaManager::new(media_folder, media_db)?; let mut media_comparer = MediaComparer::new(meta, progress, &media_manager, log)?; @@ -123,16 +127,14 @@ fn maybe_restore_media_file( } fn restore_media_file(meta: &Meta, zip_file: &mut ZipFile, path: &Path) -> Result<()> { - let mut tempfile = tempfile_in_parent_of(path)?; - - if meta.zstd_compressed() { - copy_decode(zip_file, &mut tempfile) - } else { - io::copy(zip_file, &mut tempfile).map(|_| ()) - } - .map_err(|err| AnkiError::file_io_error(err, path))?; - - atomic_rename(tempfile, path, false) + let mut tempfile = new_tempfile_in_parent_of(path)?; + meta.copy(zip_file, &mut tempfile) + .with_context(|_| FileIoSnafu { + path: tempfile.path(), + op: FileOp::copy(zip_file.name()), + })?; + atomic_rename(tempfile, path, false)?; + Ok(()) } fn copy_collection( @@ -140,9 +142,12 @@ fn copy_collection( writer: &mut impl Write, meta: &Meta, ) -> Result<()> { - let mut file = archive - .by_name(meta.collection_filename()) - .map_err(|_| AnkiError::ImportError(ImportError::Corrupt))?; + let mut file = + archive + .by_name(meta.collection_filename()) + .map_err(|_| AnkiError::ImportError { + source: ImportError::Corrupt, + })?; if !meta.zstd_compressed() { io::copy(&mut file, writer)?; } else { diff --git a/rslib/src/import_export/package/colpkg/tests.rs b/rslib/src/import_export/package/colpkg/tests.rs index b07ef084c..32e643654 100644 --- a/rslib/src/import_export/package/colpkg/tests.rs +++ b/rslib/src/import_export/package/colpkg/tests.rs @@ -8,14 +8,18 @@ use std::path::Path; use tempfile::tempdir; use crate::{ - collection::CollectionBuilder, import_export::package::import_colpkg, log::terminal, - media::MediaManager, prelude::*, + collection::CollectionBuilder, + import_export::package::import_colpkg, + io::{create_dir, create_dir_all, read_file}, + log::terminal, + media::MediaManager, + prelude::*, }; fn collection_with_media(dir: &Path, name: &str) -> Result { let name = format!("{name}_src"); let media_folder = dir.join(format!("{name}.media")); - std::fs::create_dir(&media_folder)?; + create_dir(&media_folder)?; // add collection with sentinel note let mut col = CollectionBuilder::new(dir.join(format!("{name}.anki2"))) .set_media_paths(media_folder, dir.join(format!("{name}.mdb"))) @@ -49,7 +53,7 @@ fn roundtrip() -> Result<()> { .to_string_lossy() .into_owned(); let import_media_dir = dir.join(format!("{name}.media")); - std::fs::create_dir_all(&import_media_dir)?; + create_dir_all(&import_media_dir)?; let import_media_db = dir.join(format!("{name}.mdb")); MediaManager::new(&import_media_dir, &import_media_db)?; import_colpkg( @@ -68,9 +72,9 @@ fn roundtrip() -> Result<()> { 1 ); // confirm media imported correctly - assert_eq!(std::fs::read(import_media_dir.join("1"))?, b"1"); - assert_eq!(std::fs::read(import_media_dir.join("2"))?, b"2"); - assert_eq!(std::fs::read(import_media_dir.join("3"))?, b"3"); + assert_eq!(read_file(import_media_dir.join("1"))?, b"1"); + assert_eq!(read_file(import_media_dir.join("2"))?, b"2"); + assert_eq!(read_file(import_media_dir.join("3"))?, b"3"); } Ok(()) @@ -81,13 +85,15 @@ fn roundtrip() -> Result<()> { #[test] #[cfg(not(target_vendor = "apple"))] fn normalization_check_on_export() -> Result<()> { + use crate::io::write_file; + let _dir = tempdir()?; let dir = _dir.path(); let col = collection_with_media(dir, "normalize")?; let colpkg_name = dir.join("normalize.colpkg"); // manually write a file in the wrong encoding. - std::fs::write(col.media_folder.join("ぱぱ.jpg"), "nfd encoding")?; + write_file(col.media_folder.join("ぱぱ.jpg"), "nfd encoding")?; assert_eq!( col.export_colpkg(&colpkg_name, true, false, |_, _| true,) .unwrap_err(), diff --git a/rslib/src/import_export/package/media.rs b/rslib/src/import_export/package/media.rs index 0b66e24dd..742a1b5bf 100644 --- a/rslib/src/import_export/package/media.rs +++ b/rslib/src/import_export/package/media.rs @@ -10,14 +10,13 @@ use std::{ }; use prost::Message; -use tempfile::NamedTempFile; use zip::{read::ZipFile, ZipArchive}; use zstd::stream::copy_decode; use super::{colpkg::export::MediaCopier, MediaEntries, MediaEntry, Meta}; use crate::{ error::ImportError, - io::{atomic_rename, filename_is_safe}, + io::{atomic_rename, filename_is_safe, new_tempfile_in}, media::files::normalize_filename, prelude::*, }; @@ -58,7 +57,9 @@ impl SafeMediaEntry { }); } } - Err(AnkiError::ImportError(ImportError::Corrupt)) + Err(AnkiError::ImportError { + source: ImportError::Corrupt, + }) } pub(super) fn from_legacy(legacy_entry: (&str, String)) -> Result { @@ -80,9 +81,10 @@ impl SafeMediaEntry { } pub(super) fn fetch_file<'a>(&self, archive: &'a mut ZipArchive) -> Result> { - archive - .by_name(&self.index.to_string()) - .map_err(|_| AnkiError::invalid_input(&format!("{} missing from archive", self.index))) + match archive.by_name(&self.index.to_string()) { + Ok(file) => Ok(file), + Err(err) => invalid_input!(err, "{} missing from archive", self.index), + } } pub(super) fn has_checksum_equal_to( @@ -105,14 +107,16 @@ impl SafeMediaEntry { copier: &mut MediaCopier, ) -> Result<()> { let mut file = self.fetch_file(archive)?; - let mut tempfile = NamedTempFile::new_in(target_folder)?; + let mut tempfile = new_tempfile_in(target_folder)?; if self.sha1.is_none() { let (_, sha1) = copier.copy(&mut file, &mut tempfile)?; self.sha1 = Some(sha1); } else { io::copy(&mut file, &mut tempfile)?; } - atomic_rename(tempfile, &self.file_path(target_folder), false) + atomic_rename(tempfile, &self.file_path(target_folder), false)?; + + Ok(()) } } @@ -131,7 +135,9 @@ pub(super) fn extract_media_entries( pub(super) fn safe_normalized_file_name(name: &str) -> Result> { if !filename_is_safe(name) { - Err(AnkiError::ImportError(ImportError::Corrupt)) + Err(AnkiError::ImportError { + source: ImportError::Corrupt, + }) } else { Ok(normalize_filename(name)) } diff --git a/rslib/src/import_export/package/meta.rs b/rslib/src/import_export/package/meta.rs index 0746fa389..10ac98752 100644 --- a/rslib/src/import_export/package/meta.rs +++ b/rslib/src/import_export/package/meta.rs @@ -1,10 +1,14 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::{fs::File, io::Read}; +use std::{ + fs::File, + io::{self, Read}, +}; use prost::Message; use zip::ZipArchive; +use zstd::stream::copy_decode; pub(super) use crate::pb::{package_metadata::Version, PackageMetadata as Meta}; use crate::{error::ImportError, prelude::*, storage::SchemaVersion}; @@ -53,7 +57,9 @@ impl Meta { let meta = if let Some(bytes) = meta_bytes { let meta: Meta = Message::decode(&*bytes)?; if meta.version() == Version::Unknown { - return Err(AnkiError::ImportError(ImportError::TooNew)); + return Err(AnkiError::ImportError { + source: ImportError::TooNew, + }); } meta } else { @@ -89,4 +95,16 @@ impl Meta { fn is_legacy(&self) -> bool { matches!(self.version(), Version::Legacy1 | Version::Legacy2) } + + pub(super) fn copy( + &self, + reader: &mut impl io::Read, + writer: &mut impl io::Write, + ) -> io::Result<()> { + if self.zstd_compressed() { + copy_decode(reader, writer) + } else { + io::copy(reader, writer).map(|_| ()) + } + } } diff --git a/rslib/src/import_export/text/csv/export.rs b/rslib/src/import_export/text/csv/export.rs index f4811f024..8a7d09e6d 100644 --- a/rslib/src/import_export/text/csv/export.rs +++ b/rslib/src/import_export/text/csv/export.rs @@ -37,7 +37,9 @@ impl Collection { cards.sort_unstable(); for &card in &cards { incrementor.increment()?; - writer.write_record(self.card_record(card, with_html)?)?; + writer + .write_record(self.card_record(card, with_html)?) + .or_invalid("invalid csv")?; } writer.flush()?; @@ -58,7 +60,9 @@ impl Collection { let mut writer = note_file_writer_with_header(&request.out_path, &ctx)?; guard.col.storage.for_each_note_in_search(|note| { incrementor.increment()?; - writer.write_record(ctx.record(¬e))?; + writer + .write_record(ctx.record(¬e)) + .or_invalid("invalid csv")?; Ok(()) })?; writer.flush()?; diff --git a/rslib/src/import_export/text/csv/import.rs b/rslib/src/import_export/text/csv/import.rs index f3148a67e..a95b84494 100644 --- a/rslib/src/import_export/text/csv/import.rs +++ b/rslib/src/import_export/text/csv/import.rs @@ -1,10 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::{ - fs::File, - io::{BufRead, BufReader, Read, Seek, SeekFrom}, -}; +use std::io::{BufRead, BufReader, Read, Seek, SeekFrom}; use crate::{ import_export::{ @@ -14,6 +11,7 @@ use crate::{ }, ImportProgress, NoteLog, }, + io::open_file, prelude::*, }; @@ -24,7 +22,7 @@ impl Collection { metadata: CsvMetadata, progress_fn: impl 'static + FnMut(ImportProgress, bool) -> bool, ) -> Result> { - let file = File::open(path)?; + let file = open_file(path)?; let default_deck = metadata.deck()?.name_or_id(); let default_notetype = metadata.notetype()?.name_or_id(); let mut ctx = ColumnContext::new(&metadata)?; @@ -45,15 +43,11 @@ impl Collection { impl CsvMetadata { fn deck(&self) -> Result<&CsvDeck> { - self.deck - .as_ref() - .ok_or_else(|| AnkiError::invalid_input("deck oneof not set")) + self.deck.as_ref().or_invalid("deck oneof not set") } fn notetype(&self) -> Result<&CsvNotetype> { - self.notetype - .as_ref() - .ok_or_else(|| AnkiError::invalid_input("notetype oneof not set")) + self.notetype.as_ref().or_invalid("notetype oneof not set") } fn field_source_columns(&self) -> Result { @@ -150,7 +144,7 @@ impl ColumnContext { .records() .into_iter() .map(|res| { - res.map_err(Into::into) + res.or_invalid("invalid csv") .map(|record| self.foreign_note_from_record(&record)) }) .collect() diff --git a/rslib/src/import_export/text/csv/metadata.rs b/rslib/src/import_export/text/csv/metadata.rs index 233abf030..6e7b2e39e 100644 --- a/rslib/src/import_export/text/csv/metadata.rs +++ b/rslib/src/import_export/text/csv/metadata.rs @@ -3,7 +3,6 @@ use std::{ collections::{HashMap, HashSet}, - fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, }; @@ -21,6 +20,7 @@ use crate::{ config::I32ConfigKey, error::ImportError, import_export::text::NameOrId, + io::open_file, notetype::NoteField, pb::StringList, prelude::*, @@ -40,7 +40,7 @@ impl Collection { notetype_id: Option, is_html: Option, ) -> Result { - let mut reader = File::open(path)?; + let mut reader = open_file(path)?; self.get_reader_metadata(&mut reader, delimiter, notetype_id, is_html) } @@ -206,7 +206,7 @@ impl Collection { if let Some(CsvNotetype::GlobalNotetype(ref mut global)) = metadata.notetype { let notetype = self .get_notetype(NotetypeId(global.id))? - .ok_or(AnkiError::NotFound)?; + .or_not_found(NotetypeId(global.id))?; global.field_columns = vec![0; notetype.fields.len()]; global.field_columns[0] = 1; let column_len = metadata.column_labels.len(); @@ -233,7 +233,7 @@ impl Collection { self.storage .get_all_notetype_names()? .first() - .ok_or(AnkiError::NotFound)? + .or_invalid("collection has no notetypes")? .0 }) } @@ -256,7 +256,7 @@ fn collect_preview_records( .into_iter() .take(PREVIEW_LENGTH) .collect::>() - .map_err(Into::into) + .or_invalid("invalid csv") } fn set_preview(metadata: &mut CsvMetadata, records: &[csv::StringRecord]) -> Result<()> { @@ -346,8 +346,9 @@ fn ensure_first_field_is_mapped( if field_columns[0] == 0 { field_columns[0] = (1..column_len + 1) .find(|i| !meta_columns.contains(i)) - .ok_or(AnkiError::ImportError(ImportError::NoFieldColumn))? - as u32; + .ok_or(AnkiError::ImportError { + source: ImportError::NoFieldColumn, + })? as u32; } Ok(()) } @@ -430,7 +431,9 @@ fn map_single_record( .delimiter(delimiter.byte()) .from_reader(line.as_bytes()) .headers() - .map_err(|_| AnkiError::ImportError(ImportError::Corrupt)) + .map_err(|_| AnkiError::ImportError { + source: ImportError::Corrupt, + }) .map(op) } diff --git a/rslib/src/import_export/text/import.rs b/rslib/src/import_export/text/import.rs index c29e76231..c5bc28c67 100644 --- a/rslib/src/import_export/text/import.rs +++ b/rslib/src/import_export/text/import.rs @@ -276,7 +276,7 @@ impl<'a> Context<'a> { self.col .storage .get_note(nid)? - .ok_or(AnkiError::NotFound) + .or_not_found(nid) .map(|dupe| Duplicate::new(dupe, original, false)) } @@ -446,9 +446,7 @@ impl Collection { } fn get_full_duplicates(&self, note: &ForeignNote, dupe_ids: &[NoteId]) -> Result> { - let first_field = note - .first_field_stripped() - .ok_or_else(|| AnkiError::invalid_input("no first field"))?; + let first_field = note.first_field_stripped().or_invalid("no first field")?; dupe_ids .iter() .filter_map(|&dupe_id| self.storage.get_note(dupe_id).transpose()) diff --git a/rslib/src/import_export/text/json.rs b/rslib/src/import_export/text/json.rs index f24781e81..e8fc9004d 100644 --- a/rslib/src/import_export/text/json.rs +++ b/rslib/src/import_export/text/json.rs @@ -3,6 +3,7 @@ use crate::{ import_export::{text::ForeignData, ImportProgress, NoteLog}, + io::read_file, prelude::*, }; @@ -13,7 +14,7 @@ impl Collection { mut progress_fn: impl 'static + FnMut(ImportProgress, bool) -> bool, ) -> Result> { progress_fn(ImportProgress::Gathering, false); - let slice = std::fs::read(path)?; + let slice = read_file(path)?; let data: ForeignData = serde_json::from_slice(&slice)?; data.import(self, progress_fn) } diff --git a/rslib/src/io.rs b/rslib/src/io.rs index fc86c5db6..d3ad39362 100644 --- a/rslib/src/io.rs +++ b/rslib/src/io.rs @@ -1,17 +1,80 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::path::{Component, Path}; +use std::{ + fs::File, + path::{Component, Path}, +}; use tempfile::NamedTempFile; -use crate::prelude::*; +use crate::{ + error::{FileIoError, FileIoSnafu, FileOp}, + prelude::*, +}; -pub(crate) fn tempfile_in_parent_of(file: &Path) -> Result { - let dir = file - .parent() - .ok_or_else(|| AnkiError::invalid_input("not a file path"))?; - NamedTempFile::new_in(dir).map_err(|err| AnkiError::file_io_error(err, dir)) +pub(crate) type Result = std::result::Result; + +/// See [std::fs::File::open]. +pub(crate) fn open_file(path: impl AsRef) -> Result { + File::open(&path).context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Open, + }) +} + +/// See [std::fs::write]. +pub(crate) fn write_file(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { + std::fs::write(&path, contents).context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Write, + }) +} + +/// See [std::fs::create_dir]. +pub(crate) fn create_dir(path: impl AsRef) -> Result<()> { + std::fs::create_dir(&path).context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Create, + }) +} + +/// See [std::fs::create_dir_all]. +pub(crate) fn create_dir_all(path: impl AsRef) -> Result<()> { + std::fs::create_dir_all(&path).context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Create, + }) +} + +/// See [std::fs::read]. +pub(crate) fn read_file(path: impl AsRef) -> Result> { + std::fs::read(&path).context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Read, + }) +} + +pub(crate) fn new_tempfile() -> Result { + NamedTempFile::new().context(FileIoSnafu { + path: std::env::temp_dir(), + op: FileOp::Create, + }) +} + +pub(crate) fn new_tempfile_in(dir: impl AsRef) -> Result { + NamedTempFile::new_in(&dir).context(FileIoSnafu { + path: dir.as_ref(), + op: FileOp::Create, + }) +} + +pub(crate) fn new_tempfile_in_parent_of(file: &Path) -> Result { + let dir = file.parent().unwrap_or(file); + NamedTempFile::new_in(dir).context(FileIoSnafu { + path: dir, + op: FileOp::Create, + }) } /// Atomically replace the target path with the provided temp file. @@ -22,24 +85,32 @@ pub(crate) fn tempfile_in_parent_of(file: &Path) -> Result { /// op, but it can be considerably slower. pub(crate) fn atomic_rename(file: NamedTempFile, target: &Path, fsync: bool) -> Result<()> { if fsync { - file.as_file().sync_all()?; + file.as_file().sync_all().context(FileIoSnafu { + path: file.path(), + op: FileOp::Sync, + })?; } - file.persist(&target) - .map_err(|err| AnkiError::IoError(format!("write {target:?} failed: {err}")))?; + file.persist(&target)?; #[cfg(not(windows))] if fsync { if let Some(parent) = target.parent() { - std::fs::File::open(parent) - .and_then(|file| file.sync_all()) - .map_err(|err| AnkiError::IoError(format!("sync {parent:?} failed: {err}")))?; + open_file(parent)?.sync_all().context(FileIoSnafu { + path: parent, + op: FileOp::Sync, + })?; } } Ok(()) } /// Like [std::fs::read_dir], but only yielding files. [Err]s are not filtered. -pub(crate) fn read_dir_files(path: impl AsRef) -> std::io::Result { - std::fs::read_dir(path).map(ReadDirFiles) +pub(crate) fn read_dir_files(path: impl AsRef) -> Result { + std::fs::read_dir(&path) + .map(ReadDirFiles) + .context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Read, + }) } /// True if name does not contain any path separators. diff --git a/rslib/src/media/changetracker.rs b/rslib/src/media/changetracker.rs index d89dd56c0..d1eaf9fac 100644 --- a/rslib/src/media/changetracker.rs +++ b/rslib/src/media/changetracker.rs @@ -4,6 +4,7 @@ use std::{collections::HashMap, path::Path, time}; use crate::{ + io::read_dir_files, log::{debug, Logger}, media::{ database::{MediaDatabaseContext, MediaEntry}, @@ -85,14 +86,9 @@ where let mut added_or_changed = vec![]; // loop through on-disk files - for dentry in self.media_folder.read_dir()? { + for dentry in read_dir_files(self.media_folder)? { let dentry = dentry?; - // skip folders - if dentry.file_type()?.is_dir() { - continue; - } - // if the filename is not valid unicode, skip it let fname_os = dentry.file_name(); let disk_fname = match fname_os.to_str() { @@ -141,8 +137,7 @@ where } // add entry to the list - let data = sha1_of_file(&dentry.path()) - .map_err(|e| AnkiError::IoError(format!("unable to read {}: {}", fname, e)))?; + let data = sha1_of_file(&dentry.path())?; let sha1 = Some(data); added_or_changed.push(FilesystemEntry { fname: fname.to_string(), @@ -239,6 +234,7 @@ mod test { use crate::{ error::Result, + io::{create_dir, write_file}, media::{ changetracker::ChangeTracker, database::MediaEntry, files::sha1_of_data, MediaManager, }, @@ -259,7 +255,7 @@ mod test { fn change_tracking() -> Result<()> { let dir = tempdir()?; let media_dir = dir.path().join("media"); - std::fs::create_dir(&media_dir)?; + create_dir(&media_dir)?; let media_db = dir.path().join("media.db"); let mgr = MediaManager::new(&media_dir, media_db)?; @@ -269,7 +265,7 @@ mod test { // add a file and check it's picked up let f1 = media_dir.join("file.jpg"); - fs::write(&f1, "hello")?; + write_file(&f1, "hello")?; change_mtime(&media_dir); @@ -304,7 +300,7 @@ mod test { assert!(ctx.get_pending_uploads(1)?.is_empty()); // modify it - fs::write(&f1, "hello1")?; + write_file(&f1, "hello1")?; change_mtime(&f1); change_mtime(&media_dir); diff --git a/rslib/src/media/check.rs b/rslib/src/media/check.rs index 2e67393e0..7478d8279 100644 --- a/rslib/src/media/check.rs +++ b/rslib/src/media/check.rs @@ -520,6 +520,7 @@ pub(crate) mod test { use crate::{ collection::{Collection, CollectionBuilder}, error::Result, + io::{create_dir, write_file}, media::{ check::{MediaCheckOutput, MediaChecker}, files::trash_folder, @@ -530,10 +531,10 @@ pub(crate) mod test { fn common_setup() -> Result<(TempDir, MediaManager, Collection)> { let dir = tempdir()?; let media_folder = dir.path().join("media"); - fs::create_dir(&media_folder)?; + create_dir(&media_folder)?; let media_db = dir.path().join("media.db"); let col_path = dir.path().join("col.anki2"); - fs::write(&col_path, MEDIACHECK_ANKI2)?; + write_file(&col_path, MEDIACHECK_ANKI2)?; let mgr = MediaManager::new(&media_folder, media_db.clone())?; let col = CollectionBuilder::new(col_path) @@ -548,12 +549,12 @@ pub(crate) mod test { let (_dir, mgr, mut col) = common_setup()?; // add some test files - fs::write(&mgr.media_folder.join("zerobytes"), "")?; - fs::create_dir(&mgr.media_folder.join("folder"))?; - fs::write(&mgr.media_folder.join("normal.jpg"), "normal")?; - fs::write(&mgr.media_folder.join("foo[.jpg"), "foo")?; - fs::write(&mgr.media_folder.join("_under.jpg"), "foo")?; - fs::write(&mgr.media_folder.join("unused.jpg"), "foo")?; + write_file(&mgr.media_folder.join("zerobytes"), "")?; + create_dir(&mgr.media_folder.join("folder"))?; + write_file(&mgr.media_folder.join("normal.jpg"), "normal")?; + write_file(&mgr.media_folder.join("foo[.jpg"), "foo")?; + write_file(&mgr.media_folder.join("_under.jpg"), "foo")?; + write_file(&mgr.media_folder.join("unused.jpg"), "foo")?; let progress = |_n| true; @@ -623,7 +624,7 @@ Unused: unused.jpg fn trash_handling() -> Result<()> { let (_dir, mgr, mut col) = common_setup()?; let trash_folder = trash_folder(&mgr.media_folder)?; - fs::write(trash_folder.join("test.jpg"), "test")?; + write_file(trash_folder.join("test.jpg"), "test")?; let progress = |_n| true; @@ -638,7 +639,7 @@ Unused: unused.jpg ); // if we repeat the process, restoring should do the same thing if the contents are equal - fs::write(trash_folder.join("test.jpg"), "test")?; + write_file(trash_folder.join("test.jpg"), "test")?; let mut checker = MediaChecker::new(&mut col, &mgr, progress); checker.restore_trash()?; @@ -650,7 +651,7 @@ Unused: unused.jpg ); // but rename if required - fs::write(trash_folder.join("test.jpg"), "test2")?; + write_file(trash_folder.join("test.jpg"), "test2")?; let mut checker = MediaChecker::new(&mut col, &mgr, progress); checker.restore_trash()?; @@ -671,7 +672,7 @@ Unused: unused.jpg fn unicode_normalization() -> Result<()> { let (_dir, mgr, mut col) = common_setup()?; - fs::write(&mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?; + write_file(&mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?; let progress = |_n| true; diff --git a/rslib/src/media/database.rs b/rslib/src/media/database.rs index ab49d7d86..fe305129f 100644 --- a/rslib/src/media/database.rs +++ b/rslib/src/media/database.rs @@ -266,23 +266,23 @@ fn row_to_name_and_checksum(row: &Row) -> Result<(String, Sha1Hash)> { let file_name = row.get(0)?; let sha1_str: String = row.get(1)?; let mut sha1 = [0; 20]; - hex::decode_to_slice(sha1_str, &mut sha1) - .map_err(|_| AnkiError::invalid_input(format!("bad media checksum: {file_name}")))?; + if let Err(err) = hex::decode_to_slice(sha1_str, &mut sha1) { + invalid_input!(err, "bad media checksum: {file_name}"); + } Ok((file_name, sha1)) } #[cfg(test)] mod test { - use tempfile::NamedTempFile; - use crate::{ error::Result, + io::new_tempfile, media::{database::MediaEntry, files::sha1_of_data, MediaManager}, }; #[test] fn database() -> Result<()> { - let db_file = NamedTempFile::new()?; + let db_file = new_tempfile()?; let db_file_path = db_file.path().to_str().unwrap(); let mut mgr = MediaManager::new("/dummy", db_file_path)?; let mut ctx = mgr.dbctx(); diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index 077e4ce93..2fae6bf04 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -15,7 +15,11 @@ use sha1::Sha1; use unic_ucd_category::GeneralCategory; use unicode_normalization::{is_nfc, UnicodeNormalization}; -use crate::prelude::*; +use crate::{ + error::{FileIoError, FileIoSnafu, FileOp}, + io::{create_dir, open_file, write_file}, + prelude::*, +}; /// The maximum length we allow a filename to be. When combined /// with the rest of the path, the full path needs to be under ~240 chars @@ -162,7 +166,7 @@ pub fn add_data_to_folder_uniquely<'a, P>( desired_name: &'a str, data: &[u8], sha1: Sha1Hash, -) -> io::Result> +) -> Result, FileIoError> where P: AsRef, { @@ -173,7 +177,7 @@ where let existing_file_hash = existing_file_sha1(&target_path)?; if existing_file_hash.is_none() { // no file with that name exists yet - fs::write(&target_path, data)?; + write_file(&target_path, data)?; return Ok(normalized_name); } @@ -186,7 +190,7 @@ where let hashed_name = add_hash_suffix_to_file_stem(normalized_name.as_ref(), &sha1); target_path.set_file_name(&hashed_name); - fs::write(&target_path, data)?; + write_file(&target_path, data)?; Ok(hashed_name.into()) } @@ -264,40 +268,33 @@ fn truncated_to_char_boundary(s: &str, mut max: usize) -> &str { } /// Return the SHA1 of a file if it exists, or None. -fn existing_file_sha1(path: &Path) -> io::Result> { +fn existing_file_sha1(path: &Path) -> Result, FileIoError> { match sha1_of_file(path) { Ok(o) => Ok(Some(o)), - Err(e) => { - if e.kind() == io::ErrorKind::NotFound { - Ok(None) - } else { - Err(e) - } - } + Err(e) if e.is_not_found() => Ok(None), + Err(e) => Err(e), } } /// Return the SHA1 of a file, failing if it doesn't exist. -pub(crate) fn sha1_of_file(path: &Path) -> io::Result { - let mut file = fs::File::open(path)?; - sha1_of_reader(&mut file) +pub(crate) fn sha1_of_file(path: &Path) -> Result { + let mut file = open_file(path)?; + sha1_of_reader(&mut file).context(FileIoSnafu { + path, + op: FileOp::Read, + }) } /// Return the SHA1 of a stream. -pub(crate) fn sha1_of_reader(reader: &mut impl Read) -> io::Result { +pub(crate) fn sha1_of_reader(reader: &mut impl Read) -> std::io::Result { let mut hasher = Sha1::new(); let mut buf = [0; 64 * 1024]; loop { match reader.read(&mut buf) { Ok(0) => break, Ok(n) => hasher.update(&buf[0..n]), - Err(e) => { - if e.kind() == io::ErrorKind::Interrupted { - continue; - } else { - return Err(e); - } - } + Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), }; } Ok(hasher.digest().bytes()) @@ -359,10 +356,10 @@ where pub(super) fn trash_folder(media_folder: &Path) -> Result { let trash_folder = media_folder.with_file_name("media.trash"); - match fs::create_dir(&trash_folder) { + match create_dir(&trash_folder) { Ok(()) => Ok(trash_folder), Err(e) => { - if e.kind() == io::ErrorKind::AlreadyExists { + if e.source.kind() == io::ErrorKind::AlreadyExists { Ok(trash_folder) } else { Err(e.into()) @@ -395,7 +392,7 @@ pub(super) fn add_file_from_ankiweb( let (renamed_from, path) = if let Cow::Borrowed(_) = normalized { let path = media_folder.join(normalized.as_ref()); debug!(log, "write"; "fname" => normalized.as_ref()); - fs::write(&path, data)?; + write_file(&path, data)?; (None, path) } else { // ankiweb sent us a non-normalized filename, so we'll rename it @@ -418,18 +415,9 @@ pub(super) fn add_file_from_ankiweb( } pub(super) fn data_for_file(media_folder: &Path, fname: &str) -> Result>> { - let mut file = match fs::File::open(&media_folder.join(fname)) { - Ok(file) => file, - Err(e) => { - if e.kind() == io::ErrorKind::NotFound { - return Ok(None); - } else { - return Err(AnkiError::IoError(format!( - "unable to read {}: {}", - fname, e - ))); - } - } + let mut file = match open_file(&media_folder.join(fname)) { + Err(e) if e.is_not_found() => return Ok(None), + res => res?, }; let mut buf = vec![]; file.read_to_end(&mut buf)?; diff --git a/rslib/src/media/sync.rs b/rslib/src/media/sync.rs index 853b4efd3..3ee76fd34 100644 --- a/rslib/src/media/sync.rs +++ b/rslib/src/media/sync.rs @@ -820,6 +820,7 @@ mod test { use crate::{ error::Result, + io::{create_dir, write_file}, media::{ sync::{determine_required_change, LocalState, MediaSyncProgress, RequiredChange}, MediaManager, @@ -829,10 +830,10 @@ mod test { async fn test_sync(hkey: &str) -> Result<()> { let dir = tempdir()?; let media_dir = dir.path().join("media"); - std::fs::create_dir(&media_dir)?; + create_dir(&media_dir)?; let media_db = dir.path().join("media.db"); - std::fs::write(media_dir.join("test.file").as_path(), "hello")?; + write_file(media_dir.join("test.file").as_path(), "hello")?; let progress = |progress: MediaSyncProgress| { println!("got progress: {:?}", progress); diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index f6dba082a..3d535e13d 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -13,18 +13,14 @@ use num_integer::Integer; use crate::{ cloze::contains_cloze, - decks::DeckId, define_newtype, - error::{AnkiError, Result}, - notetype::{CardGenContext, NoteField, Notetype, NotetypeId}, + notetype::{CardGenContext, NoteField}, ops::StateChanges, pb, pb::note_fields_check_response::State as NoteFieldsState, prelude::*, template::field_is_empty, text::{ensure_string_in_nfc, normalize_to_nfc, strip_html_preserving_media_filenames}, - timestamp::TimestampSecs, - types::Usn, }; define_newtype!(NoteId, i64); @@ -60,11 +56,7 @@ impl Note { } pub fn set_field(&mut self, idx: usize, text: impl Into) -> Result<()> { - if idx >= self.fields.len() { - return Err(AnkiError::invalid_input( - "field idx out of range".to_string(), - )); - } + require!(idx < self.fields.len(), "field idx out of range"); self.fields[idx] = text.into(); self.mark_dirty(); @@ -78,7 +70,7 @@ impl Collection { self.transact(Op::AddNote, |col| { let nt = col .get_notetype(note.notetype_id)? - .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; + .or_invalid("missing note type")?; let last_deck = col.get_last_deck_added_to_for_notetype(note.notetype_id); let ctx = CardGenContext::new(nt.as_ref(), last_deck, col.usn()?); let norm = col.get_config_bool(BoolKey::NormalizeNoteText); @@ -178,13 +170,11 @@ impl Note { pub(crate) fn prepare_for_update(&mut self, nt: &Notetype, normalize_text: bool) -> Result<()> { assert!(nt.id == self.notetype_id); let notetype_field_count = nt.fields.len().max(1); - if notetype_field_count != self.fields.len() { - return Err(AnkiError::invalid_input(format!( - "note has {} fields, expected {}", - self.fields.len(), - notetype_field_count - ))); - } + require!( + notetype_field_count == self.fields.len(), + "note has {} fields, expected {notetype_field_count}", + self.fields.len() + ); for field in self.fields_mut() { normalize_field(field, normalize_text); @@ -390,14 +380,14 @@ impl Collection { } pub(crate) fn update_note_inner(&mut self, note: &mut Note) -> Result<()> { - let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?; + let mut existing_note = self.storage.get_note(note.id)?.or_not_found(note.id)?; if !note_differs_from_db(&mut existing_note, note) { // nothing to do return Ok(()); } let nt = self .get_notetype(note.notetype_id)? - .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; + .or_invalid("missing note type")?; let last_deck = self.get_last_deck_added_to_for_notetype(note.notetype_id); let ctx = CardGenContext::new(nt.as_ref(), last_deck, self.usn()?); let norm = self.get_config_bool(BoolKey::NormalizeNoteText); @@ -493,9 +483,7 @@ impl Collection { let usn = self.usn()?; for (ntid, group) in &nids_by_notetype.into_iter().group_by(|tup| tup.0) { - let nt = self - .get_notetype(ntid)? - .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; + let nt = self.get_notetype(ntid)?.or_invalid("missing note type")?; let mut genctx = None; for (_, nid) in group { @@ -584,7 +572,7 @@ impl Collection { fn field_cloze_check(&mut self, note: &Note) -> Result { let notetype = self .get_notetype(note.notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(note.notetype_id)?; let cloze_fields = notetype.cloze_fields(); let mut has_cloze = false; let extraneous_cloze = note.fields.iter().enumerate().find_map(|(i, field)| { diff --git a/rslib/src/notes/undo.rs b/rslib/src/notes/undo.rs index 0ff309320..ff44836a8 100644 --- a/rslib/src/notes/undo.rs +++ b/rslib/src/notes/undo.rs @@ -22,7 +22,7 @@ impl Collection { let current = self .storage .get_note(note.id)? - .ok_or_else(|| AnkiError::invalid_input("note disappeared"))?; + .or_invalid("note disappeared")?; self.update_note_undoable(¬e, ¤t) } UndoableNoteChange::Removed(note) => self.restore_deleted_note(*note), @@ -32,7 +32,7 @@ impl Collection { let current = self .storage .get_note_tags_by_id(note_tags.id)? - .ok_or_else(|| AnkiError::invalid_input("note disappeared"))?; + .or_invalid("note disappeared")?; self.update_note_tags_undoable(¬e_tags, current) } } @@ -92,12 +92,9 @@ impl Collection { /// Add a note, not adding any cards. Caller guarantees id is unique. pub(crate) fn add_note_only_with_id_undoable(&mut self, note: &mut Note) -> Result<()> { - if self.storage.add_note_if_unique(note)? { - self.save_undo(UndoableNoteChange::Added(Box::new(note.clone()))); - Ok(()) - } else { - Err(AnkiError::invalid_input("note id existed")) - } + require!(self.storage.add_note_if_unique(note)?, "note id existed"); + self.save_undo(UndoableNoteChange::Added(Box::new(note.clone()))); + Ok(()) } pub(crate) fn update_note_tags_undoable( diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index 3c7ee9843..9a15df913 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -339,7 +339,7 @@ impl Collection { fn default_deck_conf(&mut self) -> Result<(DeckId, DeckConfigId)> { // currently hard-coded to 1, we could create this as needed in the future self.deck_conf_if_normal(DeckId(1))? - .ok_or_else(|| AnkiError::invalid_input("invalid default deck")) + .or_invalid("invalid default deck") } /// If deck exists and and is a normal deck, return its ID and config diff --git a/rslib/src/notetype/fields.rs b/rslib/src/notetype/fields.rs index 88586a156..b87e9635e 100644 --- a/rslib/src/notetype/fields.rs +++ b/rslib/src/notetype/fields.rs @@ -2,10 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{NoteFieldConfig, NoteFieldProto}; -use crate::{ - error::{AnkiError, Result}, - pb::UInt32, -}; +use crate::{pb::UInt32, prelude::*}; #[derive(Debug, PartialEq, Clone)] pub struct NoteField { @@ -54,9 +51,7 @@ impl NoteField { /// Fix the name of the field if it's valid. Otherwise explain why it's not. pub(crate) fn fix_name(&mut self) -> Result<()> { - if self.name.is_empty() { - return Err(AnkiError::invalid_input("Empty field name")); - } + require!(!self.name.is_empty(), "Empty field name"); let bad_chars = |c| c == ':' || c == '{' || c == '}' || c == '"'; if self.name.contains(bad_chars) { self.name = self.name.replace(bad_chars, ""); @@ -64,11 +59,7 @@ impl NoteField { // and leading/trailing whitespace and special chars let bad_start_chars = |c: char| c == '#' || c == '/' || c == '^' || c.is_whitespace(); let trimmed = self.name.trim().trim_start_matches(bad_start_chars); - if trimmed.is_empty() { - return Err(AnkiError::invalid_input( - "Field name: ".to_owned() + &self.name, - )); - } + require!(!trimmed.is_empty(), "Field name: {}", self.name); if trimmed.len() != self.name.len() { self.name = trimmed.into(); } diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 56f70afe5..d97157540 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -43,7 +43,7 @@ pub use crate::pb::{ }; use crate::{ define_newtype, - error::{CardTypeError, CardTypeErrorDetails}, + error::{CardTypeError, CardTypeErrorDetails, CardTypeSnafu, MissingClozeSnafu}, prelude::*, search::{JoinSearches, Node, SearchNode}, storage::comma_separated_ids, @@ -127,7 +127,7 @@ impl Notetype { self.templates.get(card_ord as usize) }; - template.ok_or(AnkiError::NotFound) + template.or_not_found(card_ord) } } @@ -159,7 +159,7 @@ impl Collection { let original = col .storage .get_notetype(notetype.id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(notetype.id)?; let usn = col.usn()?; notetype.set_modified(usn); col.add_or_update_notetype_with_existing_id_inner( @@ -241,15 +241,13 @@ impl Collection { /// Return the notetype used by `note_ids`, or an error if not exactly 1 /// notetype is in use. pub fn get_single_notetype_of_notes(&mut self, note_ids: &[NoteId]) -> Result { - if note_ids.is_empty() { - return Err(AnkiError::NotFound); - } + require!(!note_ids.is_empty(), "no note id provided"); let nids_node: Node = SearchNode::NoteIds(comma_separated_ids(note_ids)).into(); let note1 = self .storage .get_note(*note_ids.first().unwrap())? - .ok_or(AnkiError::NotFound)?; + .or_not_found(note_ids[0])?; if self .search_notes_unordered(note1.notetype_id.and(nids_node))? @@ -362,7 +360,7 @@ impl Notetype { }); } - fn ensure_template_fronts_unique(&self) -> Result<()> { + fn ensure_template_fronts_unique(&self) -> Result<(), CardTypeError> { lazy_static! { static ref CARD_TAG: Regex = Regex::new(r"\{\{\s*Card\s*\}\}").unwrap(); } @@ -371,11 +369,11 @@ impl Notetype { for (index, card) in self.templates.iter().enumerate() { if let Some(old_index) = map.insert(&card.config.q_format, index) { if !CARD_TAG.is_match(&card.config.q_format) { - return Err(AnkiError::CardTypeError(CardTypeError { + return Err(CardTypeError { notetype: self.name.clone(), ordinal: index, - details: CardTypeErrorDetails::Duplicate(old_index), - })); + source: CardTypeErrorDetails::Duplicate { index: old_index }, + }); } } } @@ -388,32 +386,32 @@ impl Notetype { fn ensure_valid_parsed_templates( &self, templates: &[(Option, Option)], - ) -> Result<()> { - if let Some((invalid_index, details)) = - templates.iter().enumerate().find_map(|(index, sides)| { - if let (Some(q), Some(a)) = sides { - let q_fields = q.all_referenced_field_names(); - if q_fields.is_empty() { - Some((index, CardTypeErrorDetails::NoFrontField)) - } else if self - .unknown_field_name(q_fields.union(&a.all_referenced_field_names())) - { - Some((index, CardTypeErrorDetails::NoSuchField)) - } else { - None - } - } else { - Some((index, CardTypeErrorDetails::TemplateError)) - } - }) - { - Err(AnkiError::CardTypeError(CardTypeError { - notetype: self.name.clone(), - ordinal: invalid_index, - details, - })) - } else { + ) -> Result<(), CardTypeError> { + for (ordinal, sides) in templates.iter().enumerate() { + self.ensure_valid_parsed_card_templates(sides) + .context(CardTypeSnafu { + notetype: &self.name, + ordinal, + })?; + } + Ok(()) + } + + fn ensure_valid_parsed_card_templates( + &self, + sides: &(Option, Option), + ) -> Result<(), CardTypeErrorDetails> { + if let (Some(q), Some(a)) = sides { + let q_fields = q.all_referenced_field_names(); + if q_fields.is_empty() { + return Err(CardTypeErrorDetails::NoFrontField); + } + if self.unknown_field_name(q_fields.union(&a.all_referenced_field_names())) { + return Err(CardTypeErrorDetails::NoSuchField); + } Ok(()) + } else { + Err(CardTypeErrorDetails::TemplateParseError) } } @@ -435,15 +433,15 @@ impl Notetype { fn ensure_cloze_if_cloze_notetype( &self, parsed_templates: &[(Option, Option)], - ) -> Result<()> { + ) -> Result<(), CardTypeError> { if self.is_cloze() && missing_cloze_filter(parsed_templates) { - return Err(AnkiError::CardTypeError(CardTypeError { - notetype: self.name.clone(), - ordinal: 0, - details: CardTypeErrorDetails::MissingCloze, - })); + MissingClozeSnafu.fail().context(CardTypeSnafu { + notetype: &self.name, + ordinal: 0usize, + }) + } else { + Ok(()) } - Ok(()) } pub(crate) fn normalize_names(&mut self) { @@ -474,19 +472,13 @@ impl Notetype { existing: Option<&Notetype>, skip_checks: bool, ) -> Result<()> { - if self.fields.is_empty() { - return Err(AnkiError::invalid_input("1 field required")); - } - if self.templates.is_empty() { - return Err(AnkiError::invalid_input("1 template required")); - } + require!(!self.fields.is_empty(), "1 field required"); + require!(!self.templates.is_empty(), "1 template required"); let bad_chars = |c| c == '"'; if self.name.contains(bad_chars) { self.name = self.name.replace(bad_chars, ""); } - if self.name.is_empty() { - return Err(AnkiError::invalid_input("Empty note type name")); - } + require!(!self.name.is_empty(), "Empty notetype name"); self.normalize_names(); self.fix_field_names()?; self.fix_template_names()?; @@ -505,14 +497,22 @@ impl Notetype { } self.config.reqs = reqs; if !skip_checks { - self.ensure_template_fronts_unique()?; - self.ensure_valid_parsed_templates(&parsed_templates)?; - self.ensure_cloze_if_cloze_notetype(&parsed_templates)?; + self.check_templates(parsed_templates)?; } Ok(()) } + fn check_templates( + &self, + parsed_templates: Vec<(Option, Option)>, + ) -> Result<()> { + self.ensure_template_fronts_unique() + .and(self.ensure_valid_parsed_templates(&parsed_templates)) + .and(self.ensure_cloze_if_cloze_notetype(&parsed_templates))?; + Ok(()) + } + fn renamed_and_removed_fields(&self, current: &Notetype) -> HashMap> { let mut remaining_ords = HashSet::new(); // gather renames diff --git a/rslib/src/notetype/notetypechange.rs b/rslib/src/notetype/notetypechange.rs index 661ca41e5..f685f197d 100644 --- a/rslib/src/notetype/notetypechange.rs +++ b/rslib/src/notetype/notetypechange.rs @@ -73,10 +73,10 @@ impl Collection { ) -> Result { let old_notetype = self .get_notetype(old_notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(old_notetype_id)?; let new_notetype = self .get_notetype(new_notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(new_notetype_id)?; let current_schema = self.storage.get_collection_timestamps()?.schema_change; let old_notetype_name = &old_notetype.name; @@ -206,16 +206,17 @@ fn default_field_map(current_notetype: &Notetype, new_notetype: &Notetype) -> Ve impl Collection { fn change_notetype_of_notes_inner(&mut self, input: ChangeNotetypeInput) -> Result<()> { - if input.current_schema != self.storage.get_collection_timestamps()?.schema_change { - return Err(AnkiError::invalid_input("schema changed")); - } + require!( + input.current_schema == self.storage.get_collection_timestamps()?.schema_change, + "schema changed" + ); let usn = self.usn()?; self.set_schema_modified()?; if let Some(new_templates) = input.new_templates { let old_notetype = self .get_notetype(input.old_notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(input.old_notetype_id)?; self.update_cards_for_new_notetype( &input.note_ids, old_notetype.templates.len(), @@ -253,12 +254,12 @@ impl Collection { ) -> Result<()> { let notetype = self .get_notetype(new_notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(new_notetype_id)?; let last_deck = self.get_last_deck_added_to_for_notetype(notetype.id); let ctx = CardGenContext::new(notetype.as_ref(), last_deck, usn); for nid in note_ids { - let mut note = self.storage.get_note(*nid)?.ok_or(AnkiError::NotFound)?; + let mut note = self.storage.get_note(*nid)?.or_not_found(nid)?; let original = note.clone(); remap_fields(note.fields_mut(), new_fields); note.notetype_id = new_notetype_id; @@ -335,7 +336,7 @@ impl Collection { notetype_id: NotetypeId, usn: Usn, ) -> Result<()> { - let notetype = self.get_notetype(notetype_id)?.ok_or(AnkiError::NotFound)?; + let notetype = self.get_notetype(notetype_id)?.or_not_found(notetype_id)?; if notetype.config.kind() == NotetypeKind::Normal { // cloze -> normal change requires clean up diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index 9083d9080..94023eef9 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -5,11 +5,7 @@ use std::{borrow::Cow, collections::HashMap}; use super::{CardTemplate, Notetype, NotetypeKind}; use crate::{ - card::{Card, CardId}, - collection::Collection, - error::{AnkiError, Result}, - i18n::I18n, - notes::{Note, NoteId}, + prelude::*, template::{field_is_empty, render_card, ParsedTemplate, RenderedNode}, }; @@ -23,22 +19,19 @@ pub struct RenderCardOutput { impl Collection { /// Render an existing card saved in the database. pub fn render_existing_card(&mut self, cid: CardId, browser: bool) -> Result { - let card = self - .storage - .get_card(cid)? - .ok_or_else(|| AnkiError::invalid_input("no such card"))?; + let card = self.storage.get_card(cid)?.or_invalid("no such card")?; let note = self .storage .get_note(card.note_id)? - .ok_or_else(|| AnkiError::invalid_input("no such note"))?; + .or_invalid("no such note")?; let nt = self .get_notetype(note.notetype_id)? - .ok_or_else(|| AnkiError::invalid_input("no such notetype"))?; + .or_invalid("no such notetype")?; let template = match nt.config.kind() { NotetypeKind::Normal => nt.templates.get(card.template_idx as usize), NotetypeKind::Cloze => nt.templates.get(0), } - .ok_or_else(|| AnkiError::invalid_input("missing template"))?; + .or_invalid("missing template")?; self.render_card(¬e, &card, &nt, template, browser) } @@ -56,7 +49,7 @@ impl Collection { let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?; let nt = self .get_notetype(note.notetype_id)? - .ok_or_else(|| AnkiError::invalid_input("no such notetype"))?; + .or_invalid("no such notetype")?; if fill_empty { fill_empty_fields(note, &template.config.q_format, &nt, &self.tr); diff --git a/rslib/src/notetype/templates.rs b/rslib/src/notetype/templates.rs index 0b8b544fb..b2f11c897 100644 --- a/rslib/src/notetype/templates.rs +++ b/rslib/src/notetype/templates.rs @@ -2,14 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{CardTemplateConfig, CardTemplateProto}; -use crate::{ - decks::DeckId, - error::{AnkiError, Result}, - pb::UInt32, - template::ParsedTemplate, - timestamp::TimestampSecs, - types::Usn, -}; +use crate::{pb::UInt32, prelude::*, template::ParsedTemplate}; #[derive(Debug, PartialEq, Clone)] pub struct CardTemplate { @@ -106,15 +99,9 @@ impl CardTemplate { /// Return whether the name is valid. Remove quote characters if it leads to a valid name. pub(crate) fn fix_name(&mut self) -> Result<()> { let bad_chars = |c| c == '"'; - if self.name.is_empty() { - return Err(AnkiError::invalid_input("Empty template name")); - } + require!(!self.name.is_empty(), "Empty template name"); let trimmed = self.name.replace(bad_chars, ""); - if trimmed.is_empty() { - return Err(AnkiError::invalid_input( - "Template name contain only quotes", - )); - } + require!(!trimmed.is_empty(), "Template name contains only quotes"); if self.name.len() != trimmed.len() { self.name = trimmed; } diff --git a/rslib/src/notetype/undo.rs b/rslib/src/notetype/undo.rs index 5780ed9b2..99f76f944 100644 --- a/rslib/src/notetype/undo.rs +++ b/rslib/src/notetype/undo.rs @@ -19,7 +19,7 @@ impl Collection { let current = self .storage .get_notetype(nt.id)? - .ok_or_else(|| AnkiError::invalid_input("notetype disappeared"))?; + .or_invalid("notetype disappeared")?; self.update_notetype_undoable(&nt, current) } UndoableNotetypeChange::Removed(nt) => self.restore_deleted_notetype(*nt), diff --git a/rslib/src/prelude.rs b/rslib/src/prelude.rs index 042080415..d9e2a5a88 100644 --- a/rslib/src/prelude.rs +++ b/rslib/src/prelude.rs @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html pub use slog::{debug, Logger}; +pub use snafu::ResultExt; pub(crate) use crate::types::IntoNewtypeVec; pub use crate::{ @@ -10,12 +11,14 @@ pub use crate::{ config::BoolKey, deckconfig::{DeckConfig, DeckConfigId}, decks::{Deck, DeckId, DeckKind, NativeDeckName}, - error::{AnkiError, Result}, + error::{AnkiError, OrInvalid, OrNotFound, Result}, i18n::I18n, + invalid_input, media::Sha1Hash, notes::{Note, NoteId}, notetype::{Notetype, NotetypeId}, ops::{Op, OpChanges, OpOutput}, + require, revlog::RevlogId, search::{SearchBuilder, TryIntoSearch}, timestamp::{TimestampMillis, TimestampSecs}, diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 21ec1d3b0..01bb9f65f 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -116,9 +116,7 @@ impl CardStateUpdater { if let CardState::Filtered(filtered) = ¤t { match filtered { FilteredState::Preview(_) => { - return Err(AnkiError::invalid_input( - "should set finished=true, not return different state", - )); + invalid_input!("should set finished=true, not return different state") } FilteredState::Rescheduling(_) => { // card needs to be removed from normal filtered deck, then scheduled normally @@ -166,13 +164,11 @@ impl CardStateUpdater { } fn ensure_filtered(&self) -> Result<()> { - if self.card.original_deck_id.0 == 0 { - Err(AnkiError::invalid_input( - "card answering can't transition into filtered state", - )) - } else { - Ok(()) - } + require!( + self.card.original_deck_id.0 != 0, + "card answering can't transition into filtered state", + ); + Ok(()) } } @@ -190,7 +186,7 @@ impl Rating { impl Collection { /// Return the next states that will be applied for each answer button. pub fn get_scheduling_states(&mut self, cid: CardId) -> Result { - let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?; + let card = self.storage.get_card(cid)?.or_not_found(cid)?; let ctx = self.card_state_updater(card)?; let current = ctx.current_card_state(); let state_ctx = ctx.state_context(); @@ -254,19 +250,19 @@ impl Collection { let card = self .storage .get_card(answer.card_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(answer.card_id)?; let original = card.clone(); let usn = self.usn()?; let mut updater = self.card_state_updater(card)?; answer.cap_answer_secs(updater.config.inner.cap_answer_time_to_secs); let current_state = updater.current_card_state(); - if current_state != answer.current_state { - return Err(AnkiError::invalid_input(format!( - "card was modified: {:#?} {:#?}", - current_state, answer.current_state, - ))); - } + require!( + current_state == answer.current_state, + "card was modified: {current_state:#?} {:#?}", + answer.current_state, + ); + let revlog_partial = updater.apply_study_state(current_state, answer.new_state)?; self.add_partial_revlog(revlog_partial, usn, answer)?; @@ -348,7 +344,7 @@ impl Collection { let deck = self .storage .get_deck(card.deck_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(card.deck_id)?; let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; Ok(CardStateUpdater { fuzz_seed: get_fuzz_seed(&card), @@ -371,8 +367,8 @@ impl Collection { let home_deck = self .storage .get_deck(home_deck_id)? - .ok_or(AnkiError::NotFound)?; - home_deck.config_id().ok_or(AnkiError::NotFound)? + .or_not_found(home_deck_id)?; + home_deck.config_id().or_invalid("home deck is filtered")? }; Ok(self.storage.get_deck_config(config_id)?.unwrap_or_default()) diff --git a/rslib/src/scheduler/filtered/custom_study.rs b/rslib/src/scheduler/filtered/custom_study.rs index 8dbf6a0bd..8753a39f6 100644 --- a/rslib/src/scheduler/filtered/custom_study.rs +++ b/rslib/src/scheduler/filtered/custom_study.rs @@ -26,14 +26,14 @@ impl Collection { deck_id: DeckId, ) -> Result { // daily counts - let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; + let deck = self.get_deck(deck_id)?.or_not_found(deck_id)?; let normal = deck.normal()?; let extend_new = normal.extend_new; let extend_review = normal.extend_review; let subtree = self .deck_tree(Some(TimestampSecs::now()))? .get_deck(deck_id) - .ok_or(AnkiError::NotFound)?; + .or_not_found(deck_id)?; let v3 = self.get_config_bool(BoolKey::Sched2021); let available_new_including_children = subtree.sum(|node| node.new_uncapped); let available_review_including_children = subtree.sum(|node| node.review_uncapped); @@ -99,17 +99,14 @@ impl Collection { let mut deck = self .storage .get_deck(input.deck_id.into())? - .ok_or(AnkiError::NotFound)?; + .or_not_found(input.deck_id)?; - match input - .value - .ok_or_else(|| AnkiError::invalid_input("missing oneof value"))? - { + match input.value.or_invalid("missing oneof value")? { CustomStudyValue::NewLimitDelta(delta) => { let today = self.current_due_day(0)?; self.extend_limits(today, self.usn()?, deck.id, delta, 0)?; if delta > 0 { - deck = self.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; + deck = self.storage.get_deck(deck.id)?.or_not_found(deck.id)?; let original = deck.clone(); deck.normal_mut()?.extend_new = delta as u32; self.update_deck_inner(&mut deck, original, self.usn()?)?; @@ -120,7 +117,7 @@ impl Collection { let today = self.current_due_day(0)?; self.extend_limits(today, self.usn()?, deck.id, 0, delta)?; if delta > 0 { - deck = self.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; + deck = self.storage.get_deck(deck.id)?.or_not_found(deck.id)?; let original = deck.clone(); deck.normal_mut()?.extend_review = delta as u32; self.update_deck_inner(&mut deck, original, self.usn()?)?; @@ -162,11 +159,7 @@ impl Collection { let human_name = self.tr.custom_study_custom_study_session().to_string(); if let Some(did) = self.get_deck_id(&human_name)? { - if !self - .get_deck(did)? - .ok_or(AnkiError::NotFound)? - .is_filtered() - { + if !self.get_deck(did)?.or_not_found(did)?.is_filtered() { return Err(CustomStudyError::ExistingDeck.into()); } id = did; @@ -181,7 +174,12 @@ impl Collection { self.add_or_update_filtered_deck_inner(deck) .map(|_| ()) .map_err(|err| { - if err == AnkiError::FilteredDeckError(FilteredDeckError::SearchReturnedNoCards) { + if matches!( + err, + AnkiError::FilteredDeckError { + source: FilteredDeckError::SearchReturnedNoCards + } + ) { CustomStudyError::NoMatchingCards.into() } else { err @@ -352,9 +350,9 @@ mod test { deck_id: 1, value: Some(Value::Cram(cram.clone())), }), - Err(AnkiError::CustomStudyError( - CustomStudyError::NoMatchingCards - )) + Err(AnkiError::CustomStudyError { + source: CustomStudyError::NoMatchingCards + }) ); assert_eq!( &get_defaults(&mut col)?, diff --git a/rslib/src/scheduler/filtered/mod.rs b/rslib/src/scheduler/filtered/mod.rs index a9d3bfe84..c52ce2d10 100644 --- a/rslib/src/scheduler/filtered/mod.rs +++ b/rslib/src/scheduler/filtered/mod.rs @@ -42,7 +42,7 @@ impl Collection { let deck = if deck_id.0 == 0 { self.new_filtered_deck_for_adding()? } else { - self.storage.get_deck(deck_id)?.ok_or(AnkiError::NotFound)? + self.storage.get_deck(deck_id)?.or_not_found(deck_id)? }; deck.try_into() @@ -71,7 +71,7 @@ impl Collection { // Unlike the old Python code, this also marks the cards as modified. pub fn rebuild_filtered_deck(&mut self, did: DeckId) -> Result> { self.transact(Op::RebuildFilteredDeck, |col| { - let deck = col.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let deck = col.get_deck(did)?.or_not_found(did)?; col.rebuild_filtered_deck_inner(&deck, col.usn()?) }) } @@ -170,10 +170,7 @@ impl Collection { apply_update_to_filtered_deck(&mut deck, update); self.add_deck_inner(&mut deck, usn)?; } else { - let original = self - .storage - .get_deck(update.id)? - .ok_or(AnkiError::NotFound)?; + let original = self.storage.get_deck(update.id)?.or_not_found(update.id)?; deck = original.clone(); apply_update_to_filtered_deck(&mut deck, update); self.update_deck_inner(&mut deck, original, usn)?; @@ -241,14 +238,13 @@ impl TryFrom for FilteredDeckForUpdate { fn try_from(value: Deck) -> Result { let human_name = value.human_name(); - if let DeckKind::Filtered(filtered) = value.kind { - Ok(FilteredDeckForUpdate { + match value.kind { + DeckKind::Filtered(filtered) => Ok(FilteredDeckForUpdate { id: value.id, human_name, config: filtered, - }) - } else { - Err(AnkiError::invalid_input("not filtered")) + }), + _ => invalid_input!("not filtered"), } } } diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index 7f553af76..5090bd8f4 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -126,7 +126,7 @@ impl QueueBuilder { pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result { let timing = col.timing_for_timestamp(TimestampSecs::now())?; let config_map = col.storage.get_deck_config_map()?; - let root_deck = col.storage.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; + let root_deck = col.storage.get_deck(deck_id)?.or_not_found(deck_id)?; let child_decks = col.storage.child_decks(&root_deck)?; let limits = LimitTreeMap::build(&root_deck, child_decks, &config_map, timing.days_elapsed); let sort_options = sort_options(&root_deck, &config_map); diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 308220364..c9e4d8774 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -88,12 +88,11 @@ impl Collection { let card = self .storage .get_card(entry.card_id())? - .ok_or(AnkiError::NotFound)?; - if card.mtime != entry.mtime() { - return Err(AnkiError::invalid_input( - "bug: card modified without updating queue", - )); - } + .or_not_found(entry.card_id())?; + require!( + card.mtime == entry.mtime(), + "bug: card modified without updating queue", + ); // fixme: pass in card instead of id let next_states = self.get_scheduling_states(card.id)?; @@ -141,7 +140,7 @@ impl CardQueues { } else if self.main.front().filter(|e| e.id == id).is_some() { Ok(self.pop_main().unwrap().into()) } else { - Err(AnkiError::invalid_input("not at top of queue")) + invalid_input!("not at top of queue") } } diff --git a/rslib/src/scheduler/reviews.rs b/rslib/src/scheduler/reviews.rs index b5bf36839..fe1d270ee 100644 --- a/rslib/src/scheduler/reviews.rs +++ b/rslib/src/scheduler/reviews.rs @@ -80,7 +80,7 @@ pub fn parse_due_date_str(s: &str) -> Result { ) .unwrap(); } - let caps = RE.captures(s).ok_or_else(|| AnkiError::invalid_input(s))?; + let caps = RE.captures(s).or_invalid(s)?; let min: u32 = caps.name("min").unwrap().as_str().parse()?; let max = if let Some(max) = caps.name("max") { max.as_str().parse()? @@ -117,11 +117,8 @@ impl Collection { let ease_factor = match decks_initial_ease.get(&deck_id) { Some(ease) => *ease, None => { - let config_id = col - .get_deck(deck_id)? - .ok_or(AnkiError::NotFound)? - .config_id() - .ok_or(AnkiError::NotFound)?; + let deck = col.get_deck(deck_id)?.or_not_found(deck_id)?; + let config_id = deck.config_id().or_invalid("home deck is filtered")?; let ease = col .get_deck_config(config_id, true)? // just for compiler; get_deck_config() is guaranteed to return a value diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 48f05909d..4588f5f3c 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -16,14 +16,7 @@ use rusqlite::{params_from_iter, types::FromSql}; use sqlwriter::{RequiredTable, SqlWriter}; pub use writer::replace_search_node; -use crate::{ - browser_table::Column, - card::{Card, CardId, CardType}, - collection::Collection, - error::Result, - notes::NoteId, - prelude::AnkiError, -}; +use crate::{browser_table::Column, card::CardType, prelude::*}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ReturnItemType { @@ -333,12 +326,7 @@ fn write_order( ReturnItemType::Cards => card_order_from_sort_column(column), ReturnItemType::Notes => note_order_from_sort_column(column), }; - if order.is_empty() { - return Err(AnkiError::invalid_input(format!( - "Can't sort {:?} by {:?}.", - item_type, column - ))); - } + require!(!order.is_empty(), "Can't sort {item_type:?} by {column:?}."); if reverse { sql.push_str( &order diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index eb9110c08..a08a5287d 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -26,7 +26,7 @@ fn parse_failure(input: &str, kind: FailKind) -> nom::Err> { } fn parse_error(input: &str) -> nom::Err> { - nom::Err::Error(ParseError::Anki(input, FailKind::Other(None))) + nom::Err::Error(ParseError::Anki(input, FailKind::Other { info: None })) } #[derive(Debug, PartialEq, Clone)] @@ -256,13 +256,20 @@ fn unquoted_term(s: &str) -> IResult { if let nom::Err::Error((c, NomErrorKind::NoneOf)) = err { Err(parse_failure( s, - FailKind::UnknownEscape(format!("\\{}", c)), + FailKind::UnknownEscape { + provided: format!("\\{}", c), + }, )) } else if "\"() \u{3000}".contains(s.chars().next().unwrap()) { Err(parse_error(s)) } else { // input ends in an odd number of backslashes - Err(parse_failure(s, FailKind::UnknownEscape('\\'.to_string()))) + Err(parse_failure( + s, + FailKind::UnknownEscape { + provided: '\\'.to_string(), + }, + )) } } } @@ -394,7 +401,9 @@ fn parse_prop(prop_clause: &str) -> ParseResult { .map_err(|_| { parse_failure( prop_clause, - FailKind::InvalidPropProperty(prop_clause.into()), + FailKind::InvalidPropProperty { + provided: prop_clause.into(), + }, ) })?; @@ -406,7 +415,14 @@ fn parse_prop(prop_clause: &str) -> ParseResult { tag("<"), tag(">"), ))(tail) - .map_err(|_| parse_failure(prop_clause, FailKind::InvalidPropOperator(prop.to_string())))?; + .map_err(|_| { + parse_failure( + prop_clause, + FailKind::InvalidPropOperator { + provided: prop.to_string(), + }, + ) + })?; let kind = match prop { "ease" => PropertyKind::Ease(parse_f32(num, prop_clause)?), @@ -556,7 +572,12 @@ fn parse_state(s: &str) -> ParseResult { "buried-manually" => UserBuried, "buried-sibling" => SchedBuried, "suspended" => Suspended, - _ => return Err(parse_failure(s, FailKind::InvalidState(s.into()))), + _ => { + return Err(parse_failure( + s, + FailKind::InvalidState { provided: s.into() }, + )) + } })) } @@ -580,10 +601,9 @@ fn check_id_list<'a, 'b>(s: &'a str, context: &'b str) -> ParseResult<'a, &'a st Err(parse_failure( s, // id lists are undocumented, so no translation - FailKind::Other(Some(format!( - "expected only digits and commas in {}:", - context - ))), + FailKind::Other { + info: Some(format!("expected only digits and commas in {}:", context)), + }, )) } } @@ -601,7 +621,9 @@ fn parse_dupe(s: &str) -> ParseResult { // this is an undocumented keyword, so no translation/help Err(parse_failure( s, - FailKind::Other(Some("invalid 'dupe:' search".into())), + FailKind::Other { + info: Some("invalid 'dupe:' search".into()), + }, )) } } @@ -643,7 +665,10 @@ fn unescape_quotes_and_backslashes(s: &str) -> String { /// Unescape chars with special meaning to the parser. fn unescape(txt: &str) -> ParseResult { if let Some(seq) = invalid_escape_sequence(txt) { - Err(parse_failure(txt, FailKind::UnknownEscape(seq))) + Err(parse_failure( + txt, + FailKind::UnknownEscape { provided: seq }, + )) } else { Ok(if is_parser_escape(txt) { lazy_static! { @@ -883,11 +908,11 @@ mod test { use crate::error::AnkiError; fn assert_err_kind(input: &str, kind: FailKind) { - assert_eq!(parse(input), Err(AnkiError::SearchError(kind))); + assert_eq!(parse(input), Err(AnkiError::SearchError { source: kind })); } fn failkind(input: &str) -> SearchErrorKind { - if let Err(AnkiError::SearchError(err)) = parse(input) { + if let Err(AnkiError::SearchError { source: err }) = parse(input) { err } else { panic!("expected search error"); @@ -927,12 +952,42 @@ mod test { assert_err_kind(":foo", MissingKey); assert_err_kind(r#":"foo""#, MissingKey); - assert_err_kind(r"\", UnknownEscape(r"\".to_string())); - assert_err_kind(r"\%", UnknownEscape(r"\%".to_string())); - assert_err_kind(r"foo\", UnknownEscape(r"\".to_string())); - assert_err_kind(r"\foo", UnknownEscape(r"\f".to_string())); - assert_err_kind(r"\ ", UnknownEscape(r"\".to_string())); - assert_err_kind(r#""\ ""#, UnknownEscape(r"\ ".to_string())); + assert_err_kind( + r"\", + UnknownEscape { + provided: r"\".to_string(), + }, + ); + assert_err_kind( + r"\%", + UnknownEscape { + provided: r"\%".to_string(), + }, + ); + assert_err_kind( + r"foo\", + UnknownEscape { + provided: r"\".to_string(), + }, + ); + assert_err_kind( + r"\foo", + UnknownEscape { + provided: r"\f".to_string(), + }, + ); + assert_err_kind( + r"\ ", + UnknownEscape { + provided: r"\".to_string(), + }, + ); + assert_err_kind( + r#""\ ""#, + UnknownEscape { + provided: r"\ ".to_string(), + }, + ); for term in &[ "nid:1_2,3", @@ -944,14 +999,39 @@ mod test { "cid:,2,3", "cid:1,2,", ] { - assert!(matches!(failkind(term), SearchErrorKind::Other(_))); + assert!(matches!(failkind(term), SearchErrorKind::Other { .. })); } - assert_err_kind("is:foo", InvalidState("foo".into())); - assert_err_kind("is:DUE", InvalidState("DUE".into())); - assert_err_kind("is:New", InvalidState("New".into())); - assert_err_kind("is:", InvalidState("".into())); - assert_err_kind(r#""is:learn ""#, InvalidState("learn ".into())); + assert_err_kind( + "is:foo", + InvalidState { + provided: "foo".into(), + }, + ); + assert_err_kind( + "is:DUE", + InvalidState { + provided: "DUE".into(), + }, + ); + assert_err_kind( + "is:New", + InvalidState { + provided: "New".into(), + }, + ); + assert_err_kind( + "is:", + InvalidState { + provided: "".into(), + }, + ); + assert_err_kind( + r#""is:learn ""#, + InvalidState { + provided: "learn ".into(), + }, + ); assert_err_kind(r#""flag: ""#, InvalidFlag); assert_err_kind("flag:-0", InvalidFlag); @@ -1008,13 +1088,43 @@ mod test { SearchErrorKind::InvalidWholeNumber { .. } )); - assert_err_kind("prop:", InvalidPropProperty("".into())); - assert_err_kind("prop:=1", InvalidPropProperty("=1".into())); - assert_err_kind("prop:DUE<5", InvalidPropProperty("DUE<5".into())); + assert_err_kind( + "prop:", + InvalidPropProperty { + provided: "".into(), + }, + ); + assert_err_kind( + "prop:=1", + InvalidPropProperty { + provided: "=1".into(), + }, + ); + assert_err_kind( + "prop:DUE<5", + InvalidPropProperty { + provided: "DUE<5".into(), + }, + ); - assert_err_kind("prop:lapses", InvalidPropOperator("lapses".to_string())); - assert_err_kind("prop:pos~1", InvalidPropOperator("pos".to_string())); - assert_err_kind("prop:reps10", InvalidPropOperator("reps".to_string())); + assert_err_kind( + "prop:lapses", + InvalidPropOperator { + provided: "lapses".to_string(), + }, + ); + assert_err_kind( + "prop:pos~1", + InvalidPropOperator { + provided: "pos".to_string(), + }, + ); + assert_err_kind( + "prop:reps10", + InvalidPropOperator { + provided: "reps".to_string(), + }, + ); // unsigned diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 20bd5286a..92d4adb29 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -687,12 +687,13 @@ impl SearchNode { #[cfg(test)] mod test { - use std::fs; - use tempfile::tempdir; use super::{super::parser::parse, *}; - use crate::collection::{Collection, CollectionBuilder}; + use crate::{ + collection::{Collection, CollectionBuilder}, + io::write_file, + }; // shortcut fn s(req: &mut Collection, search: &str) -> (String, Vec) { @@ -709,7 +710,7 @@ mod test { use crate::media::check::test::MEDIACHECK_ANKI2; let dir = tempdir().unwrap(); let col_path = dir.path().join("col.anki2"); - fs::write(&col_path, MEDIACHECK_ANKI2).unwrap(); + write_file(&col_path, MEDIACHECK_ANKI2).unwrap(); let mut col = CollectionBuilder::new(col_path).build().unwrap(); let ctx = &mut col; diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 9aae46e45..76b08ef40 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -10,18 +10,18 @@ use crate::{ impl Collection { pub fn card_stats(&mut self, cid: CardId) -> Result { - let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?; + let card = self.storage.get_card(cid)?.or_not_found(cid)?; let note = self .storage .get_note(card.note_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(card.note_id)?; let nt = self .get_notetype(note.notetype_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(note.notetype_id)?; let deck = self .storage .get_deck(card.deck_id)? - .ok_or(AnkiError::NotFound)?; + .or_not_found(card.deck_id)?; let revlog = self.storage.get_revlog_entries_for_card(card.id)?; let (average_secs, total_secs) = average_and_total_secs_strings(&revlog); diff --git a/rslib/src/storage/card/data.rs b/rslib/src/storage/card/data.rs index 39d64280b..a97883e98 100644 --- a/rslib/src/storage/card/data.rs +++ b/rslib/src/storage/card/data.rs @@ -72,18 +72,16 @@ fn meta_is_empty(s: &str) -> bool { fn validate_custom_data(json_str: &str) -> Result<()> { if !meta_is_empty(json_str) { - let object: HashMap<&str, Value> = serde_json::from_str(json_str) - .map_err(|e| AnkiError::invalid_input(format!("custom data not an object: {e}")))?; - if object.keys().any(|k| k.as_bytes().len() > 8) { - return Err(AnkiError::invalid_input( - "custom data keys must be <= 8 bytes", - )); - } - if json_str.len() > 100 { - return Err(AnkiError::invalid_input( - "serialized custom data must be under 100 bytes", - )); - } + let object: HashMap<&str, Value> = + serde_json::from_str(json_str).or_invalid("custom data not an object")?; + require!( + object.keys().all(|k| k.as_bytes().len() <= 8), + "custom data keys must be <= 8 bytes" + ); + require!( + json_str.len() <= 100, + "serialized custom data must be under 100 bytes" + ); } Ok(()) } diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs index 8b8976aa8..ccc3d0ca4 100644 --- a/rslib/src/storage/deck/mod.rs +++ b/rslib/src/storage/deck/mod.rs @@ -167,9 +167,7 @@ impl SqliteStorage { } pub(crate) fn update_deck(&self, deck: &Deck) -> Result<()> { - if deck.id.0 == 0 { - return Err(AnkiError::invalid_input("deck with id 0")); - } + require!(deck.id.0 != 0, "deck with id 0"); let mut stmt = self.db.prepare_cached(include_str!("update_deck.sql"))?; let mut common = vec![]; deck.common.encode(&mut common)?; @@ -187,21 +185,14 @@ impl SqliteStorage { deck.id ])?; - if count == 0 { - Err(AnkiError::invalid_input( - "update_deck() called with non-existent deck", - )) - } else { - Ok(()) - } + require!(count != 0, "update_deck() called with non-existent deck"); + Ok(()) } /// Used for syncing&undo; will keep existing ID. Shouldn't be used to add /// new decks locally, since it does not allocate an id. pub(crate) fn add_or_update_deck_with_existing_id(&self, deck: &Deck) -> Result<()> { - if deck.id.0 == 0 { - return Err(AnkiError::invalid_input("deck with id 0")); - } + require!(deck.id.0 != 0, "deck with id 0"); let mut stmt = self .db .prepare_cached(include_str!("add_or_update_deck.sql"))?; @@ -263,7 +254,7 @@ impl SqliteStorage { } pub(crate) fn deck_with_children(&self, deck_id: DeckId) -> Result> { - let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; + let deck = self.get_deck(deck_id)?.or_not_found(deck_id)?; let prefix_start = format!("{}\x1f", deck.name); let prefix_end = format!("{}\x20", deck.name); iter::once(Ok(deck)) @@ -382,7 +373,9 @@ impl SqliteStorage { let usn = self.usn(server)?; let decks = self .get_schema11_decks() - .map_err(|e| AnkiError::JsonError(format!("decoding decks: {}", e)))?; + .map_err(|e| AnkiError::JsonError { + info: format!("decoding decks: {}", e), + })?; let mut names = HashSet::new(); for (_id, deck) in decks { let oldname = deck.name().to_string(); diff --git a/rslib/src/storage/deckconfig/mod.rs b/rslib/src/storage/deckconfig/mod.rs index c7b31dc92..82fae17dd 100644 --- a/rslib/src/storage/deckconfig/mod.rs +++ b/rslib/src/storage/deckconfig/mod.rs @@ -104,9 +104,7 @@ impl SqliteStorage { &self, conf: &DeckConfig, ) -> Result<()> { - if conf.id.0 == 0 { - return Err(AnkiError::invalid_input("deck with id 0")); - } + require!(conf.id.0 != 0, "deck with id 0"); let mut conf_bytes = vec![]; conf.inner.encode(&mut conf_bytes)?; self.db @@ -176,7 +174,9 @@ impl SqliteStorage { let conf: Value = serde_json::from_str(text)?; serde_json::from_value(conf) }) - .map_err(|e| AnkiError::JsonError(format!("decoding deck config: {}", e))) + .map_err(|e| AnkiError::JsonError { + info: format!("decoding deck config: {}", e), + }) })?; for (id, mut conf) in conf.into_iter() { // buggy clients may have failed to set inner id to match hash key diff --git a/rslib/src/storage/graves/mod.rs b/rslib/src/storage/graves/mod.rs index a28d20f04..5d4db21c0 100644 --- a/rslib/src/storage/graves/mod.rs +++ b/rslib/src/storage/graves/mod.rs @@ -7,14 +7,7 @@ use num_enum::TryFromPrimitive; use rusqlite::params; use super::SqliteStorage; -use crate::{ - card::CardId, - decks::DeckId, - error::{AnkiError, Result}, - notes::NoteId, - sync::Graves, - types::Usn, -}; +use crate::{prelude::*, sync::Graves}; #[derive(TryFromPrimitive)] #[repr(u8)] @@ -63,8 +56,8 @@ impl SqliteStorage { let mut graves = Graves::default(); while let Some(row) = rows.next()? { let oid: i64 = row.get(0)?; - let kind = GraveKind::try_from(row.get::<_, u8>(1)?) - .map_err(|_| AnkiError::invalid_input("invalid grave kind"))?; + let kind = + GraveKind::try_from(row.get::<_, u8>(1)?).or_invalid("invalid grave kind")?; match kind { GraveKind::Card => graves.cards.push(CardId(oid)), GraveKind::Note => graves.notes.push(NoteId(oid)), diff --git a/rslib/src/storage/notetype/mod.rs b/rslib/src/storage/notetype/mod.rs index cd73ab0d4..fa2eca591 100644 --- a/rslib/src/storage/notetype/mod.rs +++ b/rslib/src/storage/notetype/mod.rs @@ -9,13 +9,12 @@ use unicase::UniCase; use super::{ids_to_string, SqliteStorage}; use crate::{ - error::{AnkiError, DbErrorKind, Result}, - notes::NoteId, + error::DbErrorKind, notetype::{ AlreadyGeneratedCardInfo, CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig, - Notetype, NotetypeConfig, NotetypeId, NotetypeSchema11, + NotetypeConfig, NotetypeSchema11, }, - timestamp::TimestampMillis, + prelude::*, }; fn row_to_notetype_core(row: &Row) -> Result { @@ -217,11 +216,7 @@ impl SqliteStorage { /// Notetype should have an existing id, and will be added if missing. fn update_notetype_core(&self, nt: &Notetype) -> Result<()> { - if nt.id.0 == 0 { - return Err(AnkiError::invalid_input( - "notetype with id 0 passed in as existing", - )); - } + require!(nt.id.0 != 0, "notetype with id 0 passed in as existing"); let mut stmt = self.db.prepare_cached(include_str!("add_or_update.sql"))?; let mut config_bytes = vec![]; nt.config.encode(&mut config_bytes)?; @@ -330,7 +325,9 @@ impl SqliteStorage { pub(crate) fn upgrade_notetypes_to_schema15(&self) -> Result<()> { let nts = self .get_schema11_notetypes() - .map_err(|e| AnkiError::JsonError(format!("decoding models: {}", e)))?; + .map_err(|e| AnkiError::JsonError { + info: format!("decoding models: {}", e), + })?; let mut names = HashSet::new(); for (mut ntid, nt) in nts { let mut nt = Notetype::from(nt); diff --git a/rslib/src/storage/sync.rs b/rslib/src/storage/sync.rs index c9bc17247..5d95cf45f 100644 --- a/rslib/src/storage/sync.rs +++ b/rslib/src/storage/sync.rs @@ -69,21 +69,12 @@ impl SqliteStorage { pub(crate) fn open_and_check_sqlite_file(path: &Path) -> Result { let db = Connection::open(path)?; match db.pragma_query_value(None, "integrity_check", |row| row.get::<_, String>(0)) { - Ok(s) => { - if s != "ok" { - return Err(AnkiError::invalid_input(format!("corrupt: {}", s))); - } - } + Ok(s) => require!(s == "ok", "corrupt: {s}"), Err(e) => return Err(e.into()), }; match db.pragma_query_value(None, "journal_mode", |row| row.get::<_, String>(0)) { - Ok(s) => { - if s == "delete" { - Ok(db) - } else { - Err(AnkiError::invalid_input(format!("corrupt: {}", s))) - } - } + Ok(s) if s == "delete" => Ok(db), + Ok(s) => invalid_input!("corrupt: {s}"), Err(e) => Err(e.into()), } } diff --git a/rslib/src/sync/http.rs b/rslib/src/sync/http.rs index 665d61abf..5b9b8a9cc 100644 --- a/rslib/src/sync/http.rs +++ b/rslib/src/sync/http.rs @@ -1,12 +1,12 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::{fs, path::PathBuf}; +use std::path::PathBuf; use serde::{Deserialize, Serialize}; use super::{Chunk, Graves, SanityCheckCounts, UnchunkedChanges}; -use crate::{pb::sync_server_method_request::Method, prelude::*}; +use crate::{io::read_file, pb::sync_server_method_request::Method, prelude::*}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub enum SyncRequest { @@ -44,7 +44,7 @@ impl SyncRequest { SyncRequest::Abort => ("abort", b"{}".to_vec()), SyncRequest::FullUpload(v) => { // fixme: stream in the data instead, in a different call - ("upload", fs::read(&v)?) + ("upload", read_file(&v)?) } SyncRequest::FullDownload => ("download", b"{}".to_vec()), }) diff --git a/rslib/src/sync/http_client.rs b/rslib/src/sync/http_client.rs index d835bdda6..099fbbc44 100644 --- a/rslib/src/sync/http_client.rs +++ b/rslib/src/sync/http_client.rs @@ -28,7 +28,13 @@ use super::{ Chunk, FullSyncProgress, Graves, SanityCheckCounts, SanityCheckResponse, SyncMeta, UnchunkedChanges, SYNC_VERSION_MAX, }; -use crate::{error::SyncErrorKind, notes::guid, prelude::*, version::sync_client_version}; +use crate::{ + error::SyncErrorKind, + io::{new_tempfile, new_tempfile_in}, + notes::guid, + prelude::*, + version::sync_client_version, +}; lazy_static! { // These limits are enforced server-side, but are made adjustable for users @@ -167,9 +173,9 @@ impl SyncServer for HttpSyncClient { col_folder: Option<&Path>, ) -> Result { let mut temp_file = if let Some(folder) = col_folder { - NamedTempFile::new_in(folder) + new_tempfile_in(folder) } else { - NamedTempFile::new() + new_tempfile() }?; let (size, mut stream) = self.download_inner().await?; let mut progress = FullSyncProgress { @@ -400,10 +406,12 @@ mod test { assert!(matches!( syncer.login("nosuchuser", "nosuchpass").await, - Err(AnkiError::SyncError(SyncError { - kind: SyncErrorKind::AuthFailed, - .. - })) + Err(AnkiError::SyncError { + source: SyncError { + kind: SyncErrorKind::AuthFailed, + .. + } + }) )); assert!(syncer.login(&username, &password).await.is_ok()); @@ -413,10 +421,12 @@ mod test { // aborting before a start is a conflict assert!(matches!( syncer.abort().await, - Err(AnkiError::SyncError(SyncError { - kind: SyncErrorKind::Conflict, - .. - })) + Err(AnkiError::SyncError { + source: SyncError { + kind: SyncErrorKind::Conflict, + .. + } + }) )); let _graves = syncer.start(Usn(1), true, None).await?; diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 23debee32..5a503659a 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -344,10 +344,13 @@ where self.col.storage.rollback_trx()?; let _ = self.remote.abort().await; - if let AnkiError::SyncError(SyncError { - info, - kind: SyncErrorKind::DatabaseCheckRequired, - }) = &e + if let AnkiError::SyncError { + source: + SyncError { + info, + kind: SyncErrorKind::DatabaseCheckRequired, + }, + } = &e { debug!(self.col.log, "sanity check failed:\n{}", info); } @@ -671,9 +674,7 @@ impl Collection { pub(crate) async fn full_download_inner(self, server: Box) -> Result<()> { let col_path = self.col_path.clone(); - let col_folder = col_path - .parent() - .ok_or_else(|| AnkiError::invalid_input("couldn't get col_folder"))?; + let col_folder = col_path.parent().or_invalid("couldn't get col_folder")?; self.close(None)?; let out_file = server.full_download(Some(col_folder)).await?; // check file ok @@ -964,7 +965,7 @@ impl Collection { let mut note: Note = entry.into(); let nt = self .get_notetype(note.notetype_id)? - .ok_or_else(|| AnkiError::invalid_input("note missing notetype"))?; + .or_invalid("note missing notetype")?; note.prepare_for_update(&nt, false)?; self.storage.add_or_update_note(¬e)?; } @@ -1576,14 +1577,14 @@ mod test { // use std::fs; // use tempfile::NamedTempFile; - // let client_col_file = NamedTempFile::new()?; + // let client_col_file = new_named_tempfile()?; // let client_col_name = client_col_file // .path() // .file_name() // .unwrap() // .to_string_lossy(); // fs::copy(client_fname, client_col_file.path())?; - // let server_col_file = NamedTempFile::new()?; + // let server_col_file = new_named_tempfile()?; // let server_col_name = server_col_file // .path() // .file_name() diff --git a/rslib/src/sync/server.rs b/rslib/src/sync/server.rs index 9a42f0ad4..789a07714 100644 --- a/rslib/src/sync/server.rs +++ b/rslib/src/sync/server.rs @@ -9,6 +9,7 @@ use tempfile::NamedTempFile; use super::ChunkableIds; use crate::{ collection::CollectionBuilder, + io::new_tempfile, prelude::*, storage::{open_and_check_sqlite_file, SchemaVersion}, sync::{ @@ -193,7 +194,7 @@ impl SyncServer for LocalServer { // create a copy if necessary let new_file: NamedTempFile; if !can_consume { - new_file = NamedTempFile::new()?; + new_file = new_tempfile()?; fs::copy(col_path, &new_file.path())?; col_path = new_file.path(); } @@ -224,7 +225,7 @@ impl SyncServer for LocalServer { self.col.close(Some(SchemaVersion::V11))?; // copy file and return path - let temp_file = NamedTempFile::new()?; + let temp_file = new_tempfile()?; fs::copy(&col_path, temp_file.path())?; Ok(temp_file) diff --git a/rslib/src/tags/findreplace.rs b/rslib/src/tags/findreplace.rs index 642936249..7f3dcac69 100644 --- a/rslib/src/tags/findreplace.rs +++ b/rslib/src/tags/findreplace.rs @@ -18,11 +18,10 @@ impl Collection { regex: bool, match_case: bool, ) -> Result> { - if replacement.contains(is_tag_separator) { - return Err(AnkiError::invalid_input( - "replacement name can not contain a space", - )); - } + require!( + !replacement.contains(is_tag_separator), + "replacement name cannot contain a space", + ); let mut search = if regex { Cow::from(search) diff --git a/rslib/src/tags/register.rs b/rslib/src/tags/register.rs index bb5cab145..3c576e70f 100644 --- a/rslib/src/tags/register.rs +++ b/rslib/src/tags/register.rs @@ -180,7 +180,7 @@ pub(super) fn normalize_tag_name(name: &str) -> Result> { }; if normalized_name.is_empty() { // this should not be possible - Err(AnkiError::invalid_input("blank tag")) + invalid_input!("blank tag"); } else { Ok(normalized_name) } diff --git a/rslib/src/tags/rename.rs b/rslib/src/tags/rename.rs index 253a62cd9..fed028c18 100644 --- a/rslib/src/tags/rename.rs +++ b/rslib/src/tags/rename.rs @@ -16,16 +16,14 @@ impl Collection { impl Collection { fn rename_tag_inner(&mut self, old_prefix: &str, new_prefix: &str) -> Result { - if new_prefix.contains(is_tag_separator) { - return Err(AnkiError::invalid_input( - "replacement name can not contain a space", - )); - } - if new_prefix.trim().is_empty() { - return Err(AnkiError::invalid_input( - "replacement name must not be empty", - )); - } + require!( + !new_prefix.contains(is_tag_separator), + "replacement name can not contain a space", + ); + require!( + !new_prefix.trim().is_empty(), + "replacement name must not be empty", + ); let usn = self.usn()?; diff --git a/rslib/src/tags/undo.rs b/rslib/src/tags/undo.rs index 554b42878..98f431da1 100644 --- a/rslib/src/tags/undo.rs +++ b/rslib/src/tags/undo.rs @@ -20,7 +20,7 @@ impl Collection { let current = self .storage .get_tag(&tag.name)? - .ok_or_else(|| AnkiError::invalid_input("tag disappeared"))?; + .or_invalid("tag disappeared")?; self.update_tag_undoable(&tag, current) } } diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 49f5cfb32..476c744b1 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -268,12 +268,12 @@ fn template_error_to_anki_error( }; let details = htmlescape::encode_minimal(&localized_template_error(tr, err)); let more_info = tr.card_template_rendering_more_info(); - let info = format!( + let source = format!( "{}
{}
{}", header, details, TEMPLATE_ERROR_LINK, more_info ); - AnkiError::TemplateError(info) + AnkiError::TemplateError { info: source } } fn localized_template_error(tr: &I18n, err: TemplateError) -> String { diff --git a/rslib/src/tests.rs b/rslib/src/tests.rs index 4c8cad417..630801c42 100644 --- a/rslib/src/tests.rs +++ b/rslib/src/tests.rs @@ -8,6 +8,7 @@ use tempfile::{tempdir, TempDir}; use crate::{ collection::{open_test_collection, CollectionBuilder}, deckconfig::UpdateDeckConfigsRequest, + io::create_dir, media::MediaManager, pb::deck_configs_for_update::current_deck::Limits, prelude::*, @@ -17,7 +18,7 @@ pub(crate) fn open_fs_test_collection(name: &str) -> (Collection, TempDir) { let tempdir = tempdir().unwrap(); let dir = tempdir.path(); let media_folder = dir.join(format!("{name}.media")); - std::fs::create_dir(&media_folder).unwrap(); + create_dir(&media_folder).unwrap(); let col = CollectionBuilder::new(dir.join(format!("{name}.anki2"))) .set_media_paths(media_folder, dir.join(format!("{name}.mdb"))) .build() diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index d61c0b3fb..79693bb25 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -34,7 +34,9 @@ impl TimestampSecs { #[cfg(windows)] pub(crate) fn local_datetime(self) -> Result> { std::panic::catch_unwind(|| Local.timestamp(self.0, 0)) - .map_err(|_err| AnkiError::invalid_input("invalid date")) + // discard error as it doesn't satisfiy trait bounds + .ok() + .or_invalid("invalid date") } #[cfg(not(windows))] diff --git a/rslib/src/undo/mod.rs b/rslib/src/undo/mod.rs index 86a1a0354..182d26102 100644 --- a/rslib/src/undo/mod.rs +++ b/rslib/src/undo/mod.rs @@ -149,7 +149,7 @@ impl UndoManager { } }) .next() - .ok_or_else(|| AnkiError::invalid_input("target undo op not found"))?; + .or_invalid("target undo op not found")?; let mut removed = vec![]; for _ in 0..target_idx { removed.push(self.undo_steps.pop_front().unwrap()); diff --git a/tools/bundle b/tools/bundle index 54f372103..ecbb169f8 100755 --- a/tools/bundle +++ b/tools/bundle @@ -27,8 +27,6 @@ if [[ "$OSTYPE" == "darwin"* ]]; then else bazel query @audio_mac_arm64//:* > /dev/null bazel query @pyqt6.4_mac_bundle_arm64//:* > /dev/null - bazel query @protobuf_wheel_mac_arm64//:* > /dev/null - fi else bazel query @pyqt515//:* > /dev/null