diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8f3caa2..7895480f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- **Breaking**: the codes the page reports through `odr.onError` and + `odr.onEditRefused` moved from 1 to 9 onto 1001 to 1009. `readOnly` is 1005, + not 5; the `reason` string is unchanged. + +- **Added**: `odr::ErrorCode` (`odr/error_code.hpp`), one number space every + binding reports for both a thrown exception and a refused edit. `ODRError` + casts from it, `OdrException.getCode()` and the wasm envelope's `code` carry + it, and `enumTables()` and the python module expose it. + - **Breaking**: `AnchorType` gains `none` as its first value, so every later ordinal shifts by one. `Frame::anchor_type()` answers it for a frame that does not exist, instead of `as_char`, which a real frame also answers. diff --git a/CMakeLists.txt b/CMakeLists.txt index a2e96342b..8e9909a8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ set(ODR_SOURCE_FILES "src/odr/document.cpp" "src/odr/document_element.cpp" "src/odr/document_path.cpp" + "src/odr/error_code.cpp" "src/odr/exceptions.cpp" "src/odr/file.cpp" "src/odr/filesystem.cpp" diff --git a/apple/AGENTS.md b/apple/AGENTS.md index 50392a050..94d06da7e 100644 --- a/apple/AGENTS.md +++ b/apple/AGENTS.md @@ -72,8 +72,10 @@ consumer, and a SwiftPM binary target gives the consumer no way to pass `guarded` where the caller gets an `NSError **`, `guarded_value` for a property, and `guarded_void` for a `void` method. Pick a fallback that keeps the caller sane — `YES` for a walker's `end`, so a `while (!end)` loop - terminates instead of spinning. The `NSError` code list mirrors - `jni/src/odr_jni.cpp::throw_java` — keep the two in step. + terminates instead of spinning. `ODRError` is the head of `odr::ErrorCode` + numbered the same, so `error_code()` is a cast and `ODRInternal.mm` + static_asserts it. Adding a case means adding the enumerator here too; a code + past the list reports `ODRErrorUnknown`. - **Elements carry their owner.** Most public C++ handles own a `shared_ptr`, so a wrapper holding one by value is self-sufficient and needs no keep-alive. `odr::Element` and `odr::HtmlView` are the exceptions: the first holds a bare diff --git a/apple/include/OdrCoreObjC/ODRError.h b/apple/include/OdrCoreObjC/ODRError.h index 367798e57..4930dc52b 100644 --- a/apple/include/OdrCoreObjC/ODRError.h +++ b/apple/include/OdrCoreObjC/ODRError.h @@ -5,10 +5,10 @@ NS_ASSUME_NONNULL_BEGIN /// Domain of every error this framework reports. extern NSErrorDomain const ODRErrorDomain; -/// The subset of `odr::Exception` the bindings tell apart, mirroring the one -/// `jni/src/odr_jni.cpp` maps to typed java exceptions. Anything else arrives -/// as `ODRErrorUnknown`; the C++ message is always the error's -/// `NSLocalizedDescriptionKey`, so nothing is lost by not having a code. +/// The head of `odr::ErrorCode`, numbered the same — `ODRInternal.mm` asserts +/// that and casts. A code past this list arrives as `ODRErrorUnknown`; the C++ +/// message is always the error's `NSLocalizedDescriptionKey`, so nothing is +/// lost by not having a case here. typedef NS_ERROR_ENUM(ODRErrorDomain, ODRError){ ODRErrorUnknown = 1, ODRErrorUnsupportedOperation = 2, diff --git a/apple/src/ODRInternal.mm b/apple/src/ODRInternal.mm index 17f3e58a2..3c75c5f6d 100644 --- a/apple/src/ODRInternal.mm +++ b/apple/src/ODRInternal.mm @@ -2,6 +2,8 @@ #import +#include + #include #include @@ -59,39 +61,38 @@ namespace { -/// The code for the exception being handled. Mirrors the mapping in -/// `jni/src/odr_jni.cpp::throw_java` — keep the two in step. +#define ODR_SAME_CODE(code, objc) \ + static_assert(static_cast(odr::ErrorCode::code) == objc, \ + "ODRError must stay odr::ErrorCode numbered the same") + +ODR_SAME_CODE(unknown, ODRErrorUnknown); +ODR_SAME_CODE(unsupported_operation, ODRErrorUnsupportedOperation); +ODR_SAME_CODE(file_not_found, ODRErrorFileNotFound); +ODR_SAME_CODE(unknown_file_type, ODRErrorUnknownFileType); +ODR_SAME_CODE(unsupported_file_type, ODRErrorUnsupportedFileType); +ODR_SAME_CODE(file_read_error, ODRErrorFileReadError); +ODR_SAME_CODE(file_write_error, ODRErrorFileWriteError); +ODR_SAME_CODE(no_document_file, ODRErrorNoDocumentFile); +ODR_SAME_CODE(unknown_document_type, ODRErrorUnknownDocumentType); +ODR_SAME_CODE(unsupported_crypto_algorithm, ODRErrorUnsupportedCryptoAlgorithm); +ODR_SAME_CODE(wrong_password, ODRErrorWrongPassword); +ODR_SAME_CODE(decryption_failed, ODRErrorDecryptionFailed); +ODR_SAME_CODE(not_encrypted, ODRErrorNotEncrypted); +ODR_SAME_CODE(file_encrypted, ODRErrorFileEncrypted); +ODR_SAME_CODE(document_copy_protected, ODRErrorDocumentCopyProtected); + +#undef ODR_SAME_CODE + +/// `ODRError` is `odr::ErrorCode` numbered the same, so this is a cast. A code +/// past the ones `ODRError` names reports `ODRErrorUnknown`. ODRError error_code() { try { throw; - } catch (const odr::UnsupportedOperation &) { - return ODRErrorUnsupportedOperation; - } catch (const odr::FileNotFound &) { - return ODRErrorFileNotFound; - } catch (const odr::UnknownFileType &) { - return ODRErrorUnknownFileType; - } catch (const odr::UnsupportedFileType &) { - return ODRErrorUnsupportedFileType; - } catch (const odr::FileReadError &) { - return ODRErrorFileReadError; - } catch (const odr::FileWriteError &) { - return ODRErrorFileWriteError; - } catch (const odr::NoDocumentFile &) { - return ODRErrorNoDocumentFile; - } catch (const odr::UnknownDocumentType &) { - return ODRErrorUnknownDocumentType; - } catch (const odr::UnsupportedCryptoAlgorithm &) { - return ODRErrorUnsupportedCryptoAlgorithm; - } catch (const odr::WrongPasswordError &) { - return ODRErrorWrongPassword; - } catch (const odr::DecryptionFailed &) { - return ODRErrorDecryptionFailed; - } catch (const odr::NotEncryptedError &) { - return ODRErrorNotEncrypted; - } catch (const odr::FileEncryptedError &) { - return ODRErrorFileEncrypted; - } catch (const odr::DocumentCopyProtectedException &) { - return ODRErrorDocumentCopyProtected; + } catch (const std::exception &e) { + const odr::ErrorCode code = odr::error_code(e); + return code > odr::ErrorCode::document_copy_protected + ? ODRErrorUnknown + : static_cast(code); } catch (...) { return ODRErrorUnknown; } diff --git a/docs/design/editing.md b/docs/design/editing.md index 5cda776e6..9ba7e0150 100644 --- a/docs/design/editing.md +++ b/docs/design/editing.md @@ -181,8 +181,10 @@ project, and hand-rolling gives full control over the model↔op mapping. runs or paragraphs. It owns: - the **mode** — `enable()`, `disable()`, `isEnabled()`, `isEditable()`; -- the **refusals** — the code table, the repeat suppression, the outline a - refused element gets, and `odr.onEditRefused`; +- the **refusals** — the repeat suppression, the outline a refused element + gets, and `odr.onEditRefused`. The codes are `odr::ErrorCode` and the + renderer writes them into the page as `odr.errorCodes`, so the script holds + the wording and not the numbers; - the **log** — `getOperations()`, `undo()`, `redo()`, `committed()`, and the `dirty` / `canUndo` / `canRedo` state `odr.onEditChange` reports; - the **keyboard classes** the page may take (decision 12). diff --git a/docs/design/spreadsheet-editing.md b/docs/design/spreadsheet-editing.md index 3c6540347..e8d55d8c6 100644 --- a/docs/design/spreadsheet-editing.md +++ b/docs/design/spreadsheet-editing.md @@ -237,11 +237,17 @@ is the same thing spelled for a reader of the log. We still ship an English `odr.onError` default does exactly this) and a desktop host with no catalogue can show it as it stands. -**Codes are appended, never renumbered**, and share one space with -`odr.onError`'s — `errorIllegalEditNewLine` holds 1. The rule the wasm enum -ordinals already live under: appending stays silent, reordering goes loud. -Pin them in `test/browser/sheet` the way `tests/enums.test.mjs` pins the -enums. +**The codes are `odr::ErrorCode`**, defined in `src/odr/error_code.hpp` and +written into the page by `html/frontend.cpp::write_error_codes`, so the scripts +restate no number. They share one space with `odr.onError`'s and with the code +every binding reports for a thrown `odr::Exception`: below 1000 an exception +names itself, and the refusals sit from 1001, `newLine` first. + +**Codes are appended, never renumbered.** The rule the wasm enum ordinals +already live under: appending stays silent, reordering goes loud. `odr_test` +pins both bands (`error_code_test.cpp`), `tests/enums.test.mjs` pins them on the +JS side, and `test/browser/sheet` reads the table `serve.py` builds from the +header rather than a copy. **One object argument, never positional.** `onError(code, message)` cannot grow a field without breaking every host that implements it; an object can. diff --git a/jni/AGENTS.md b/jni/AGENTS.md index b86f92dec..9482f7657 100644 --- a/jni/AGENTS.md +++ b/jni/AGENTS.md @@ -47,8 +47,11 @@ package `app.opendocument.core`. Mirrors the surface of the python bindings enum declaration; `-1` encodes an absent `std::optional`. - **Strings**: use `odr_jni::to_string`/`to_jstring` (real UTF-8 ↔ UTF-16), never JNI's modified-UTF-8 `GetStringUTFChars`. -- **Exceptions**: every native body runs inside `odr_jni::guarded`; C++ - exceptions map to `OdrException` subclasses (`odr_jni.cpp::throw_java`). +- **Exceptions**: every native body runs inside `odr_jni::guarded`. + `throw_java` names the `OdrException` subclass after `odr::ErrorCode`, so a + code with no class here arrives as the base and the mapping cannot drift. + Every one carries `getCode()`, the number the page and the other bindings + report too. - Mirror the C++ names. `Logger` is bound as a `NativeResource`; entry points that take one get an overload (e.g. `Odr.open(path, logger)`). - `ILogger` is implementable in Java. `jni_logger.cpp`'s `JavaLogger` holds a diff --git a/jni/java/app/opendocument/core/OdrException.java b/jni/java/app/opendocument/core/OdrException.java index 582f2ca69..430647620 100644 --- a/jni/java/app/opendocument/core/OdrException.java +++ b/jni/java/app/opendocument/core/OdrException.java @@ -1,15 +1,33 @@ package app.opendocument.core; /** - * Base class for exceptions thrown by the native library. The subclasses - * mirror the typed exceptions in {@code odr/exceptions.hpp}; native errors - * without a dedicated subclass are thrown as plain {@code OdrException}. + * Base class for exceptions thrown by the native library. The subclasses are + * named after {@code odr::ErrorCode}; a code with no subclass here arrives as + * plain {@code OdrException}. */ public class OdrException extends RuntimeException { private static final long serialVersionUID = 1L; + /** {@code odr::ErrorCode::unknown}. */ + private static final int UNKNOWN = 1; + + private final int code; + public OdrException(String message) { + this(message, UNKNOWN); + } + + public OdrException(String message, int code) { super(message); + this.code = code; + } + + /** + * The {@code odr::ErrorCode}, the same number the rendered page reports + * through {@code odr.onError} and {@code odr.onEditRefused}. + */ + public int getCode() { + return code; } public static final class UnsupportedOperation extends OdrException { @@ -18,6 +36,10 @@ public static final class UnsupportedOperation extends OdrException { public UnsupportedOperation(String message) { super(message); } + + public UnsupportedOperation(String message, int code) { + super(message, code); + } } public static final class FileNotFound extends OdrException { @@ -26,6 +48,10 @@ public static final class FileNotFound extends OdrException { public FileNotFound(String message) { super(message); } + + public FileNotFound(String message, int code) { + super(message, code); + } } public static final class UnknownFileType extends OdrException { @@ -34,6 +60,10 @@ public static final class UnknownFileType extends OdrException { public UnknownFileType(String message) { super(message); } + + public UnknownFileType(String message, int code) { + super(message, code); + } } public static final class UnsupportedFileType extends OdrException { @@ -42,6 +72,10 @@ public static final class UnsupportedFileType extends OdrException { public UnsupportedFileType(String message) { super(message); } + + public UnsupportedFileType(String message, int code) { + super(message, code); + } } public static final class FileReadError extends OdrException { @@ -50,6 +84,10 @@ public static final class FileReadError extends OdrException { public FileReadError(String message) { super(message); } + + public FileReadError(String message, int code) { + super(message, code); + } } public static final class FileWriteError extends OdrException { @@ -58,6 +96,10 @@ public static final class FileWriteError extends OdrException { public FileWriteError(String message) { super(message); } + + public FileWriteError(String message, int code) { + super(message, code); + } } public static final class NoDocumentFile extends OdrException { @@ -66,6 +108,10 @@ public static final class NoDocumentFile extends OdrException { public NoDocumentFile(String message) { super(message); } + + public NoDocumentFile(String message, int code) { + super(message, code); + } } public static final class UnknownDocumentType extends OdrException { @@ -74,6 +120,10 @@ public static final class UnknownDocumentType extends OdrException { public UnknownDocumentType(String message) { super(message); } + + public UnknownDocumentType(String message, int code) { + super(message, code); + } } public static final class UnsupportedCryptoAlgorithm extends OdrException { @@ -82,6 +132,10 @@ public static final class UnsupportedCryptoAlgorithm extends OdrException { public UnsupportedCryptoAlgorithm(String message) { super(message); } + + public UnsupportedCryptoAlgorithm(String message, int code) { + super(message, code); + } } public static final class WrongPassword extends OdrException { @@ -90,6 +144,10 @@ public static final class WrongPassword extends OdrException { public WrongPassword(String message) { super(message); } + + public WrongPassword(String message, int code) { + super(message, code); + } } public static final class DecryptionFailed extends OdrException { @@ -98,6 +156,10 @@ public static final class DecryptionFailed extends OdrException { public DecryptionFailed(String message) { super(message); } + + public DecryptionFailed(String message, int code) { + super(message, code); + } } public static final class NotEncrypted extends OdrException { @@ -106,6 +168,10 @@ public static final class NotEncrypted extends OdrException { public NotEncrypted(String message) { super(message); } + + public NotEncrypted(String message, int code) { + super(message, code); + } } public static final class FileEncrypted extends OdrException { @@ -114,6 +180,10 @@ public static final class FileEncrypted extends OdrException { public FileEncrypted(String message) { super(message); } + + public FileEncrypted(String message, int code) { + super(message, code); + } } public static final class DocumentCopyProtected extends OdrException { @@ -122,5 +192,9 @@ public static final class DocumentCopyProtected extends OdrException { public DocumentCopyProtected(String message) { super(message); } + + public DocumentCopyProtected(String message, int code) { + super(message, code); + } } } diff --git a/jni/src/odr_jni.cpp b/jni/src/odr_jni.cpp index 7377b00a0..c8f21350f 100644 --- a/jni/src/odr_jni.cpp +++ b/jni/src/odr_jni.cpp @@ -1,5 +1,6 @@ #include "odr_jni.hpp" +#include #include #include @@ -9,15 +10,6 @@ namespace odr_jni { namespace { -void throw_new(JNIEnv *env, const char *class_name, const char *message) { - jclass cls = env->FindClass(class_name); - if (cls == nullptr) { - return; // a NoClassDefFoundError is pending instead - } - env->ThrowNew(cls, message); - env->DeleteLocalRef(cls); -} - void append_utf8(std::string &out, const std::uint32_t code_point) { if (code_point < 0x80) { out.push_back(static_cast(code_point)); @@ -119,55 +111,52 @@ jbyteArray to_jbytes(JNIEnv *env, const std::string_view bytes) { return result; } +/// Throws @p class_name, constructed from `(String, int)`. False where the +/// class is absent. +bool throw_coded(JNIEnv *env, const char *class_name, const char *message, + const odr::ErrorCode code) { + jclass cls = env->FindClass(class_name); + if (cls == nullptr) { + env->ExceptionClear(); + return false; + } + const jmethodID constructor = + env->GetMethodID(cls, "", "(Ljava/lang/String;I)V"); + if (constructor == nullptr) { + env->ExceptionClear(); + env->DeleteLocalRef(cls); + return false; + } + jstring text = env->NewStringUTF(message); + auto throwable = static_cast( + env->NewObject(cls, constructor, text, static_cast(code))); + env->DeleteLocalRef(text); + env->DeleteLocalRef(cls); + if (throwable == nullptr) { + return false; // an OutOfMemoryError is pending instead + } + env->Throw(throwable); + env->DeleteLocalRef(throwable); + return true; +} + void throw_java(JNIEnv *env) { constexpr auto base = "app/opendocument/core/OdrException"; try { throw; - } catch (const odr::UnsupportedOperation &e) { - throw_new(env, "app/opendocument/core/OdrException$UnsupportedOperation", - e.what()); - } catch (const odr::FileNotFound &e) { - throw_new(env, "app/opendocument/core/OdrException$FileNotFound", e.what()); - } catch (const odr::UnknownFileType &e) { - throw_new(env, "app/opendocument/core/OdrException$UnknownFileType", - e.what()); - } catch (const odr::UnsupportedFileType &e) { - throw_new(env, "app/opendocument/core/OdrException$UnsupportedFileType", - e.what()); - } catch (const odr::FileReadError &e) { - throw_new(env, "app/opendocument/core/OdrException$FileReadError", - e.what()); - } catch (const odr::FileWriteError &e) { - throw_new(env, "app/opendocument/core/OdrException$FileWriteError", - e.what()); - } catch (const odr::NoDocumentFile &e) { - throw_new(env, "app/opendocument/core/OdrException$NoDocumentFile", - e.what()); - } catch (const odr::UnknownDocumentType &e) { - throw_new(env, "app/opendocument/core/OdrException$UnknownDocumentType", - e.what()); - } catch (const odr::UnsupportedCryptoAlgorithm &e) { - throw_new(env, - "app/opendocument/core/OdrException$UnsupportedCryptoAlgorithm", - e.what()); - } catch (const odr::WrongPasswordError &e) { - throw_new(env, "app/opendocument/core/OdrException$WrongPassword", - e.what()); - } catch (const odr::DecryptionFailed &e) { - throw_new(env, "app/opendocument/core/OdrException$DecryptionFailed", - e.what()); - } catch (const odr::NotEncryptedError &e) { - throw_new(env, "app/opendocument/core/OdrException$NotEncrypted", e.what()); - } catch (const odr::FileEncryptedError &e) { - throw_new(env, "app/opendocument/core/OdrException$FileEncrypted", - e.what()); - } catch (const odr::DocumentCopyProtectedException &e) { - throw_new(env, "app/opendocument/core/OdrException$DocumentCopyProtected", - e.what()); } catch (const std::exception &e) { - throw_new(env, base, e.what()); + const odr::ErrorCode code = odr::error_code(e); + // The nested class is named after the code; one with no class of its own + // falls back to the base. + const std::string name = + std::string(base) + "$" + std::string(odr::error_code_name(code)); + if (code != odr::ErrorCode::unknown && + throw_coded(env, name.c_str(), e.what(), code)) { + return; + } + throw_coded(env, base, e.what(), code); } catch (...) { - throw_new(env, base, "unknown native error"); + throw_coded(env, base, "unknown native error", odr::ErrorCode::unknown); } } diff --git a/python/src/bind_core.cpp b/python/src/bind_core.cpp index 07a5af955..d63bcee93 100644 --- a/python/src/bind_core.cpp +++ b/python/src/bind_core.cpp @@ -1,5 +1,6 @@ #include "bindings.hpp" +#include #include #include #include @@ -21,6 +22,12 @@ void odr_python::bind_core(py::module_ &m) { m.def("identify", &odr::identify, "Identification string of the underlying odrcore library."); + py::enum_ error_code(m, "ErrorCode"); + for (const odr::ErrorCode code : odr::all_error_codes()) { + error_code.value(std::string(odr::error_code_name(code)).c_str(), code); + } + // No `export_values`: `range` and its neighbours would land in the module. + // Mirrors odr::Exception, so `except odr.Error` catches the whole library. // `register_exception`, not `py::exception`: the latter has no translator. // Registered first so it is tried last - pybind11 reverses that order. diff --git a/src/odr/error_code.cpp b/src/odr/error_code.cpp new file mode 100644 index 000000000..6bcf736cf --- /dev/null +++ b/src/odr/error_code.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include +#include + +namespace { + +struct Row final { + odr::ErrorCode code; + std::string_view name; +}; + +using odr::ErrorCode; + +constexpr std::array rows{{ + {ErrorCode::unknown, "Unknown"}, + {ErrorCode::unsupported_operation, "UnsupportedOperation"}, + {ErrorCode::file_not_found, "FileNotFound"}, + {ErrorCode::unknown_file_type, "UnknownFileType"}, + {ErrorCode::unsupported_file_type, "UnsupportedFileType"}, + {ErrorCode::file_read_error, "FileReadError"}, + {ErrorCode::file_write_error, "FileWriteError"}, + {ErrorCode::no_document_file, "NoDocumentFile"}, + {ErrorCode::unknown_document_type, "UnknownDocumentType"}, + {ErrorCode::unsupported_crypto_algorithm, "UnsupportedCryptoAlgorithm"}, + {ErrorCode::wrong_password, "WrongPassword"}, + {ErrorCode::decryption_failed, "DecryptionFailed"}, + {ErrorCode::not_encrypted, "NotEncrypted"}, + {ErrorCode::file_encrypted, "FileEncrypted"}, + {ErrorCode::document_copy_protected, "DocumentCopyProtected"}, + {ErrorCode::unsupported_text_encoding, "UnsupportedTextEncoding"}, + {ErrorCode::no_zip_file, "NoZipFile"}, + {ErrorCode::zip_save_error, "ZipSaveError"}, + {ErrorCode::cfb_error, "CfbError"}, + {ErrorCode::no_cfb_file, "NoCfbFile"}, + {ErrorCode::cfb_file_corrupted, "CfbFileCorrupted"}, + {ErrorCode::no_text_file, "NoTextFile"}, + {ErrorCode::no_csv_file, "NoCsvFile"}, + {ErrorCode::no_markdown_file, "NoMarkdownFile"}, + {ErrorCode::no_json_file, "NoJsonFile"}, + {ErrorCode::no_image_file, "NoImageFile"}, + {ErrorCode::no_archive_file, "NoArchiveFile"}, + {ErrorCode::no_open_document_file, "NoOpenDocumentFile"}, + {ErrorCode::no_office_open_xml_file, "NoOfficeOpenXmlFile"}, + {ErrorCode::no_pdf_file, "NoPdfFile"}, + {ErrorCode::no_font_file, "NoFontFile"}, + {ErrorCode::no_legacy_microsoft_file, "NoLegacyMicrosoftFile"}, + {ErrorCode::no_iwork_file, "NoIworkFile"}, + {ErrorCode::no_xml_file, "NoXmlFile"}, + {ErrorCode::no_svg_file, "NoSvgFile"}, + {ErrorCode::no_rtf_file, "NoRtfFile"}, + {ErrorCode::no_svm_file, "NoSvmFile"}, + {ErrorCode::malformed_svm_file, "MalformedSvmFile"}, + {ErrorCode::unsupported_endian, "UnsupportedEndian"}, + {ErrorCode::ms_unsupported_crypto_algorithm, + "MsUnsupportedCryptoAlgorithm"}, + {ErrorCode::value_not_stated, "ValueNotStated"}, + {ErrorCode::invalid_prefix, "InvalidPrefix"}, + {ErrorCode::resource_not_accessible, "ResourceNotAccessible"}, + {ErrorCode::prefix_in_use, "PrefixInUse"}, + {ErrorCode::server_bind_failed, "ServerBindFailed"}, + {ErrorCode::server_already_bound, "ServerAlreadyBound"}, + {ErrorCode::server_not_bound, "ServerNotBound"}, + {ErrorCode::unsupported_option, "UnsupportedOption"}, + {ErrorCode::null_pointer_error, "NullPointerError"}, + {ErrorCode::invalid_path, "InvalidPath"}, + {ErrorCode::unsupported_file_encoding, "UnsupportedFileEncoding"}, + {ErrorCode::unauthenticated_read_error, "UnauthenticatedReadError"}, + + {ErrorCode::edit_new_line, "newLine"}, + {ErrorCode::edit_formula, "formula"}, + {ErrorCode::edit_rich, "rich"}, + {ErrorCode::edit_shapes, "shapes"}, + {ErrorCode::edit_read_only, "readOnly"}, + {ErrorCode::edit_formula_input, "formulaInput"}, + {ErrorCode::edit_unsupported, "unsupportedEdit"}, + {ErrorCode::edit_range, "range"}, + {ErrorCode::edit_unnameable, "unnameableEdit"}, +}}; + +} // namespace + +std::string_view odr::error_code_name(const ErrorCode code) noexcept { + const auto row = std::ranges::find(rows, code, &Row::code); + return row == rows.end() ? std::string_view{"Unknown"} : row->name; +} + +std::vector odr::all_error_codes() { + std::vector result; + result.reserve(rows.size()); + std::ranges::transform(rows, std::back_inserter(result), &Row::code); + return result; +} diff --git a/src/odr/error_code.hpp b/src/odr/error_code.hpp new file mode 100644 index 000000000..e62184080 --- /dev/null +++ b/src/odr/error_code.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +namespace odr { + +/// @brief What went wrong, as one number every binding reports. +/// +/// Below 1000 an @ref Exception names itself, 1 to 15 in the order `ODRError` +/// shipped. From 1000 the rendered page names an edit it refused, which +/// nothing throws. **Appended, never renumbered.** Every code needs a row in +/// `error_code.cpp`. +enum class ErrorCode : std::int32_t { + unknown = 1, ///< Also any `std::exception` that is not an @ref Exception. + unsupported_operation = 2, + file_not_found = 3, + unknown_file_type = 4, + unsupported_file_type = 5, + file_read_error = 6, + file_write_error = 7, + no_document_file = 8, + unknown_document_type = 9, + unsupported_crypto_algorithm = 10, + wrong_password = 11, + decryption_failed = 12, + not_encrypted = 13, + file_encrypted = 14, + document_copy_protected = 15, + + unsupported_text_encoding = 16, + no_zip_file = 17, + zip_save_error = 18, + cfb_error = 19, + no_cfb_file = 20, + cfb_file_corrupted = 21, + no_text_file = 22, + no_csv_file = 23, + no_markdown_file = 24, + no_json_file = 25, + no_image_file = 26, + no_archive_file = 27, + no_open_document_file = 28, + no_office_open_xml_file = 29, + no_pdf_file = 30, + no_font_file = 31, + no_legacy_microsoft_file = 32, + no_iwork_file = 33, + no_xml_file = 34, + no_svg_file = 35, + no_rtf_file = 36, + no_svm_file = 37, + malformed_svm_file = 38, + unsupported_endian = 39, + ms_unsupported_crypto_algorithm = 40, + value_not_stated = 41, + invalid_prefix = 42, + resource_not_accessible = 43, + prefix_in_use = 44, + server_bind_failed = 45, + server_already_bound = 46, + server_not_bound = 47, + unsupported_option = 48, + null_pointer_error = 49, + invalid_path = 50, + unsupported_file_encoding = 51, + unauthenticated_read_error = 52, + + /// The editing scripts report these through `odr.onEditRefused` and + /// `odr.onError`. + edit_new_line = 1001, ///< A line break inside a paragraph. + edit_formula = 1002, + edit_rich = 1003, ///< The cell holds more than one plain run. + edit_shapes = 1004, + edit_read_only = 1005, + edit_formula_input = 1006, + edit_unsupported = 1007, + edit_range = 1008, ///< The range reaches over a picture or a table. + edit_unnameable = 1009, ///< The edit landed where no operation names it. +}; + +/// @brief The code's name, as the bindings already spell it. +/// +/// An identifier, not a message: nothing here is localised, so a host maps the +/// code to its own wording. +[[nodiscard]] std::string_view error_code_name(ErrorCode code) noexcept; + +/// Every code, in declaration order. +[[nodiscard]] std::vector all_error_codes(); + +} // namespace odr diff --git a/src/odr/exceptions.cpp b/src/odr/exceptions.cpp index 8b27a7693..23c0b7ce3 100644 --- a/src/odr/exceptions.cpp +++ b/src/odr/exceptions.cpp @@ -6,143 +6,154 @@ namespace odr { UnsupportedOperation::UnsupportedOperation() - : Exception("unsupported operation") {} + : CodedException("unsupported operation") {} UnsupportedOperation::UnsupportedOperation(const std::string &message) - : Exception("unsupported operation: " + message) {} + : CodedException("unsupported operation: " + message) {} -FileNotFound::FileNotFound() : Exception("file not found") {} +FileNotFound::FileNotFound() : CodedException("file not found") {} FileNotFound::FileNotFound(const std::string &path) - : Exception("file not found: " + path) {} + : CodedException("file not found: " + path) {} -UnknownFileType::UnknownFileType() : Exception("unknown file type") {} +UnknownFileType::UnknownFileType() : CodedException("unknown file type") {} UnsupportedFileType::UnsupportedFileType(const FileType file_type) - : Exception("unsupported file type: " + file_type_to_string(file_type)), + : CodedException("unsupported file type: " + + file_type_to_string(file_type)), file_type{file_type} {} UnsupportedTextEncoding::UnsupportedTextEncoding( const TextEncoding text_encoding) - : Exception("unsupported text encoding"), text_encoding{text_encoding} {} + : CodedException("unsupported text encoding"), + text_encoding{text_encoding} {} -FileReadError::FileReadError() : Exception("file read error") {} +FileReadError::FileReadError() : CodedException("file read error") {} FileWriteError::FileWriteError(const std::string &path) - : Exception("file write error: " + path) {} + : CodedException("file write error: " + path) {} -NoZipFile::NoZipFile() : Exception("not a zip file") {} +NoZipFile::NoZipFile() : CodedException("not a zip file") {} -ZipSaveError::ZipSaveError() : Exception("zip save error") {} +ZipSaveError::ZipSaveError() : CodedException("zip save error") {} -CfbError::CfbError(const std::string &desc) : Exception(desc) {} +CfbError::CfbError(const std::string &desc) : CodedException(desc) {} NoCfbFile::NoCfbFile() : CfbError("no cfb file") {} CfbFileCorrupted::CfbFileCorrupted() : CfbError("cfb file corrupted") {} -NoTextFile::NoTextFile() : Exception("not a text file") {} +NoTextFile::NoTextFile() : CodedException("not a text file") {} -NoCsvFile::NoCsvFile() : Exception("not a csv file") {} +NoCsvFile::NoCsvFile() : CodedException("not a csv file") {} -NoMarkdownFile::NoMarkdownFile() : Exception("not a markdown file") {} +NoMarkdownFile::NoMarkdownFile() : CodedException("not a markdown file") {} -NoJsonFile::NoJsonFile() : Exception("not a json file") {} +NoJsonFile::NoJsonFile() : CodedException("not a json file") {} -NoImageFile::NoImageFile() : Exception("not an image file") {} +NoImageFile::NoImageFile() : CodedException("not an image file") {} -NoArchiveFile::NoArchiveFile() : Exception("not an archive file") {} +NoArchiveFile::NoArchiveFile() : CodedException("not an archive file") {} -NoDocumentFile::NoDocumentFile() : Exception("not a document file") {} +NoDocumentFile::NoDocumentFile() : CodedException("not a document file") {} NoOpenDocumentFile::NoOpenDocumentFile() - : Exception("not an open document file") {} + : CodedException("not an open document file") {} NoOfficeOpenXmlFile::NoOfficeOpenXmlFile() - : Exception("not an office open xml file") {} + : CodedException("not an office open xml file") {} -NoPdfFile::NoPdfFile() : Exception("not a pdf file") {} +NoPdfFile::NoPdfFile() : CodedException("not a pdf file") {} -NoFontFile::NoFontFile() : Exception("not a font file") {} +NoFontFile::NoFontFile() : CodedException("not a font file") {} NoLegacyMicrosoftFile::NoLegacyMicrosoftFile() - : Exception("not a legacy microsoft office file") {} + : CodedException("not a legacy microsoft office file") {} -NoIworkFile::NoIworkFile() : Exception("not an iwork file") {} +NoIworkFile::NoIworkFile() : CodedException("not an iwork file") {} -NoXmlFile::NoXmlFile() : Exception("not an xml file") {} +NoXmlFile::NoXmlFile() : CodedException("not an xml file") {} -NoSvgFile::NoSvgFile() : Exception("not an svg file") {} +NoSvgFile::NoSvgFile() : CodedException("not an svg file") {} -NoRtfFile::NoRtfFile() : Exception("not an rtf file") {} +NoRtfFile::NoRtfFile() : CodedException("not an rtf file") {} UnsupportedCryptoAlgorithm::UnsupportedCryptoAlgorithm() - : Exception("unsupported crypto algorithm") {} + : CodedException("unsupported crypto algorithm") {} -NoSvmFile::NoSvmFile() : Exception("not a svm file") {} +NoSvmFile::NoSvmFile() : CodedException("not a svm file") {} -MalformedSvmFile::MalformedSvmFile() : Exception("malformed svm file") {} +MalformedSvmFile::MalformedSvmFile() : CodedException("malformed svm file") {} -UnsupportedEndian::UnsupportedEndian() : Exception("unsupported endian") {} +UnsupportedEndian::UnsupportedEndian() : CodedException("unsupported endian") {} MsUnsupportedCryptoAlgorithm::MsUnsupportedCryptoAlgorithm() - : Exception("unsupported crypto algorithm") {} + : CodedException("unsupported crypto algorithm") {} UnknownDocumentType::UnknownDocumentType() - : Exception("unknown document type") {} + : CodedException("unknown document type") {} -ValueNotStated::ValueNotStated() : Exception("value not stated") {} +ValueNotStated::ValueNotStated() : CodedException("value not stated") {} -InvalidPrefix::InvalidPrefix() : Exception("invalid prefix string") {} +InvalidPrefix::InvalidPrefix() : CodedException("invalid prefix string") {} InvalidPrefix::InvalidPrefix(const std::string &prefix) - : Exception("invalid prefix string: " + prefix) {} + : CodedException("invalid prefix string: " + prefix) {} DocumentCopyProtectedException::DocumentCopyProtectedException() - : Exception("document copy protection") {} + : CodedException("document copy protection") {} ResourceNotAccessible::ResourceNotAccessible() - : Exception("resource not accessible") {} + : CodedException("resource not accessible") {} ResourceNotAccessible::ResourceNotAccessible(const std::string &name, const std::string &path) - : Exception("resource not accessible: " + name + " at " + path) {} + : CodedException("resource not accessible: " + name + " at " + path) {} -PrefixInUse::PrefixInUse() : Exception("prefix in use") {} +PrefixInUse::PrefixInUse() : CodedException("prefix in use") {} PrefixInUse::PrefixInUse(const std::string &prefix) - : Exception("prefix in use: " + prefix) {} + : CodedException("prefix in use: " + prefix) {} ServerBindFailed::ServerBindFailed(const std::string &host, const std::uint32_t port) - : Exception("server bind failed: " + host + ":" + std::to_string(port)) {} + : CodedException("server bind failed: " + host + ":" + + std::to_string(port)) {} ServerAlreadyBound::ServerAlreadyBound() - : Exception("server is bound already") {} + : CodedException("server is bound already") {} -ServerNotBound::ServerNotBound() : Exception("server is not bound") {} +ServerNotBound::ServerNotBound() : CodedException("server is not bound") {} UnsupportedOption::UnsupportedOption(const std::string &message) - : Exception("unsupported option: " + message) {} + : CodedException("unsupported option: " + message) {} NullPointerError::NullPointerError(const std::string &variable) - : Exception("null pointer error: " + variable) {} + : CodedException("null pointer error: " + variable) {} -WrongPasswordError::WrongPasswordError() : Exception("wrong password error") {} +WrongPasswordError::WrongPasswordError() + : CodedException("wrong password error") {} -DecryptionFailed::DecryptionFailed() : Exception("decryption failed") {} +DecryptionFailed::DecryptionFailed() : CodedException("decryption failed") {} -NotEncryptedError::NotEncryptedError() : Exception("not encrypted error") {} +NotEncryptedError::NotEncryptedError() + : CodedException("not encrypted error") {} InvalidPath::InvalidPath(const std::string &message) - : Exception("invalid path: " + message) {} + : CodedException("invalid path: " + message) {} UnsupportedFileEncoding::UnsupportedFileEncoding(const std::string &message) - : Exception("unsupported file encoding: " + message) {} + : CodedException("unsupported file encoding: " + message) {} -FileEncryptedError::FileEncryptedError() : Exception("file encrypted error") {} +FileEncryptedError::FileEncryptedError() + : CodedException("file encrypted error") {} UnauthenticatedReadError::UnauthenticatedReadError() - : Exception("cannot read encrypted object without authentication") {} + : CodedException("cannot read encrypted object without authentication") {} } // namespace odr + +odr::ErrorCode odr::error_code(const std::exception &exception) noexcept { + const auto *coded = dynamic_cast(&exception); + return coded == nullptr ? ErrorCode::unknown : coded->code(); +} diff --git a/src/odr/exceptions.hpp b/src/odr/exceptions.hpp index 58064b1da..4b5a1a91f 100644 --- a/src/odr/exceptions.hpp +++ b/src/odr/exceptions.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include +#include #include namespace odr { @@ -12,270 +15,308 @@ enum class TextEncoding; /// that remains the widest net. struct Exception : std::runtime_error { using std::runtime_error::runtime_error; + + /// What a binding reports. @ref ErrorCode::unknown where a type states none. + [[nodiscard]] virtual ErrorCode code() const noexcept { + return ErrorCode::unknown; + } +}; + +/// Base for an exception type whose code is @p C. +template struct CodedException : Exception { + using Exception::Exception; + + [[nodiscard]] ErrorCode code() const noexcept override { return C; } }; +/// @ref ErrorCode of @p exception, @ref ErrorCode::unknown for anything that is +/// not an @ref Exception. +[[nodiscard]] ErrorCode error_code(const std::exception &exception) noexcept; + /// Unsupported operation exception -struct UnsupportedOperation final : Exception { +struct UnsupportedOperation final + : CodedException { UnsupportedOperation(); explicit UnsupportedOperation(const std::string &message); }; /// File not found exception -struct FileNotFound final : Exception { +struct FileNotFound final : CodedException { FileNotFound(); explicit FileNotFound(const std::string &path); }; /// Unknown file type exception -struct UnknownFileType final : Exception { +struct UnknownFileType final : CodedException { UnknownFileType(); }; /// Unsupported file type exception -struct UnsupportedFileType final : Exception { +struct UnsupportedFileType final + : CodedException { FileType file_type; explicit UnsupportedFileType(FileType file_type); }; /// Unsupported text encoding exception -struct UnsupportedTextEncoding final : Exception { +struct UnsupportedTextEncoding final + : CodedException { TextEncoding text_encoding; explicit UnsupportedTextEncoding(TextEncoding text_encoding); }; /// File read error -struct FileReadError final : Exception { +struct FileReadError final : CodedException { FileReadError(); }; /// File write error -struct FileWriteError final : Exception { +struct FileWriteError final : CodedException { explicit FileWriteError(const std::string &path); }; /// No ZIP file exception base -struct NoZipFile final : Exception { +struct NoZipFile final : CodedException { NoZipFile(); }; /// ZIP save error base; `internal::zip::MinizSaveError` refines it. -struct ZipSaveError : Exception { +struct ZipSaveError : CodedException { ZipSaveError(); }; /// CFB error base; NoCfbFile and CfbFileCorrupted refine it. -struct CfbError : Exception { +struct CfbError : CodedException { explicit CfbError(const std::string &desc); }; /// No CFB file exception base struct NoCfbFile final : CfbError { NoCfbFile(); + + [[nodiscard]] ErrorCode code() const noexcept override { + return ErrorCode::no_cfb_file; + } }; /// CFB file corrupted exception base struct CfbFileCorrupted final : CfbError { CfbFileCorrupted(); + + [[nodiscard]] ErrorCode code() const noexcept override { + return ErrorCode::cfb_file_corrupted; + } }; /// No text file exception -struct NoTextFile final : Exception { +struct NoTextFile final : CodedException { NoTextFile(); }; /// No csv file exception -struct NoCsvFile final : Exception { +struct NoCsvFile final : CodedException { NoCsvFile(); }; /// No markdown file exception -struct NoMarkdownFile final : Exception { +struct NoMarkdownFile final : CodedException { NoMarkdownFile(); }; /// No json file exception -struct NoJsonFile final : Exception { +struct NoJsonFile final : CodedException { NoJsonFile(); }; /// No image file exception -struct NoImageFile final : Exception { +struct NoImageFile final : CodedException { NoImageFile(); }; /// No archive file exception -struct NoArchiveFile final : Exception { +struct NoArchiveFile final : CodedException { NoArchiveFile(); }; /// No document file exception -struct NoDocumentFile final : Exception { +struct NoDocumentFile final : CodedException { NoDocumentFile(); }; /// No open document file exception -struct NoOpenDocumentFile final : Exception { +struct NoOpenDocumentFile final + : CodedException { NoOpenDocumentFile(); }; /// No office open document file exception -struct NoOfficeOpenXmlFile final : Exception { +struct NoOfficeOpenXmlFile final + : CodedException { NoOfficeOpenXmlFile(); }; /// No PDF file exception -struct NoPdfFile final : Exception { +struct NoPdfFile final : CodedException { NoPdfFile(); }; /// No font file exception -struct NoFontFile final : Exception { +struct NoFontFile final : CodedException { NoFontFile(); }; /// No legacy Microsoft Office file -struct NoLegacyMicrosoftFile final : Exception { +struct NoLegacyMicrosoftFile final + : CodedException { NoLegacyMicrosoftFile(); }; /// No iWork file exception -struct NoIworkFile final : Exception { +struct NoIworkFile final : CodedException { NoIworkFile(); }; /// No XML file exception -struct NoXmlFile final : Exception { +struct NoXmlFile final : CodedException { NoXmlFile(); }; /// No SVG file exception -struct NoSvgFile final : Exception { +struct NoSvgFile final : CodedException { NoSvgFile(); }; /// No RTF file exception -struct NoRtfFile final : Exception { +struct NoRtfFile final : CodedException { NoRtfFile(); }; /// Unsupported crypto algorithm exception -struct UnsupportedCryptoAlgorithm final : Exception { +struct UnsupportedCryptoAlgorithm final + : CodedException { UnsupportedCryptoAlgorithm(); }; /// No SVM file exception base -struct NoSvmFile final : Exception { +struct NoSvmFile final : CodedException { NoSvmFile(); }; /// Malformed SVM file exception base -struct MalformedSvmFile final : Exception { +struct MalformedSvmFile final : CodedException { MalformedSvmFile(); }; /// Unsupported endian exception -struct UnsupportedEndian final : Exception { +struct UnsupportedEndian final : CodedException { UnsupportedEndian(); }; /// Unsupported MS crypto algorithm exception -struct MsUnsupportedCryptoAlgorithm final : Exception { +struct MsUnsupportedCryptoAlgorithm final + : CodedException { MsUnsupportedCryptoAlgorithm(); }; /// Unknown document type exception -struct UnknownDocumentType final : Exception { +struct UnknownDocumentType final + : CodedException { UnknownDocumentType(); }; /// A value asked of something that states none, e.g. `CellValue::number` on a /// cell holding a string -struct ValueNotStated final : Exception { +struct ValueNotStated final : CodedException { ValueNotStated(); }; /// Invalid prefix string -struct InvalidPrefix final : Exception { +struct InvalidPrefix final : CodedException { InvalidPrefix(); explicit InvalidPrefix(const std::string &prefix); }; /// Document copy protected exception -struct DocumentCopyProtectedException final : Exception { +struct DocumentCopyProtectedException final + : CodedException { DocumentCopyProtectedException(); }; /// Resource is not accessible -struct ResourceNotAccessible final : Exception { +struct ResourceNotAccessible final + : CodedException { ResourceNotAccessible(); ResourceNotAccessible(const std::string &name, const std::string &path); }; /// Prefix already in use -struct PrefixInUse final : Exception { +struct PrefixInUse final : CodedException { PrefixInUse(); explicit PrefixInUse(const std::string &prefix); }; /// HTTP server socket could not be bound -struct ServerBindFailed final : Exception { +struct ServerBindFailed final : CodedException { ServerBindFailed(const std::string &host, std::uint32_t port); }; /// HTTP server is bound already -struct ServerAlreadyBound final : Exception { +struct ServerAlreadyBound final + : CodedException { ServerAlreadyBound(); }; /// HTTP server has not been bound -struct ServerNotBound final : Exception { +struct ServerNotBound final : CodedException { ServerNotBound(); }; /// Unsupported option -struct UnsupportedOption final : Exception { +struct UnsupportedOption final : CodedException { explicit UnsupportedOption(const std::string &message); }; /// Null pointer error -struct NullPointerError final : Exception { +struct NullPointerError final : CodedException { explicit NullPointerError(const std::string &variable); }; /// Wrong password error -struct WrongPasswordError final : Exception { +struct WrongPasswordError final : CodedException { explicit WrongPasswordError(); }; /// Decryption failed -struct DecryptionFailed final : Exception { +struct DecryptionFailed final : CodedException { explicit DecryptionFailed(); }; /// Not encrypted error -struct NotEncryptedError final : Exception { +struct NotEncryptedError final : CodedException { explicit NotEncryptedError(); }; /// Invalid path -struct InvalidPath final : Exception { +struct InvalidPath final : CodedException { explicit InvalidPath(const std::string &message); }; /// Unsupported file encoding -struct UnsupportedFileEncoding final : Exception { +struct UnsupportedFileEncoding final + : CodedException { explicit UnsupportedFileEncoding(const std::string &message); }; /// File is encrypted -struct FileEncryptedError final : Exception { +struct FileEncryptedError final : CodedException { explicit FileEncryptedError(); }; /// Read attempted on an encrypted file that has not been authenticated -struct UnauthenticatedReadError final : Exception { +struct UnauthenticatedReadError final + : CodedException { explicit UnauthenticatedReadError(); }; diff --git a/src/odr/internal/html/frontend.cpp b/src/odr/internal/html/frontend.cpp index 49ef12a7f..4c4d383b1 100644 --- a/src/odr/internal/html/frontend.cpp +++ b/src/odr/internal/html/frontend.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -10,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -178,6 +181,27 @@ void write_dark_style(const Asset &asset, const WritingState &state) { } } +/// The editing band of @ref odr::ErrorCode. Always inline, even where the +/// config links the scripts: it is per-render data, not an asset. +void write_error_codes(const WritingState &state) { + state.out().write_script_begin(); + + std::ostream &out = state.out().out(); + out << "\nwindow.odr = window.odr || {};\nwindow.odr.errorCodes = {"; + bool first = true; + for (const ErrorCode code : all_error_codes()) { + if (code < ErrorCode::edit_new_line) { + continue; + } + out << (first ? "\n" : ",\n") << " \"" << error_code_name(code) + << "\": " << static_cast(code); + first = false; + } + out << "\n};\n"; + + state.out().write_script_end(); +} + void write_script(const Asset &asset, const WritingState &state) { if (const HtmlResourceLocation location = locate(asset, state.config(), state.resources()); @@ -260,6 +284,7 @@ void html::write_search_dark_style(const WritingState &state) { } void html::write_editing_script(const WritingState &state) { + write_error_codes(state); write_script(editing_js_asset, state); } diff --git a/src/odr/internal/html/frontend/document.js b/src/odr/internal/html/frontend/document.js index 410dac885..da333cd4f 100644 --- a/src/odr/internal/html/frontend/document.js +++ b/src/odr/internal/html/frontend/document.js @@ -832,7 +832,7 @@ : runOf(selection.getRangeAt(0).startContainer); var target = landed !== null ? landed : run; if (target === null) { - odr.onError(9, "an edit landed where no operation can name it"); + odr.onError(odr.errorCodes.unnameableEdit, "an edit landed where no operation can name it"); return; } // whatever the browser built inside the run, its text is the operation diff --git a/src/odr/internal/html/frontend/editing.js b/src/odr/internal/html/frontend/editing.js index 66f62b975..2c718e51e 100644 --- a/src/odr/internal/html/frontend/editing.js +++ b/src/odr/internal/html/frontend/editing.js @@ -15,21 +15,30 @@ var editors = []; var lastRefusal = null; - // One space of codes, appended and never renumbered - `odr.onError` shares - // it, and holds 9. The host maps the code; the message is for a console, and - // says what the code means today rather than what it meant when it was - // added. - var refusals = { - newLine: { code: 1, message: "a line break inside a paragraph is not supported" }, - formula: { code: 2, message: "cell holds a formula" }, - rich: { code: 3, message: "cell holds more than one plain run" }, - shapes: { code: 4, message: "cell holds a drawing" }, - readOnly: { code: 5, message: "document cannot be edited" }, - formulaInput: { code: 6, message: "typing a formula is not supported" }, - unsupportedEdit: { code: 7, message: "this kind of edit is not supported" }, - range: { code: 8, message: "an edit cannot reach over a picture or a table" }, + // The codes are `odr::ErrorCode`, written into the page ahead of this + // script. The message stays here: it is for a console, and nothing in this + // library is localised. + var codes = (odr.errorCodes = odr.errorCodes || {}); + var messages = { + newLine: "a line break inside a paragraph is not supported", + formula: "cell holds a formula", + rich: "cell holds more than one plain run", + shapes: "cell holds a drawing", + readOnly: "document cannot be edited", + formulaInput: "typing a formula is not supported", + unsupportedEdit: "this kind of edit is not supported", + range: "an edit cannot reach over a picture or a table", + unnameableEdit: "an edit landed where no operation can name it", }; + /// Falls back to `readOnly` for a reason no script here states. + function refusal(reason) { + var known = Object.prototype.hasOwnProperty.call(messages, reason) + ? reason + : "readOnly"; + return { code: codes[known] || 0, message: messages[known] }; + } + odr.onError = function (code, message) { console.error("error " + code + " message " + message); }; @@ -85,12 +94,13 @@ } function modeChange(reason) { + var refused = reason ? refusal(reason) : null; fire("onEditModeChange", { editing: editing, editable: editable, reason: reason || null, - code: reason ? refusals[reason].code : 0, - message: reason ? refusals[reason].message : "", + code: refused ? refused.code : 0, + message: refused ? refused.message : "", }); } @@ -136,14 +146,14 @@ /// seconds: four taps on a locked cell are one snackbar. @p detail is how /// the format addresses it. Painting it is the editor's. refuse: function (reason, detail) { - var refusal = refusals[reason] || refusals.readOnly; + var refused = refusal(reason); var key = reason + ":" + JSON.stringify(detail || null); var now = Date.now(); if (lastRefusal !== null && lastRefusal.key === key && now - lastRefusal.at < 2000) { return; } lastRefusal = { key: key, at: now }; - var event = { reason: reason, code: refusal.code, message: refusal.message }; + var event = { reason: reason, code: refused.code, message: refused.message }; for (var field in detail) { if (Object.prototype.hasOwnProperty.call(detail, field)) { event[field] = detail[field]; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ff1aa7777..2c2d21f61 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable(odr_test "src/document_list_test.cpp" "src/document_path_test.cpp" "src/enum_ordinals_test.cpp" + "src/error_code_test.cpp" "src/document_test.cpp" "src/file_test.cpp" "src/html_output_test.cpp" diff --git a/test/browser/plaintext/tests.html b/test/browser/plaintext/tests.html index 3ac3dd1fa..20e0f6b1a 100644 --- a/test/browser/plaintext/tests.html +++ b/test/browser/plaintext/tests.html @@ -23,6 +23,9 @@
+ + @@ -91,7 +94,7 @@ caretAt(0, 5); check("and an edit is refused", input("insertText", "X") === "refused"); - check("as read-only", refusals.pop() === "readOnly 5"); + check("as read-only", refusals.pop() === "readOnly 1005"); check("with nothing changed", lines() === "first line|second line|third line"); check("the mode turns on", odr.editing.enable() === true); @@ -122,7 +125,7 @@ ); check("a bold toggle is refused", input("formatBold") === "refused"); - check("as an edit we cannot record", refusals.pop() === "unsupportedEdit 7"); + check("as an edit we cannot record", refusals.pop() === "unsupportedEdit 1007"); check("and nothing changed", lines() === "firstXY line|second line|third line"); // ------------------------------------------------------- lines come and go diff --git a/test/browser/serve.py b/test/browser/serve.py index 5cc714a5f..648b77459 100644 --- a/test/browser/serve.py +++ b/test/browser/serve.py @@ -5,11 +5,16 @@ the file the library embeds rather than a copy of it. `checks.js` is shared by every check directory and sits here, which is the second place a name is looked up. + +`error-codes.js` is the third case: the library writes that table into the page +itself (`html/frontend.cpp::write_error_codes`), so there is no file to serve. +It is built from `odr/error_code.{hpp,cpp}` here rather than copied. """ import functools import http.server import pathlib +import re import socketserver ASSETS = ( @@ -24,8 +29,39 @@ SHARED = pathlib.Path(__file__).resolve().parent +_ERROR_CODE_DIRECTORY = pathlib.Path(__file__).resolve().parents[2] / "src" / "odr" + +#: `edit_read_only = 1005` in the header. +_EDIT_VALUE = re.compile(r"^\s*(edit_\w+)\s*=\s*(\d+)\s*,", re.MULTILINE) +#: `{ErrorCode::edit_read_only, "readOnly"}` in the table. +_EDIT_NAME = re.compile(r"\{ErrorCode::(edit_\w+),\s*\"([^\"]+)\"\}") + + +def _error_codes_js() -> bytes: + """`odr.errorCodes` as `write_error_codes` writes it.""" + values = dict( + _EDIT_VALUE.findall((_ERROR_CODE_DIRECTORY / "error_code.hpp").read_text()) + ) + names = _EDIT_NAME.findall((_ERROR_CODE_DIRECTORY / "error_code.cpp").read_text()) + body = ",\n".join(f' "{name}": {values[code]}' for code, name in names) + return ( + "window.odr = window.odr || {};\n" + f"window.odr.errorCodes = {{\n{body}\n}};\n" + ).encode() + class Handler(http.server.SimpleHTTPRequestHandler): + def do_GET(self) -> None: # noqa: N802 - the base class spells it this way + if self.path.split("?")[0].endswith("/error-codes.js"): + body = _error_codes_js() + self.send_response(200) + self.send_header("Content-Type", "text/javascript") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + super().do_GET() + def translate_path(self, path: str) -> str: translated = pathlib.Path(super().translate_path(path)) if not translated.is_file(): diff --git a/test/browser/sheet/editing.html b/test/browser/sheet/editing.html index f8392f7d9..8559533d4 100644 --- a/test/browser/sheet/editing.html +++ b/test/browser/sheet/editing.html @@ -66,6 +66,9 @@
+ + @@ -171,14 +174,14 @@ odr.editing.editAt(2, 1); editor().value = "=SUM(A1:A2)"; press(editor(), "Enter"); - check("a typed formula is refused", refusals.length === 1 && refusals[0] === "formulaInput 6 at 2,1"); + check("a typed formula is refused", refusals.length === 1 && refusals[0] === "formulaInput 1006 at 2,1"); check("and the editor stays open", editor() !== null && editor().value === "=SUM(A1:A2)"); press(editor(), "Escape"); check("the cell was not written", cell(2, 1).textContent === "" && ops().length === 1); refusals = []; check("a formula cell cannot be edited", odr.editing.editAt(1, 2) === false && editor() === null); - check("and says why", refusals.length === 1 && refusals[0] === "formula 2 at 1,2"); + check("and says why", refusals.length === 1 && refusals[0] === "formula 1002 at 1,2"); check("the cell is outlined for the tap it answers", cell(1, 2).classList.contains("odr-sheet-refused")); check("a cell holding a link neither", odr.editing.editAt(3, 2) === false); diff --git a/test/browser/sheet/keyboard.html b/test/browser/sheet/keyboard.html index 96ec9f696..063729e60 100644 --- a/test/browser/sheet/keyboard.html +++ b/test/browser/sheet/keyboard.html @@ -39,6 +39,9 @@
+ + diff --git a/test/browser/sheet/positions.html b/test/browser/sheet/positions.html index 9fd5b15a5..110276f6e 100644 --- a/test/browser/sheet/positions.html +++ b/test/browser/sheet/positions.html @@ -62,6 +62,9 @@
+ + @@ -183,7 +186,7 @@ check("a plain one carries no lock", odr.editing.lockAt(1, 2) === null); check("editing a locked cell is refused", odr.editing.refuseAt(2, 1) === true); check("a plain cell is not", odr.editing.refuseAt(1, 2) === false); - check("the refusal reached the host", refusals.length === 1 && refusals[0] === "formula 2 at 2,1"); + check("the refusal reached the host", refusals.length === 1 && refusals[0] === "formula 1002 at 2,1"); odr.editing.refuseAt(2, 1); check("and the same one again is dropped", refusals.length === 1); diff --git a/test/browser/sheet/sorting.html b/test/browser/sheet/sorting.html index 858c12cba..3494f6e1d 100644 --- a/test/browser/sheet/sorting.html +++ b/test/browser/sheet/sorting.html @@ -57,6 +57,9 @@
+ + diff --git a/test/browser/sheet/tests.html b/test/browser/sheet/tests.html index 91875d5d8..776e0e476 100644 --- a/test/browser/sheet/tests.html +++ b/test/browser/sheet/tests.html @@ -54,6 +54,9 @@
+ + diff --git a/test/browser/text/tests.html b/test/browser/text/tests.html index 3283c54d7..508d49a97 100644 --- a/test/browser/text/tests.html +++ b/test/browser/text/tests.html @@ -64,6 +64,9 @@
+ + @@ -191,7 +194,7 @@ check("the mode is off to start with", odr.editing.isEnabled() === false); select(run(11).firstChild, 3); check("and refuses an edit", input("insertText", "x") === "refused"); - check("saying the document is read-only", refusals.pop() === "readOnly 5"); + check("saying the document is read-only", refusals.pop() === "readOnly 1005"); check("with nothing changed", texts().indexOf("first run ") === 0); check( @@ -419,17 +422,17 @@ // lands, and says `range` before it looks at the input type. select(run(11).firstChild, 3); check("a bold toggle is refused", input("formatBold") === "refused"); - check("as an edit we cannot replay", refusals.pop() === "unsupportedEdit 7"); + check("as an edit we cannot replay", refusals.pop() === "unsupportedEdit 1007"); // The same reason twice within two seconds is one event, so the list has // to land in another run to be heard. select(run(13).firstChild, 3); check("so is a list", input("insertOrderedList") === "refused"); - check("under the same reason", refusals.pop() === "unsupportedEdit 7"); + check("under the same reason", refusals.pop() === "unsupportedEdit 1007"); refusals = []; select(run(11).firstChild, 3); check("a soft line break is refused", input("insertLineBreak") === "refused"); - check("by the name the reader knows", refusals.pop() === "newLine 1"); + check("by the name the reader knows", refusals.pop() === "newLine 1001"); // A paragraph holding no run has nothing an operation can name, so the // edit is taken and changes nothing rather than being refused. @@ -449,7 +452,7 @@ return input("insertText", "x") === "refused"; })() ); - check("under the same reason", refusals.pop() === "range 8"); + check("under the same reason", refusals.pop() === "range 1008"); // A range over a picture takes it away: the frame carries an address, so // `removeElement` can name it, and it holds no run to orphan. @@ -469,7 +472,7 @@ reset(); selectRuns(61, 1, 65, 3); check("a range over a text box is refused", input("insertText", "-") === "refused"); - check("because it holds text of its own", refusals.pop() === "range 8"); + check("because it holds text of its own", refusals.pop() === "range 1008"); check("and the box is still there", paragraph(60).querySelector("x-p") !== null); // A run under a link and one under a style-only wrapper are still runs. diff --git a/test/data.cmake b/test/data.cmake index beac12d1f..4abc8b93d 100644 --- a/test/data.cmake +++ b/test/data.cmake @@ -17,9 +17,9 @@ odr_test_data( odr_test_data( PATH "reference-output/odr-public" URL "https://github.com/opendocument-app/OpenDocument.test.output.git" - REVISION "ceb237a7cd3e1556a62d2a6686d051c6bf9ea479") + REVISION "2768ee0e96e8ae92caad00999be7c1380e2ecd75") odr_test_data( PATH "reference-output/odr-private" URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git" - REVISION "2e051e2e1365288aa8464e7ae977bc07340dc088") + REVISION "906f894f00945865e51c190422722d1c4c1319a0") diff --git a/test/src/error_code_test.cpp b/test/src/error_code_test.cpp new file mode 100644 index 000000000..7fa82f87f --- /dev/null +++ b/test/src/error_code_test.cpp @@ -0,0 +1,138 @@ +// `odr::ErrorCode` crosses every binding as a number a host switches on, so a +// code that moves is a wrong message on a screen, not a build failure. +// Appending is silent; renumbering fails here. + +#include +#include + +#include + +#include +#include +#include + +using namespace odr; + +namespace { + +/// One of every exception type the header declares. +std::vector> thrown_codes() { + const auto of = [](const std::exception &e) { return error_code(e); }; + return { + {of(UnsupportedOperation()), "UnsupportedOperation"}, + {of(FileNotFound()), "FileNotFound"}, + {of(UnknownFileType()), "UnknownFileType"}, + {of(FileReadError()), "FileReadError"}, + {of(FileWriteError("p")), "FileWriteError"}, + {of(NoZipFile()), "NoZipFile"}, + {of(ZipSaveError()), "ZipSaveError"}, + {of(CfbError("d")), "CfbError"}, + {of(NoCfbFile()), "NoCfbFile"}, + {of(CfbFileCorrupted()), "CfbFileCorrupted"}, + {of(NoTextFile()), "NoTextFile"}, + {of(NoCsvFile()), "NoCsvFile"}, + {of(NoMarkdownFile()), "NoMarkdownFile"}, + {of(NoJsonFile()), "NoJsonFile"}, + {of(NoImageFile()), "NoImageFile"}, + {of(NoArchiveFile()), "NoArchiveFile"}, + {of(NoDocumentFile()), "NoDocumentFile"}, + {of(NoOpenDocumentFile()), "NoOpenDocumentFile"}, + {of(NoOfficeOpenXmlFile()), "NoOfficeOpenXmlFile"}, + {of(NoPdfFile()), "NoPdfFile"}, + {of(NoFontFile()), "NoFontFile"}, + {of(NoLegacyMicrosoftFile()), "NoLegacyMicrosoftFile"}, + {of(NoIworkFile()), "NoIworkFile"}, + {of(NoXmlFile()), "NoXmlFile"}, + {of(NoSvgFile()), "NoSvgFile"}, + {of(NoRtfFile()), "NoRtfFile"}, + {of(UnsupportedCryptoAlgorithm()), "UnsupportedCryptoAlgorithm"}, + {of(NoSvmFile()), "NoSvmFile"}, + {of(MalformedSvmFile()), "MalformedSvmFile"}, + {of(UnsupportedEndian()), "UnsupportedEndian"}, + {of(MsUnsupportedCryptoAlgorithm()), "MsUnsupportedCryptoAlgorithm"}, + {of(UnknownDocumentType()), "UnknownDocumentType"}, + {of(ValueNotStated()), "ValueNotStated"}, + {of(InvalidPrefix()), "InvalidPrefix"}, + {of(DocumentCopyProtectedException()), "DocumentCopyProtected"}, + {of(ResourceNotAccessible()), "ResourceNotAccessible"}, + {of(PrefixInUse()), "PrefixInUse"}, + {of(ServerBindFailed("h", 1)), "ServerBindFailed"}, + {of(ServerAlreadyBound()), "ServerAlreadyBound"}, + {of(ServerNotBound()), "ServerNotBound"}, + {of(UnsupportedOption("o")), "UnsupportedOption"}, + {of(NullPointerError("v")), "NullPointerError"}, + {of(WrongPasswordError()), "WrongPassword"}, + {of(DecryptionFailed()), "DecryptionFailed"}, + {of(NotEncryptedError()), "NotEncrypted"}, + {of(InvalidPath("p")), "InvalidPath"}, + {of(UnsupportedFileEncoding("e")), "UnsupportedFileEncoding"}, + {of(FileEncryptedError()), "FileEncrypted"}, + {of(UnauthenticatedReadError()), "UnauthenticatedReadError"}, + }; +} + +} // namespace + +/// Moving one of these breaks an installed iOS app, which no build would +/// notice. +TEST(ErrorCode, apple_head_is_pinned) { + EXPECT_EQ(static_cast(ErrorCode::unknown), 1); + EXPECT_EQ(static_cast(ErrorCode::unsupported_operation), 2); + EXPECT_EQ(static_cast(ErrorCode::file_not_found), 3); + EXPECT_EQ(static_cast(ErrorCode::unknown_file_type), 4); + EXPECT_EQ(static_cast(ErrorCode::unsupported_file_type), 5); + EXPECT_EQ(static_cast(ErrorCode::file_read_error), 6); + EXPECT_EQ(static_cast(ErrorCode::file_write_error), 7); + EXPECT_EQ(static_cast(ErrorCode::no_document_file), 8); + EXPECT_EQ(static_cast(ErrorCode::unknown_document_type), 9); + EXPECT_EQ(static_cast(ErrorCode::unsupported_crypto_algorithm), 10); + EXPECT_EQ(static_cast(ErrorCode::wrong_password), 11); + EXPECT_EQ(static_cast(ErrorCode::decryption_failed), 12); + EXPECT_EQ(static_cast(ErrorCode::not_encrypted), 13); + EXPECT_EQ(static_cast(ErrorCode::file_encrypted), 14); + EXPECT_EQ(static_cast(ErrorCode::document_copy_protected), 15); +} + +/// What the editing scripts raise. +TEST(ErrorCode, edit_band_is_pinned) { + EXPECT_EQ(static_cast(ErrorCode::edit_new_line), 1001); + EXPECT_EQ(static_cast(ErrorCode::edit_formula), 1002); + EXPECT_EQ(static_cast(ErrorCode::edit_rich), 1003); + EXPECT_EQ(static_cast(ErrorCode::edit_shapes), 1004); + EXPECT_EQ(static_cast(ErrorCode::edit_read_only), 1005); + EXPECT_EQ(static_cast(ErrorCode::edit_formula_input), 1006); + EXPECT_EQ(static_cast(ErrorCode::edit_unsupported), 1007); + EXPECT_EQ(static_cast(ErrorCode::edit_range), 1008); + EXPECT_EQ(static_cast(ErrorCode::edit_unnameable), 1009); +} + +TEST(ErrorCode, every_code_is_named_once) { + std::set seen; + std::set names; + for (const ErrorCode code : all_error_codes()) { + EXPECT_TRUE(seen.insert(code).second) + << "duplicate code " << static_cast(code); + const std::string_view name = error_code_name(code); + EXPECT_FALSE(name.empty()); + EXPECT_TRUE(names.insert(name).second) << "duplicate name " << name; + } +} + +/// So no binding needs a catch ladder to recover one. +TEST(ErrorCode, every_exception_states_its_code) { + for (const auto &[code, name] : thrown_codes()) { + EXPECT_NE(code, ErrorCode::unknown) << name << " states no code"; + EXPECT_EQ(error_code_name(code), name); + } +} + +TEST(ErrorCode, unknown_for_anything_else) { + EXPECT_EQ(error_code(std::runtime_error("not ours")), ErrorCode::unknown); + EXPECT_EQ(error_code(Exception("the bare base")), ErrorCode::unknown); +} + +TEST(ErrorCode, bands_do_not_overlap) { + for (const auto &[code, name] : thrown_codes()) { + EXPECT_LT(code, ErrorCode::edit_new_line) << name << " is in the edit band"; + } +} diff --git a/wasm/AGENTS.md b/wasm/AGENTS.md index 38f9cb8e4..f2054a013 100644 --- a/wasm/AGENTS.md +++ b/wasm/AGENTS.md @@ -31,9 +31,9 @@ Worker**, where every value that crosses is structured-cloned. `guarded` and returns `{ok, value | error}`. The worker protocol has to turn a failure into data regardless, and an *unconverted* C++ exception reaches JS as an opaque pointer. `js/index.js` turns the envelope back into a thrown - `OdrError`, so only the wire carries envelopes. The `error.type` names come - from the same list as `jni/src/odr_jni.cpp`'s `throw_java` and - `apple/src/ODRInternal.mm`; keep the three in step. + `OdrError`, so only the wire carries envelopes. `error.type` and `error.code` + both come from `odr::ErrorCode`, which every binding reads — there is no + second list to keep in step. - **Nothing escapes as an embind handle.** A `class_`-bound wrapper cannot be structured-cloned, so a document is a `std::uint32_t` into a registry and a view an index within its session. This also dissolves the keep-alive problem @@ -64,7 +64,7 @@ Worker**, where every value that crosses is structured-cloned. wrong for a PNG or a font, so binary results go through `to_uint8_array`. - **`to_uint8_array` copies, deliberately.** A `typed_memory_view` aliases the wasm heap and `ALLOW_MEMORY_GROWTH` detaches it on the next allocation. -- **Enums cross by ordinal.** `enum_tables()` derives `FileType`, +- **Enums cross by ordinal.** `enum_tables()` derives `ErrorCode`, `FileType`, `FileCategory` and `DocumentType` from the library's own tables; the rest are listed by hand in `wasm_core.cpp` and pinned by `tests/enums.test.mjs`. Appending stays silent, reordering goes loud — the rule diff --git a/wasm/js/index.d.ts b/wasm/js/index.d.ts index 051ab2ca4..62aa4f5d4 100644 --- a/wasm/js/index.d.ts +++ b/wasm/js/index.d.ts @@ -128,6 +128,10 @@ export interface OpenOptions extends HtmlConfig { /** `name` is the C++ exception type: `WrongPassword`, `UnsupportedFileType`, … */ export declare class OdrError extends Error { + /** `odr::ErrorCode`, the number every binding reports — the same space the + * rendered page uses for `odr.onError` and `odr.onEditRefused`. Prefer it + * over `name` for anything but a log. */ + code: number; /** Set when `name` is `UnsupportedFileType`. */ fileType?: number; } diff --git a/wasm/js/index.js b/wasm/js/index.js index c81e82f5b..d577c576b 100644 --- a/wasm/js/index.js +++ b/wasm/js/index.js @@ -5,6 +5,8 @@ import createOdrModule from './odr-core.mjs'; export class OdrError extends Error { + /// `detail` carries `code` (`odr::ErrorCode`) and, for an unsupported type, + /// `fileType`. constructor(type, message, detail) { super(message); this.name = type; diff --git a/wasm/src/odr_wasm.cpp b/wasm/src/odr_wasm.cpp index 52b4a1e7a..857905158 100644 --- a/wasm/src/odr_wasm.cpp +++ b/wasm/src/odr_wasm.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -23,8 +24,8 @@ Handle &next_handle() { return instance; } -emscripten::val error_for(const std::exception &e, const std::string &type) { - return error(type, e.what()); +emscripten::val error_for(const std::exception &e) { + return error(error_code(e), e.what()); } } // namespace @@ -66,9 +67,13 @@ emscripten::val ok(emscripten::val value) { emscripten::val ok() { return ok(emscripten::val::undefined()); } -emscripten::val error(const std::string &type, const std::string &message) { +emscripten::val error(const ErrorCode code, const std::string &message) { emscripten::val detail = emscripten::val::object(); - detail.set("type", type); + // The catch-all keeps the name `js/index.js` gives the thrown error. + detail.set("type", code == ErrorCode::unknown + ? std::string("OdrError") + : std::string(error_code_name(code))); + detail.set("code", static_cast(code)); detail.set("message", message); emscripten::val result = emscripten::val::object(); @@ -80,42 +85,16 @@ emscripten::val error(const std::string &type, const std::string &message) { emscripten::val current_exception_error() { try { throw; - } catch (const UnsupportedOperation &e) { - return error_for(e, "UnsupportedOperation"); - } catch (const FileNotFound &e) { - return error_for(e, "FileNotFound"); - } catch (const UnknownFileType &e) { - return error_for(e, "UnknownFileType"); } catch (const UnsupportedFileType &e) { // the only error carrying a payload the caller acts on: a viewer names the // format it cannot show - emscripten::val result = error_for(e, "UnsupportedFileType"); + emscripten::val result = error_for(e); result["error"].set("fileType", static_cast(e.file_type)); return result; - } catch (const FileReadError &e) { - return error_for(e, "FileReadError"); - } catch (const FileWriteError &e) { - return error_for(e, "FileWriteError"); - } catch (const NoDocumentFile &e) { - return error_for(e, "NoDocumentFile"); - } catch (const UnknownDocumentType &e) { - return error_for(e, "UnknownDocumentType"); - } catch (const UnsupportedCryptoAlgorithm &e) { - return error_for(e, "UnsupportedCryptoAlgorithm"); - } catch (const WrongPasswordError &e) { - return error_for(e, "WrongPassword"); - } catch (const DecryptionFailed &e) { - return error_for(e, "DecryptionFailed"); - } catch (const NotEncryptedError &e) { - return error_for(e, "NotEncrypted"); - } catch (const FileEncryptedError &e) { - return error_for(e, "FileEncrypted"); - } catch (const DocumentCopyProtectedException &e) { - return error_for(e, "DocumentCopyProtected"); } catch (const std::exception &e) { - return error_for(e, "OdrError"); + return error_for(e); } catch (...) { - return error("OdrError", "unknown native error"); + return error(ErrorCode::unknown, "unknown native error"); } } diff --git a/wasm/src/odr_wasm.hpp b/wasm/src/odr_wasm.hpp index 279aede1c..e98bfbefb 100644 --- a/wasm/src/odr_wasm.hpp +++ b/wasm/src/odr_wasm.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -44,10 +45,9 @@ void clear_sessions() noexcept; emscripten::val ok(emscripten::val value); emscripten::val ok(); -/// `{ok: false, error: {type, message, ...}}`, with `type` naming the C++ -/// exception. Kept in step with `jni/src/odr_jni.cpp`'s `throw_java` and -/// `apple/src/ODRInternal.mm`. -emscripten::val error(const std::string &type, const std::string &message); +/// `{ok: false, error: {type, code, message, ...}}`. Both name the same +/// @ref odr::ErrorCode, which is where every binding gets them. +emscripten::val error(ErrorCode code, const std::string &message); /// The envelope for the exception being handled. Call from a `catch` block. emscripten::val current_exception_error(); diff --git a/wasm/src/wasm_core.cpp b/wasm/src/wasm_core.cpp index 437678fbd..050014f47 100644 --- a/wasm/src/wasm_core.cpp +++ b/wasm/src/wasm_core.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -52,10 +53,9 @@ emscripten::val file_types() { } /// Enum name to ordinal, so the JS side never restates an ordinal by hand. -/// `FileType`, `FileCategory`, `DocumentType` and `TextEncoding` are derived -/// from the library's tables and cannot drift; the rest have no runtime table -/// and are listed here, -/// pinned by `tests/enums.test.mjs`. +/// `ErrorCode`, `FileType`, `FileCategory`, `DocumentType` and `TextEncoding` +/// are derived from the library's tables and cannot drift; the rest have no +/// runtime table and are listed here, pinned by `tests/enums.test.mjs`. emscripten::val enum_tables() { const auto table = [](const auto &...entries) { emscripten::val result = emscripten::val::object(); @@ -96,7 +96,14 @@ emscripten::val enum_tables() { static_cast(encoding)); } + emscripten::val error_code = emscripten::val::object(); + for (const ErrorCode code : odr::all_error_codes()) { + error_code.set(std::string(odr::error_code_name(code)), + static_cast(code)); + } + emscripten::val result = emscripten::val::object(); + result.set("ErrorCode", error_code); result.set("FileType", file_type); result.set("TextEncoding", text_encoding); result.set("FileCategory", file_category); diff --git a/wasm/src/wasm_html.cpp b/wasm/src/wasm_html.cpp index bfed564e5..6de09a3e4 100644 --- a/wasm/src/wasm_html.cpp +++ b/wasm/src/wasm_html.cpp @@ -91,7 +91,8 @@ emscripten::val render_view(const Handle handle, const std::size_t index) { return guarded([&] { const Session &s = warm(handle); if (index >= s.views.size()) { - return error("OdrError", "no such view index: " + std::to_string(index)); + return error(ErrorCode::unknown, + "no such view index: " + std::to_string(index)); } std::ostringstream out; @@ -125,7 +126,8 @@ emscripten::val read_path(const Handle handle, const std::string &path) { return guarded([&] { const Session &s = warm(handle); if (!s.service->exists(path)) { - return error("FileNotFound", "no such path in the document: " + path); + return error(ErrorCode::file_not_found, + "no such path in the document: " + path); } std::ostringstream out; diff --git a/wasm/tests/enums.test.mjs b/wasm/tests/enums.test.mjs index b6ad45c21..f18f6e6ee 100644 --- a/wasm/tests/enums.test.mjs +++ b/wasm/tests/enums.test.mjs @@ -83,4 +83,21 @@ describe('enums', () => { assert.equal(typeof enums.DocumentType.text, 'number'); assert.equal(typeof enums.FileCategory.document, 'number'); }); + + // The head keeps the order `ODRError` shipped, so an installed iOS app does + // not read a moved number; the edit band is what the rendered page raises. + it('derives ErrorCode, both bands pinned', () => { + assert.equal(enums.ErrorCode.Unknown, 1); + assert.equal(enums.ErrorCode.UnsupportedFileType, 5); + assert.equal(enums.ErrorCode.WrongPassword, 11); + assert.equal(enums.ErrorCode.DocumentCopyProtected, 15); + + assert.equal(enums.ErrorCode.newLine, 1001); + assert.equal(enums.ErrorCode.formula, 1002); + assert.equal(enums.ErrorCode.readOnly, 1005); + assert.equal(enums.ErrorCode.formulaInput, 1006); + assert.equal(enums.ErrorCode.unsupportedEdit, 1007); + assert.equal(enums.ErrorCode.range, 1008); + assert.equal(enums.ErrorCode.unnameableEdit, 1009); + }); });